/* * $Workfile: JavaPurse.java $ $Revision: 1 $, $Date: 2/10/98 3:43p $ * * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved. * * This software is the confidential and proprietary information of Sun * Microsystems, Inc. ("Confidential Information"). You shall not * disclose such Confidential Information and shall use it only in * accordance with the terms of the license agreement you entered into * with Sun. * * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THIS SOFTWARE OR ITS DERIVATIVES. */ // /* // $Workfile: JavaPurse.java $ // $Revision: 1 $ // $Date: 2/10/98 3:43p $ // $Author: Vadim $ // $Archive: /Releases/L 980212 JC20-DR2-P/Input/Sample-Code/src/JavaPurse/JavaPurse.java $ // $Modtime: 12/19/97 2:54p $ // Original author: Vadim Temkin // */ package applets.JavaPurse; import javacard.framework.*; import javacardx.framework.*; /** * Generic Purse service. * * @author Vadim Temkin */ public class JavaPurse extends Applet { /* * * Constants * */ final static byte PURSE_CLA = (byte)0x80; //CLA value for Java Purse final static byte ISO_CLA = (byte)0x00; //CLA value for ISO 7816-4 file commands final static byte SELECT = (byte)0xA4; //INS value for ISO 7816-4 SELECT command final static byte VERIFY = (byte)0x20; //INS value for ISO 7816-4 VERIFY command final static byte INITIALIZE_TRANSACTION = (byte)0x20; //INS byte for Initialize Transaction final static byte COMPLETE_TRANSACTION = (byte)0x22; //INS byte for Complete Transaction final static byte INITIALIZE_UPDATE = (byte)0x24; //INS byte for Initialize Parameter Update final static byte COMPLETE_UPDATE = (byte)0x26; //INS byte for Complete Parameter Update final static byte CREDIT = (byte)0x01; //P1 byte for Credit in Initialize Transaction final static byte DEBIT = (byte)0x02; //P1 byte for Debit in Initialize Transaction final static byte MASTER_PIN = (byte)0x81; //P2 byte for Master PIN in Verify final static byte USER_PIN = (byte)0x82; //P2 byte for User PIN in Verify final static short SW_CREDIT_TOO_HIGH = (short)0x9101;//SW bytes for Credit Too High condition final static short SW_NOT_ENOUGH_FUNDS = (short)0x9102;//SW bytes for Not Enough Funds condition final static short SW_AMOUNT_TOO_HIGH = (short)0x9103;//SW bytes for Amount Too High condition final static short SW_COMMAND_OUT_OF_SEQUENCE = (short)0x9104;//SW bytes for Command out of Sequence final static short SW_WRONG_SIGNATURE = (short)0x9105;//SW bytes for Wrong Signature condition final static short SW_PIN_FAILED = (short)0x69C0;//SW bytes for PIN Failed condition //The last nibble is replaced with the //number of remaining tries final static byte LC_IT = 10; //Lc byte for Initialize Transaction final static byte LC_CT = 13; //Lc byte for Complete Transaction final static byte LC_CU_MIN = 18; //Lc byte for Complete Update final static byte CAD_ID_OFFSET = 7; //Offset for CAD ID in Process Initialize Transaction final static short DATE_LENGTH = 3; //Length of ExpDate array final static short DATETIME_LENGTH = 5;//Length of DateTime array final static short ID_LENGTH = 4; //Length for CAD ID and Purse ID arrays final static short SHORT_LENGTH= 2; //Length of a short value for offset computations final static short START = 0; //For offset computations final static short SIGNATURE_LENGTH = 8; //Length of signatures JavaPurseTransientVars transientVars; FileSystem fs; LinearFixedFile balancesFile; LinearVariableFile parametersFile; CyclicFile transactionLogFile; short TN; short PUN; /* * File System constants */ final static short PARAMETERS_FID = (short) 0x9102; final static short TRANSACTION_LOG_FID = (short) 0x9103; final static short BALANCES_FID = (short) 0x9104; final static byte TRANSACTION_RECORD_LENGTH = 18; final static byte TRANSACTION_RECORD_NUMBER = 10; final static byte BALANCES_RECORD_LENGTH = 6; final static byte BALANCES_RECORD_NUMBER = 1; final static byte PARAMETERS_RECORD_NUMBER = 11; final static byte OFFSET_BAL_CURRENT = 0; final static byte OFFSET_BAL_MAX = 2; final static byte OFFSET_AMOUNT_MAX = 4; final static byte NUMBER_OF_FILES = 3; OwnerPIN masterPIN; OwnerPIN userPIN; final static byte MASTER_PIN_UPDATE = (byte)0xC1; final static byte USER_PIN_UPDATE = (byte)0xC2; final static byte EXP_DATE_UPDATE = (byte)0xC5; final static byte PURSE_ID_UPDATE = (byte)0xC6; final static byte MAX_BAL_UPDATE = (byte)0xC7; final static byte MAX_M_UPDATE = (byte)0xC8; final static byte VERSION_UPDATE = (byte)0xC9; final static short TLV_OFFSET = 13; //Offset of TLV in Complete Parameter Update /* * * Various and sundry byte arrays * */ byte[] CAD_ID_array; byte[] byteArray8; static byte[] ID_Purse = {(byte)0, (byte)0, (byte)0, (byte)0}; static byte[] ExpDate = {(byte)12, (byte)31, (byte)99}; /** * Installs Java Purse applet. * @param apdu APDU */ public static void install(APDU apdu) { new JavaPurse(apdu); } /** * Does memory allocations, initializes crypto and creates file system */ protected JavaPurse(APDU apdu) { TN = 0; PUN = 0; //Initialize transient objects. transientVars = new JavaPurseTransientVars(); javacard.framework.System.makeTransient(transientVars, javacard.framework.System.TRANSIENT_SESSION); CAD_ID_array = new byte[4]; javacard.framework.System.makeTransient(CAD_ID_array, javacard.framework.System.TRANSIENT_SESSION); byteArray8 = new byte[8]; javacard.framework.System.makeTransient(byteArray8, javacard.framework.System.TRANSIENT_SESSION); masterPIN = new OwnerPIN ((byte)1, (byte)8); //There is only one try - it's not supposed to be done by human operator userPIN = new OwnerPIN ((byte)5, (byte)8); //Initialize File system stuff fs = new FileSystem(NUMBER_OF_FILES); parametersFile = new LinearVariableFile(PARAMETERS_FID, PARAMETERS_RECORD_NUMBER); fs.addChildFile(parametersFile); parametersFile.setSecurity(File.ACCESS_READ, File.ALLOW_AUTH2);//Reading is controlled by Master PIN parametersFile.setSecurity(File.ACCESS_WRITE, File.ALLOW_NONE);//No writing from outside is allowed transactionLogFile = new CyclicFile(TRANSACTION_LOG_FID, TRANSACTION_RECORD_NUMBER, TRANSACTION_RECORD_LENGTH); fs.addChildFile(transactionLogFile); transactionLogFile.setSecurity(File.ACCESS_READ, File.ALLOW_AUTH1);//Reading is controlled by User PIN transactionLogFile.setSecurity(File.ACCESS_WRITE, File.ALLOW_NONE); //No writing from outside is allowed //Create balancesFile and initialize Current Balance to 0 balancesFile = new LinearFixedFile(BALANCES_FID, BALANCES_RECORD_NUMBER, BALANCES_RECORD_LENGTH); fs.addChildFile(balancesFile); balancesFile.setSecurity(File.ACCESS_READ, File.ALLOW_AUTH1);//Reading is controlled by User PIN balancesFile.setSecurity(File.ACCESS_WRITE, File.ALLOW_NONE); //No writing from outside is allowed balancesFile.addRecord(BALANCES_RECORD_LENGTH); byte[] balancesRecord = balancesFile.getRecord((byte)1); Util.setShort(balancesRecord, OFFSET_BAL_CURRENT, (short)0); register(); } /** * Performs the session initialization. */ public boolean select() { //This let us distinguish between APDU for file selection and APDU //received upon selection of the applet itself. transientVars.purseJustSelected = true; //These variables keep track of execution sequence. //When transient objects work correctly one doesn't need these //initializations in select() method transientVars.transactionInitialized = false; transientVars.updateInitialized = false; return true; } /** * Performs the session finalization. */ public void deselect() { userPIN.reset(); masterPIN.reset(); fs.reset(); } /** * Dispatches APDU commands. * @param apdu APDU */ public void process(APDU apdu) { byte[] buffer = apdu.getBuffer(); if (buffer[ISO.OFFSET_CLA] == PURSE_CLA) { switch (buffer[ISO.OFFSET_INS]) { case INITIALIZE_TRANSACTION: processInitializeTransaction(apdu); break; case COMPLETE_TRANSACTION: processCompleteTransaction(apdu); break; case INITIALIZE_UPDATE: processInitializeUpdate(apdu); break; case COMPLETE_UPDATE: processCompleteUpdate(apdu); break; default: ISOException.throwIt(ISO.SW_INS_NOT_SUPPORTED); } } else if (buffer[ISO.OFFSET_CLA] == ISO_CLA) { if (buffer[ISO.OFFSET_INS] == VERIFY){ processVerifyPIN(apdu); } else if ((buffer[ISO.OFFSET_INS] == SELECT) & transientVars.purseJustSelected) { processSelectPurse(apdu); } else { //All other ISO compatible instructions are processed by FileSystem if (!fs.process(apdu)) ISOException.throwIt(ISO.SW_INS_NOT_SUPPORTED); } } else ISOException.throwIt(ISO.SW_CLA_NOT_SUPPORTED); } /** * Handles Select Purse APDU. * @param apdu APDU */ void processSelectPurse(APDU apdu) { transientVars.purseJustSelected = false; // applet might return the FCI here } /** * Handles Initialize Transaction APDU. * @param apdu APDU */ void processInitializeTransaction(APDU apdu) { // if (transientVars.transactionInitialized) // ISOException.throwIt(SW_COMMAND_OUT_OF_SEQUENCE); if (!userPIN.isValidated()) ISOException.throwIt(ISO.SW_PIN_REQUIRED); byte[] buffer = apdu.getBuffer(); if (buffer[ISO.OFFSET_LC] != LC_IT) ISOException.throwIt(ISO.SW_WRONG_LENGTH); if (buffer[ISO.OFFSET_P2] != 0) ISOException.throwIt(ISO.SW_INCORRECT_P1P2); apdu.setIncomingAndReceive(); // get expected data byte transactionType = buffer[ISO.OFFSET_P1]; transientVars.transactionType = transactionType; short amount = Util.getShort(buffer, ISO.OFFSET_CDATA); transientVars.amount = amount; short balance = checkTransactionValues(transactionType, amount); // Increment TN in Transient Memory & compute signature short newTN = (short)(TN + 1); transientVars.TN = newTN; Util.arrayCopy(buffer, CAD_ID_OFFSET, CAD_ID_array, START, ID_LENGTH); // The crypo processing could be done here Util.arrayFillNonAtomic(byteArray8, (byte)0); // Send R-APDU short offset = START; Util.arrayCopy(ID_Purse, START, buffer, offset, ID_LENGTH); offset += (short)ID_LENGTH; Util.arrayCopy(ExpDate, START, buffer, offset, DATE_LENGTH); offset += DATE_LENGTH; Util.setShort(buffer, offset, balance); offset += SHORT_LENGTH; Util.setShort(buffer, offset, newTN); offset += SHORT_LENGTH; Util.arrayCopy(byteArray8, START, buffer, offset, SIGNATURE_LENGTH); short length = (short) (ID_LENGTH + DATE_LENGTH + (2 * SHORT_LENGTH) + SIGNATURE_LENGTH); apdu.setOutgoingAndSend(START, length); transientVars.transactionInitialized = true; } /** * Handles Complete Transaction APDU. * @param apdu APDU */ void processCompleteTransaction(APDU apdu) { if (!transientVars.transactionInitialized) ISOException.throwIt(SW_COMMAND_OUT_OF_SEQUENCE); byte[] buffer = apdu.getBuffer(); if (buffer[ISO.OFFSET_LC] != LC_CT) ISOException.throwIt(ISO.SW_WRONG_LENGTH); if ((buffer[ISO.OFFSET_P1] != 0) | (buffer[ISO.OFFSET_P2] != 0)) ISOException.throwIt(ISO.SW_INCORRECT_P1P2); apdu.setIncomingAndReceive(); // get expected data //restore transaction data from transient short newTN = transientVars.TN; short amount = transientVars.amount; short newBalance = transientVars.newBalance; //The signature verification could be here Util.arrayFillNonAtomic(byteArray8, (byte)0); boolean signatureOK = (0 == Util.arrayCompare(buffer, (short)ISO.OFFSET_CDATA, byteArray8, START, SIGNATURE_LENGTH)); //prepare transaction record in APDU buffer short offset = START; Util.setShort(buffer, offset, newTN); offset += SHORT_LENGTH; buffer[offset] = transientVars.transactionType; offset++; Util.setShort(buffer, offset, amount); offset += SHORT_LENGTH; //CAD ID was left in this array from Initialize Transaction Util.arrayCopy(CAD_ID_array, START, buffer, offset, ID_LENGTH); offset += ID_LENGTH; //Date and time are copied in APDU buffer to where they should go //in the transaction record. Util.arrayCopy(buffer, (short)(ISO.OFFSET_CDATA + 8), buffer, offset, DATETIME_LENGTH); offset += DATETIME_LENGTH; //Balance and SW will be added to transactionRecord later if (!signatureOK){ //Branch for unsuccessful transaction. Balance is not updated, //otherwise transactionLog is recorded the same way as in successful transaction Util.setShort(buffer, offset, transientVars.currentBalance); // old balance offset += SHORT_LENGTH; Util.setShort(buffer, offset, SW_WRONG_SIGNATURE); //done with preparing transaction record //The following few steps have to be performed atomically! javacard.framework.System.beginTransaction(); TN = newTN; byte[] theRecord = transactionLogFile.getNewFirstRecord(); Util.arrayCopy(buffer, START, theRecord, START, TRANSACTION_RECORD_LENGTH); javacard.framework.System.commitTransaction(); //Now we can throw exception transientVars.transactionInitialized = false; ISOException.throwIt(SW_WRONG_SIGNATURE); } else { //Branch for successful transaction. Util.setShort(buffer, offset, transientVars.newBalance); offset += SHORT_LENGTH; Util.setShort(buffer, offset, ISO.SW_NO_ERROR); // done with preparing transaction record //The following few steps have to be performed atomically! javacard.framework.System.beginTransaction(); TN = transientVars.TN; //Update balance byte[] balances = balancesFile.getRecord((byte)1); Util.setShort(balances, START, newBalance); byte[] theRecord = transactionLogFile.getNewFirstRecord(); Util.arrayCopy(buffer, START, theRecord, START, TRANSACTION_RECORD_LENGTH); javacard.framework.System.commitTransaction(); } //Put all '0's in signature3 (there could be crypto calculations here) Util.arrayFillNonAtomic(byteArray8, (byte)0); //send R-APDU offset = START; Util.setShort(buffer, offset, newBalance); offset += SHORT_LENGTH; Util.arrayCopy(byteArray8, START, buffer, offset, SIGNATURE_LENGTH); short length = (short)(SHORT_LENGTH + SIGNATURE_LENGTH); apdu.setOutgoingAndSend(START, length); transientVars.transactionInitialized = false; } /** * Handles Initialize Parameter Update APDU. * @param apdu APDU */ void processInitializeUpdate(APDU apdu) { if (transientVars.updateInitialized) ISOException.throwIt(SW_COMMAND_OUT_OF_SEQUENCE); if (!masterPIN.isValidated()) ISOException.throwIt(ISO.SW_PIN_REQUIRED); byte[] buffer = apdu.getBuffer(); if ((buffer[ISO.OFFSET_P1] != 0) | (buffer[ISO.OFFSET_P2] != 0)) ISOException.throwIt(ISO.SW_INCORRECT_P1P2); if (buffer[ISO.OFFSET_LC] != 0) ISOException.throwIt(ISO.SW_WRONG_LENGTH); PUN++; //Increment parameter Update Number // Send R-APDU short offset = START; Util.arrayCopy(ID_Purse, START, buffer, offset, ID_LENGTH); offset += ID_LENGTH; Util.arrayCopy(ExpDate, START, buffer, offset, DATE_LENGTH); offset += DATE_LENGTH; Util.setShort(buffer, offset, PUN); short length = (short) (ID_LENGTH + DATE_LENGTH + SHORT_LENGTH); apdu.setOutgoingAndSend(START, length); transientVars.updateInitialized = true; } /** * Handles Complete Parameter Update APDU. * @param apdu APDU */ void processCompleteUpdate(APDU apdu) { if (!transientVars.updateInitialized) ISOException.throwIt(SW_COMMAND_OUT_OF_SEQUENCE); byte[] buffer = apdu.getBuffer(); if ((buffer[ISO.OFFSET_P1] != 0) | (buffer[ISO.OFFSET_P2] != 0)) ISOException.throwIt(ISO.SW_INCORRECT_P1P2); short count = apdu.setIncomingAndReceive(); // get expected data byte lc = buffer[ISO.OFFSET_LC]; short offset; //Put all '0's in byteArray8 and compare with buffer //(there could be crypto calculations here) Util.arrayFillNonAtomic(byteArray8, (byte)0); if (0 != Util.arrayCompare(byteArray8, START, buffer, (short)(ISO.OFFSET_CDATA + lc - SIGNATURE_LENGTH), SIGNATURE_LENGTH)) ISOException.throwIt(SW_WRONG_SIGNATURE); offset = TLV_OFFSET; switch (buffer[offset]) { case MASTER_PIN_UPDATE: updatePIN(apdu, masterPIN); break; case USER_PIN_UPDATE: updatePIN(apdu, userPIN); break; case EXP_DATE_UPDATE: updateParameterValue(apdu, ExpDate); break; case PURSE_ID_UPDATE: updateParameterValue(apdu, ID_Purse); break; case MAX_BAL_UPDATE: updateBalanceValue(apdu, OFFSET_BAL_MAX); break; case MAX_M_UPDATE: updateBalanceValue(apdu, OFFSET_AMOUNT_MAX); break; case VERSION_UPDATE: updateParametersFile(apdu); break; default: ISOException.throwIt(ISO.SW_FUNC_NOT_SUPPORTED); } // The crypo processing could be done here Util.arrayFillNonAtomic(byteArray8, (byte)0); Util.arrayCopy(byteArray8, START, buffer, START, SIGNATURE_LENGTH); apdu.setOutgoingAndSend(START, SIGNATURE_LENGTH); transientVars.updateInitialized = false; } /** * Handles Verify Pin APDU. * @param apdu APDU */ void processVerifyPIN(APDU apdu) { byte[] buffer = apdu.getBuffer(); byte pinLength = buffer[ISO.OFFSET_LC]; byte triesRemaining = (byte)0; short count = apdu.setIncomingAndReceive(); // get expected data if (count < pinLength) ISOException.throwIt(ISO.SW_WRONG_LENGTH); byte pinType = buffer[ISO.OFFSET_P2]; switch (pinType) { case MASTER_PIN: if (!masterPIN.check(buffer, ISO.OFFSET_CDATA, pinLength)){ triesRemaining = masterPIN.getTriesRemaining(); } else { fs.setAuthFlag((byte)2, true); return; } break; case USER_PIN: if (!userPIN.check(buffer, ISO.OFFSET_CDATA, pinLength)){ triesRemaining = userPIN.getTriesRemaining(); } else { fs.setAuthFlag((byte)1, true); return; } break; default: ISOException.throwIt(ISO.SW_INCORRECT_P1P2); } //The last nibble of return code is number of remaining tries ISOException.throwIt((short)(SW_PIN_FAILED + triesRemaining)); } /** * Verifies numerical limitations on Transaction Amount. * @param amount Transaction Amount */ short checkTransactionValues(byte transactionType, short amount) { short newBalance; byte[] balances = balancesFile.getRecord((byte)1); short currentBalance = Util.getShort(balances, OFFSET_BAL_CURRENT); short maxBalance = Util.getShort(balances, OFFSET_BAL_MAX); short maxAmount = Util.getShort(balances, OFFSET_AMOUNT_MAX); switch (transactionType) { case CREDIT : { newBalance = (short)(currentBalance + amount); transientVars.newBalance = newBalance; if (newBalance > maxBalance) ISOException.throwIt(SW_CREDIT_TOO_HIGH); break; } case DEBIT : { if (amount > maxAmount) ISOException.throwIt(SW_AMOUNT_TOO_HIGH); newBalance = (short)(currentBalance - amount); transientVars.newBalance = newBalance; if (newBalance < 0)ISOException.throwIt(SW_NOT_ENOUGH_FUNDS); break; } default : ISOException.throwIt(ISO.SW_INCORRECT_P1P2); } transientVars.currentBalance = currentBalance; return currentBalance; } void updatePIN(APDU apdu, OwnerPIN PIN){ byte[] buffer = apdu.getBuffer(); PIN.updateAndUnblock(buffer, (short)(TLV_OFFSET + 2), buffer[TLV_OFFSET + 1]); } void updateParameterValue(APDU apdu, byte[] value){ byte[] buffer = apdu.getBuffer(); Util.arrayCopy(buffer, (short)(TLV_OFFSET + 2), value, START, buffer[TLV_OFFSET + 1]); updateParametersFile(apdu); } void updateBalanceValue(APDU apdu, short offset){ byte[] buffer = apdu.getBuffer(); byte[] balancesRecord = balancesFile.getRecord((byte)1); Util.arrayCopy(buffer, (short)(TLV_OFFSET + 2), balancesRecord, offset, SHORT_LENGTH); } void updateParametersFile(APDU apdu){ byte[] buffer = apdu.getBuffer(); byte recordNumber = parametersFile.findRecord(LinearVariableFile.DIRECTION_FIRST, (byte)1, //Start from the first record buffer[TLV_OFFSET], //match tag (byte)0); //ignore the second byte of record if (recordNumber == (byte)0) { //The record is not found. We have to create a new record byte[] newRecord = new byte[buffer[TLV_OFFSET + 1] + 2]; Util.arrayCopy(buffer, TLV_OFFSET, newRecord, START, (short)(buffer[TLV_OFFSET + 1] + 2)); parametersFile.addRecord(newRecord); } else { byte[] theRecord = parametersFile.getRecord(recordNumber); Util.arrayCopy(buffer, TLV_OFFSET, theRecord, START, (short)(buffer[TLV_OFFSET + 1] + 2)); } } }