TransitAppletpublic class TransitApplet extends javacard.framework.Applet This applet implements the on-card part of a transit system solution. The
on-card applet and the off-card applications (transit terminal and POS
terminal) use a mutual authentication scheme based on a dynamically generated
DES session key to ensure data integrity and origin authentication during a
session.
When interacting with a POS terminal, the account maintained on the card can
be credited or queried for the current balance.
When interacting with a transit terminal, the transit system entry and the
exit events are checked for consistency and processed - the account
maintained on the card is debited upon proper exit from the transit system.
Design notes:
- This sample transit applet does not account for any admin or self-admin use cases such as
resetting the card of a transit system user when it is in an inconsistent transit
state. Such an inconsistent state can, for example, result from the user jumping the gates when
the turnstile is out of order...
- This sample transit applet does not account for any system-wide transactional
operations. For example, during a credit operation, if the user removes his card
just after the balance has been updated but before the APDU response gets to
the terminal, the account on the card will remain credited but the terminal will
only be able to detect an IO error b/w the card and the card reader.
- The constants defined for this class should have been shared through
an additional class or interface with the terminal code
(see com.sun.javacard.clientsamples.transit.Constants).
- This applet could be refactored so that the mutual authentication code
be moved in a base abstract class and the transit system specific behavior be
implemented in a subclass of this base class. This refactoring would facilitate
the reuse of the mutual authentication scheme in other application domain. |
Fields Summary |
---|
static final byte | VERIFYINS value for ISO 7816-4 VERIFY command | static final byte | INITIALIZE_SESSIONINS value for INITIALIZE_SESSION command | static final byte | PROCESS_REQUESTINS value for PROCESS_REQUEST command | static final byte | PROCESS_ENTRYTLV Tag for PROCESS_ENTRY request | static final byte | PROCESS_EXITTLV Tag for PROCESS_EXIT request | static final byte | CREDITTLV Tag for CREDIT request | static final byte | GET_BALANCETLV Tag for GET_BALANCE request | static final short | TLV_TAG_OFFSETTLV tag offset | static final short | TLV_LENGTH_OFFSETTLV length offset | static final short | TLV_VALUE_OFFSETTLV value offset | static final short | MAX_BALANCEMaximum allowed balance | static final short | MIN_TRANSIT_BALANCEMinimum balance to start transit | static final short | MAX_CREDIT_AMOUNTMaximum amount to be credited | static final byte | MAX_PIN_TRIESMaximum number of incorrect tries before the PIN is blocked | static final byte | MAX_PIN_SIZEMaximum PIN size | static final short | SW_VERIFICATION_FAILEDSW bytes for PIN verification failure | static final short | SW_PIN_VERIFICATION_REQUIREDSW bytes for PIN validation required | static final short | SW_INVALID_TRANSACTION_AMOUNTSW bytes for invalid credit amount (amount > MAX_CREDIT_AMOUNT or amount <
0) | static final short | SW_EXCEED_MAXIMUM_BALANCESW bytes for maximum balance exceeded | static final short | SW_NEGATIVE_BALANCESW bytes for negative balance reached | static final short | SW_WRONG_SIGNATURESW bytes for wrong signature condition | static final short | SW_MIN_TRANSIT_BALANCESW bytes for minimum transit balance not met | static final short | SW_INVALID_TRANSIT_STATESW bytes for invalid transit state | static final short | SW_SUCCESSSW bytes for success, used in MAC | static final short | UID_LENGTHUnique ID length | static final short | LENGTH_DES_BYTEDES key length in bytes | static final short | CHALLENGE_LENGTHHost and card challenge length (note: (2 * CHALLENGE_LENGTH) * 8 ==
KeyBuilder.LENGTH_DES | static final short | MAC_LENGTHMAC length as generated by Signature.ALG_DES_MAC8_ISO9797_M2 | private byte[] | uidUnique ID | private javacardx.crypto.Cipher | cipherCipher used to encrypt - using the static DES key - the derivation data
to form the session key | private javacard.security.DESKey | staticKeyDES static key, shared b/w host and card | private byte[] | cardChallenge4-bytes Card challenge | private byte[] | keyDerivationData8-bytes key derivation data, generated from the host challenge and the
card challenge | private byte[] | sessionKeyData8-bytes session key data, generated from the derivation data | private javacard.security.DESKey | sessionKeyDES session key, generated from the derivation data | private boolean | useTransientKeyIndicates whether or not to use transient session key - for performance
measurement only | private javacard.security.Signature | signatureSignature initialized with the DES key and used to verify incoming
messages and to sign outgoing messages | private javacard.security.RandomData | randomRandom data generator, used to generate the card challenge | private javacard.framework.OwnerPIN | pinThe user PIN | private short | balanceThe balance | private short | entryStationIdThe entry ststion id, set to (-1) when not in transit | private byte | correlationIdA correlation id that may be used by the backend system to correlate
entry and exit events |
Constructors Summary |
---|
protected TransitApplet(byte[] bArray, short bOffset, byte bLength)Creates a new Transit applet instance.
// Create static DES key
staticKey = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES,
KeyBuilder.LENGTH_DES, false);
// Create cipher
cipher = Cipher.getInstance(Cipher.ALG_DES_CBC_ISO9797_M2, false);
// Create card challenge transient buffer
cardChallenge = JCSystem.makeTransientByteArray(CHALLENGE_LENGTH,
JCSystem.CLEAR_ON_DESELECT);
// Create key derivation data transient buffer
keyDerivationData = JCSystem.makeTransientByteArray(
(short) (2 * CHALLENGE_LENGTH), JCSystem.CLEAR_ON_DESELECT);
// Create session key data transient buffer
sessionKeyData = JCSystem.makeTransientByteArray(
(short) (2 * keyDerivationData.length),
JCSystem.CLEAR_ON_DESELECT);
// XXX: Allocates more than actual key to contain the complete
// encrypted key derivation data
// Create signature
signature = Signature.getInstance(Signature.ALG_DES_MAC8_ISO9797_M2,
false);
byte aidLen = bArray[bOffset]; // aid length
if (aidLen == (byte) 0) {
register();
} else {
register(bArray, (short) (bOffset + 1), aidLen);
}
// Ignore control info
bOffset = (short) (bOffset + aidLen + 1);
byte infoLen = bArray[bOffset]; // control info length
bOffset = (short) (bOffset + infoLen + 1);
byte paramLen = bArray[bOffset++]; // applet parameters length
// Retrieve UID, static key data and the PIN initialization values from
// installation parameters
if (paramLen <= (LENGTH_DES_BYTE + UID_LENGTH)
|| paramLen > (LENGTH_DES_BYTE + UID_LENGTH + MAX_PIN_SIZE)) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Retrieve the UID
uid = new byte[UID_LENGTH];
Util.arrayCopy(bArray, bOffset, uid, (short) 0, UID_LENGTH);
bOffset += UID_LENGTH;
// Retrieve the static key data
staticKey.setKey(fixParity(bArray, bOffset, LENGTH_DES_BYTE), bOffset);
bOffset += LENGTH_DES_BYTE;
// Retrieve the flag indicating whether or not to use a transient key
useTransientKey = (bArray[bOffset] != (byte) 0);
bOffset++;
// Retrieve the PIN
pin = new OwnerPIN(MAX_PIN_TRIES, MAX_PIN_SIZE);
pin.update(bArray, bOffset,
(byte) (paramLen - UID_LENGTH - LENGTH_DES_BYTE - 1));
// Create transient DES session key
if (useTransientKey) {
sessionKey = (DESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_DES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_DES,
false);
} else {
sessionKey = (DESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES,
false);
}
// Create and initialize the ramdom data generator with the UID (seed)
random = RandomData.getInstance(RandomData.ALG_PSEUDO_RANDOM);
random.setSeed(uid, (short) 0, UID_LENGTH);
// Initialize the cipher with the static key
cipher.init(staticKey, Cipher.MODE_ENCRYPT);
|
Methods Summary |
---|
private boolean | checkMAC(byte[] buffer)Checks the request message signature.
byte numBytes = buffer[ISO7816.OFFSET_LC];
if (numBytes <= MAC_LENGTH) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Initialize signature with current session key for verification
signature.init(sessionKey, Signature.MODE_VERIFY);
// Verify request message signature
return signature.verify(buffer, ISO7816.OFFSET_CDATA,
(short) (numBytes - MAC_LENGTH), buffer,
(short) (ISO7816.OFFSET_CDATA + numBytes - MAC_LENGTH),
MAC_LENGTH);
| private short | credit(byte[] buffer, short messageOffset, short messageLength)Credits the account of the passed-in amount.
Request Message: [1-byte Credit Amount]
Response Message: []
// Check access authorization
if (!pin.isValidated()) {
ISOException.throwIt(SW_PIN_VERIFICATION_REQUIRED);
}
// Request Message: [1-byte Credit Amount]
if (messageLength != 1) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Get credit amount from request message
byte creditAmount = buffer[messageOffset];
// Check credit amount
if ((creditAmount > MAX_CREDIT_AMOUNT) || (creditAmount < 0)) {
ISOException.throwIt(SW_INVALID_TRANSACTION_AMOUNT);
}
// Check the new balance
if ((short) (balance + creditAmount) > MAX_BALANCE) {
ISOException.throwIt(SW_EXCEED_MAXIMUM_BALANCE);
}
// Credit the amount
balance += creditAmount;
// Response Message: []
return 0;
| public void | deselect()
// Reset the PIN value
pin.reset();
if (!useTransientKey) {
sessionKey.clearKey();
}
| private byte[] | fixParity(byte[] buffer, short offset, short length)Fixes the parity on DES key data.
for (byte i = 0; i < length; i++) {
short parity = 0;
buffer[(short) (offset + i)] &= 0xFE;
for (byte j = 1; j < 8; j++) {
if ((buffer[(short) (offset + i)] & (byte) (1 << j)) != 0) {
parity++;
}
}
if ((parity % 2) == 0) {
buffer[(short) (offset + i)] |= 1;
}
}
return buffer;
| private void | generateCardChallenge()Generates a new random card challenge.
// Generate random card challenge
random.generateData(cardChallenge, (short) 0, CHALLENGE_LENGTH);
| private void | generateKeyDerivationData(byte[] buffer)Generates the session key derivation data from the passed-in host
challenge and the card challenge.
byte numBytes = buffer[ISO7816.OFFSET_LC];
if (numBytes < CHALLENGE_LENGTH) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Derivation data: [[8-bytes host challenge], [8-bytes card challenge]]
// Append host challenge (from buffer) to derivation data
Util.arrayCopy(buffer, ISO7816.OFFSET_CDATA, keyDerivationData,
(short) 0, CHALLENGE_LENGTH);
// Append card challenge to derivation data
Util.arrayCopy(cardChallenge, (short) 0, keyDerivationData,
CHALLENGE_LENGTH, CHALLENGE_LENGTH);
| private short | generateMAC(byte[] buffer, short offset)Generates the response message MAC: generates the MAC and appends the MAC
to the response message.
// Initialize signature with current session key for signing
signature.init(sessionKey, Signature.MODE_SIGN);
// Sign response message and append the MAC to the response message
short sigLength = signature.sign(buffer, (short) 0, offset, buffer,
offset);
return (short) (offset + sigLength);
| private void | generateSessionKey()Generates a new DES session key from the derivation data.
cipher.doFinal(keyDerivationData, (short) 0, (short) keyDerivationData.length,
sessionKeyData, (short) 0);
// Generate new session key from encrypted derivation data
sessionKey.setKey(fixParity(sessionKeyData, (short) 0, (short) sessionKeyData.length /*LENGTH_DES_BYTE*/), (short) 0);
| private short | getBalance(byte[] buffer, short messageOffset, short messageLength)Gets/returns the balance.
Request Message: []
Response Message: [2-bytes Balance]
// Check access authorization
if (!pin.isValidated()) {
ISOException.throwIt(SW_PIN_VERIFICATION_REQUIRED);
}
// Request Message: []
if (messageLength != 0) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Response Message: [2-bytes Balance]
short offset = 0;
// Append balance to response message
offset = Util.setShort(buffer, offset, balance);
return offset;
| private void | initializeSession(javacard.framework.APDU apdu)Initializes a CAD/card interaction session. This is the first step of
mutual authentication. A new card challenge is generated and used along
with the passed-in host challenge to generate the derivation data from
which a new session key is derived. The card challenge is appended to the
response message. The response message is signed using the newly
generated session key then sent back. Note that mutual authentication is
subsequently completed upon succesful verification of the signature of
the first request received.
// C-APDU: [CLA, INS, P1, P2, LC, [4-bytes Host Challenge]]
byte[] buffer = apdu.getBuffer();
if ((buffer[ISO7816.OFFSET_P1] != 0)
|| (buffer[ISO7816.OFFSET_P2] != 0)) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
byte numBytes = buffer[ISO7816.OFFSET_LC];
byte count = (byte) apdu.setIncomingAndReceive();
if (numBytes != CHALLENGE_LENGTH || count != CHALLENGE_LENGTH) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Generate card challenge
generateCardChallenge();
// Generate key derivation data from host challenge and card challenge
generateKeyDerivationData(buffer);
// Generate session key from derivation data
generateSessionKey();
// R-APDU: [[4-bytes Card Challenge], [2-bytes Status Word], [8-bytes
// MAC]]
short offset = 0;
// Append card challenge to response message
offset = Util.arrayCopyNonAtomic(cardChallenge, (short) 0, buffer,
offset, CHALLENGE_LENGTH);
// Append status word to response message
offset = Util.setShort(buffer, offset, SW_SUCCESS);
// Sign response message and append MAC to response message
offset = generateMAC(buffer, offset);
// Send R-APDU
apdu.setOutgoingAndSend((short) 0, offset);
| public static void | install(byte[] bArray, short bOffset, byte bLength)
// Create a Transit applet instance
new TransitApplet(bArray, bOffset, bLength);
| public void | process(javacard.framework.APDU apdu)
// C-APDU: [CLA, INS, P1, P2, LC, ...]
byte[] buffer = apdu.getBuffer();
// Dispatch C-APDU for processing
if (!apdu.isISOInterindustryCLA()) {
switch (buffer[ISO7816.OFFSET_INS]) {
case INITIALIZE_SESSION:
initializeSession(apdu);
return;
case PROCESS_REQUEST:
processRequest(apdu);
return;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
} else {
if (buffer[ISO7816.OFFSET_INS] == (byte)(0xA4)) {
return;
} else if (buffer[ISO7816.OFFSET_INS] == VERIFY) {
verify(apdu);
} else {
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
| private short | processEntry(byte[] buffer, short messageOffset, short messageLength)Processes a transit entry event. The passed-in entry station ID is
recorded and the correlation ID is incremented. The UID and the
correlation ID are returned in the response message.
Request Message: [2-bytes Entry Station ID]
Response Message: [[2-bytes UID], [2-bytes Correlation ID]]
// Request Message: [2-bytes Entry Station ID]
if (messageLength != 2) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Check minimum balance
if (balance < MIN_TRANSIT_BALANCE) {
ISOException.throwIt(SW_MIN_TRANSIT_BALANCE);
}
// Check consistent transit state: should not currently be in transit
if (entryStationId >= 0) {
ISOException.throwIt(SW_INVALID_TRANSIT_STATE);
}
JCSystem.beginTransaction();
// Get/assign entry station ID from request message
entryStationId = Util.getShort(buffer, messageOffset);
// Increment correlation ID
correlationId++;
JCSystem.commitTransaction();
// Response Message: [[8-bytes UID], [2-bytes Correlation ID]]
short offset = 0;
// Append UID to response message
offset = Util.arrayCopy(uid, (short) 0, buffer, offset, UID_LENGTH);
// Append correlation ID to response message
offset = Util.setShort(buffer, offset, correlationId);
return offset;
| private short | processExit(byte[] buffer, short messageOffset, short messageLength)Processes a transit exit event. The passed-in transit fee is debited from
the account. The UID and the correlation ID are returned in the response
message.
Request Message: [1-byte Transit Fee]
Response Message: [[2-bytes UID], [2-bytes Correlation ID]]
// Request Message: [1-byte Transit Fee]
if (messageLength != 1) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Check minimum balance
if (balance < MIN_TRANSIT_BALANCE) {
ISOException.throwIt(SW_MIN_TRANSIT_BALANCE);
}
// Check consistent transit state: should be currently in transit
if (entryStationId < 0) {
ISOException.throwIt(SW_INVALID_TRANSIT_STATE);
}
// Get transit fee from request message
byte transitFee = buffer[messageOffset];
// Check potential negative balance
if (balance < transitFee) {
ISOException.throwIt(SW_NEGATIVE_BALANCE);
}
JCSystem.beginTransaction();
// Debit transit fee
balance -= transitFee;
// Reset entry station ID
entryStationId = -1;
JCSystem.commitTransaction();
// Response Message: [[8-bytes UID], [2-bytes Correlation ID]]
short offset = 0;
// Append UID to response message
offset = Util.arrayCopy(uid, (short) 0, buffer, offset, UID_LENGTH);
// Append correlation ID to response message
offset = Util.setShort(buffer, offset, correlationId);
return offset;
| private void | processRequest(javacard.framework.APDU apdu)Processes an incoming request. The request message signature is verified,
then it is dispatched to the relevant handling method. The response
message is then signed and sent back.
// C-APDU: [CLA, INS, P1, P2, LC, [Request Message], [8-bytes MAC]]
// Request Message: [T, L, [V...]]
byte[] buffer = apdu.getBuffer();
if ((buffer[ISO7816.OFFSET_P1] != 0)
|| (buffer[ISO7816.OFFSET_P2] != 0)) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
byte numBytes = buffer[ISO7816.OFFSET_LC];
byte count = (byte) apdu.setIncomingAndReceive();
if (numBytes != count) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Check request message signature
if (!checkMAC(buffer)) {
ISOException.throwIt(SW_WRONG_SIGNATURE);
}
if ((numBytes - MAC_LENGTH) != (buffer[TLV_LENGTH_OFFSET] + 2)) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
// R-APDU: [[Response Message], [2-bytes Status Word], [8-bytes MAC]]
short offset = 0;
// Dispatch request message for processing
switch (buffer[TLV_TAG_OFFSET]) {
case PROCESS_ENTRY:
offset = processEntry(buffer, TLV_VALUE_OFFSET,
buffer[TLV_LENGTH_OFFSET]);
break;
case PROCESS_EXIT:
offset = processExit(buffer, TLV_VALUE_OFFSET,
buffer[TLV_LENGTH_OFFSET]);
break;
case CREDIT:
offset = credit(buffer, TLV_VALUE_OFFSET, buffer[TLV_LENGTH_OFFSET]);
break;
case GET_BALANCE:
offset = getBalance(buffer, TLV_VALUE_OFFSET,
buffer[TLV_LENGTH_OFFSET]);
break;
default:
ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED);
}
// Append status word to response message
offset = Util.setShort(buffer, offset, SW_SUCCESS);
// Sign response message and append MAC to response message
offset = generateMAC(buffer, offset);
// Send R-APDU
apdu.setOutgoingAndSend((short) 0, offset);
| public boolean | select()
// The applet declines to be selected
// if the PIN is blocked.
if (pin.getTriesRemaining() == 0) {
return false;
}
return true;
| private void | verify(javacard.framework.APDU apdu)Verifies the PIN.
byte[] buffer = apdu.getBuffer();
byte numBytes = buffer[ISO7816.OFFSET_LC];
byte count = (byte) apdu.setIncomingAndReceive();
if (numBytes != count) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Verify PIN
if (pin.check(buffer, ISO7816.OFFSET_CDATA, numBytes) == false) {
ISOException.throwIt(SW_VERIFICATION_FAILED);
}
|
|