package card;
//PACKAGE AID: 63686561706B6E6970 ("cheapknip")
//APPLET AID: 63686561706B6E697001
//Applet revision: 1.5
/**
* @author Rafael Boix & Eduardo Novella
*/
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.AESKey;
import javacard.security.CryptoException;
import javacard.security.DESKey;
import javacard.security.KeyBuilder;
import javacard.security.RandomData;
import javacard.security.Signature;
import javacardx.crypto.Cipher;
public class CardApp extends Applet {
//Constants in the card & byte arrays; the JavaCard VM sets them to false/null/0 by default
private static boolean CARD_ISSUED; //If true, the card is blocked for modifying card USER DATA and INS 0x10 is not valid
private static boolean CARD_BLOCKED; //If true, the card is blocked
private static boolean[] TRUSTED_TERMINAL;
private static byte[] AES_KEY_128;
private static byte[] TDES_KEY;
final private static short CARD_ID = (short)12345; //This should be retrieved from a manufacturer value hardwired into the Javacard
//APDU INS operation codes in the apdu header
final static private byte INS_ISSUE_NEW_CARD = (byte) 0x10;
final static private byte INS_HANDSHAKE = (byte) 0x20;
final static private byte INS_GET_BALANCE = (byte) 0x30;
final static private byte INS_MODIFY_BALANCE = (byte) 0x40;
final static private byte INS_GET_OWNER_INFO = (byte) 0x50;
final static private byte INS_GET_TRNSCT_LOG = (byte) 0x60;
final static private byte INS_GET_CARD_ID = (byte) 0x69;
final static private byte INS_BLOCK_CARD = (byte) 0x99;
final static private short MAX_BALANCE = (short) 0x61A8; // Max balance 250.00€ short decimal=25000
//Log operation types
final static private byte LOG_OP_PAY = (byte) 0x11;
final static private byte LOG_OP_TOPUP = (byte) 0x22;
final static private byte LOG_OP_ISSUE = (byte) 0x33;
//Variables in RAM
private byte[] userData; //User details: name, address, birthdate
private byte[] tempData; //Scratchpad array for data I/O, 128 bytes
private byte[] calcData; //Scratchpad array for calculations, 128 bytes
private byte[] state; //Internal state of the applet's protocols
private byte[] transLog; //Log of 10 last transactions of card
private short balance; //Actual Balance of money into the card
private byte[] cryptoTemp; //Internal state of the applet's protocols
private AESKey k; //AES key object in RAM
private DESKey sk; //3DES key object in RAM for signature purposes
private Signature sg; //Signature object for MAC generation
private Cipher c; //Cipher object for AES encryption
public CardApp() {
//Arrays init
tempData = JCSystem.makeTransientByteArray((short)128, JCSystem.CLEAR_ON_RESET);
calcData = JCSystem.makeTransientByteArray((short)128, JCSystem.CLEAR_ON_RESET);
cryptoTemp = JCSystem.makeTransientByteArray((short)128, JCSystem.CLEAR_ON_RESET);
TRUSTED_TERMINAL = JCSystem.makeTransientBooleanArray((short)1, JCSystem.CLEAR_ON_RESET);
state = JCSystem.makeTransientByteArray((short)10, JCSystem.CLEAR_ON_RESET);
userData = new byte[100];
transLog = new byte[60];//2byte transaction counter + 10*5byte logEntry + 8byte signature
//AES & 3DES key init
AES_KEY_128 = new byte[16];
TDES_KEY = new byte[24];
try{
k = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_128, false);
sk = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false);
sg = Signature.getInstance(Signature.ALG_DES_MAC8_ISO9797_M2, false);
c = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false);
}catch(CryptoException ce){
CryptoException.throwIt((short)0xCACA); //This will show up at applet install if card does not support all this crypto stuff
}
}
public static void install(byte[] bArray, short bOffset, byte bLength) {
// GP-compliant JavaCard applet registration
new CardApp().register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
/**
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
public void process(APDU apdu) {
if (selectingApplet()) {
// Good practice: Return 9000 on SELECT
TRUSTED_TERMINAL[0]=false;
state[0]=(byte)0x00; // Reset authentication protocol
return;
}
//Utils : references to fields in a CommandAPDU apdu
byte[] buff = apdu.getBuffer();
byte cla = buff[ISO7816.OFFSET_CLA];
byte ins = buff[ISO7816.OFFSET_INS];
if ( cla == (byte) 0x00){
switch ( ins ) {
case INS_ISSUE_NEW_CARD:
if (CARD_BLOCKED) ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
issueNewCard(apdu);
break;
case INS_HANDSHAKE :
handshake(apdu);
break;
case INS_GET_BALANCE :
if (CARD_BLOCKED) ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
getBalance(apdu);
break;
case INS_MODIFY_BALANCE:
if (CARD_BLOCKED) ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
modifyBalance(apdu);
break;
case INS_GET_OWNER_INFO:
getOwnerInfo(apdu);
break;
case INS_GET_TRNSCT_LOG:
getTransctLog(apdu);
break;
case INS_GET_CARD_ID :
getCardID(apdu);
break;
case INS_BLOCK_CARD :
CARD_BLOCKED=true; //Card is blocked: no further operation possible with the card, only get last transaction(s)
return;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}
/**
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void getTransctLog(APDU apdu) {
if (CARD_ISSUED) {
short le = apdu.setOutgoing();
if (le < 60)
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
//Send transaction log (log is already signed)
apdu.setOutgoingLength((short) (short)60);
apdu.sendBytesLong(transLog, (short) 0, (short)60);
} else
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
}
/**
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void getCardID(APDU apdu) {
//This function is a mockup from the real instruction that gets the hardwired (tamper-proof) unique Card ID
byte[] buff = apdu.getBuffer();
short le = apdu.setOutgoing();
if (le < 2) { //2bytes card id
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
} else {
Util.setShort(buff, (short) 0, CARD_ID);
apdu.setOutgoingLength((short) 2);
// Send len from buffer to bOff (begin offset)
apdu.sendBytes((short) 0, (short) 2);
}
}
/**
* Steps for issuing a new card:
* Store UserData & keys into EEPROM
* Set CARD_ISSUED= true
* Verify that everything works: cipher & signatures
*
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void issueNewCard (APDU apdu){
byte[] buff = apdu.getBuffer();
if(CARD_ISSUED)
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
else{
JCSystem.beginTransaction();
//We get in the buffer the card key K , card signing key SK and 100 byte array of user data
//Write card key K in AES_KEY_128[]
Util.arrayCopy(buff, ISO7816.OFFSET_CDATA, AES_KEY_128, (short)0, (short)16);
//Write card signing key K in TDES_KEY[]
Util.arrayCopy(buff, (short)21, TDES_KEY, (short)0, (short)24);
//Write user data
Util.arrayCopy(buff, (short)45, userData, (short)0, (short)100);
CARD_ISSUED=true; //Card Issuance Blocked; from now on INS 0x10 is unreachable
//Set keys to objects
try{
k.setKey(AES_KEY_128, (short)0);
sk.setKey(TDES_KEY, (short)0);
}
catch(CryptoException ce){
ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED);
}
JCSystem.commitTransaction();
//Card verification routine:We send back to the terminal the cardID+userDATA
//(first 16 bytes encrypted with K) and signed with sk back to verify everything
short le = apdu.setOutgoing();
if (le < 110) {
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
}
else{
apdu.setOutgoingLength((short)110);
Util.setShort(buff, (short)0, CARD_ID);
Util.arrayCopy(userData, (short)0, buff, (short)2, (short)100);
//Encrypt 16 first bytes of data for crypto testing
encryptAES128(buff, (short)0, (short)16);
//Sign encrypted data
signMessage(buff, (short)102, buff, (short)102);
// Send len from buffer to bOff (begin offset)
apdu.sendBytes((short)0, (short)110);
addLogEntry(LOG_OP_ISSUE, (short)0, (short)0);
}
}
}
/**
* Handshake function: verifies that the terminal is legit and also proves that the card
* is legit to the terminal (mutual authentication). Includes two challenge/response operations.
*
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void handshake(APDU apdu){
if (CARD_ISSUED) {
byte[] buff = apdu.getBuffer();
// APDU parameter XX --> 00 20 XX 00 --> 4 authentication steps (00,01,10,11)
byte p1 = buff[ISO7816.OFFSET_P1];
// Terminal sends a nonce to Card & Card generates nonceR = (nonceT ^ nonceC)
if (p1 == (byte) 0x00) {
JCSystem.beginTransaction();
if (state[0] == (byte) 0x00) {
//nonceT
Util.arrayCopy(buff, (short) ISO7816.OFFSET_CDATA,tempData, (short) 0, (short) 8);
//nonceC
RandomData nonceC = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
nonceC.generateData(calcData, (short) 0, (short) 8);
//nonceT^nonceC=nonceR
XOR(tempData, calcData, calcData, (short) 0, (short) 0,(short) 8, (short) 8);
//We send id,AES(nonceR)K and update internal state of handshake protocol
//-------------------------------------------------------------------------
state[0] = (byte) 0x01;
// Sets the data transfer direction to outbound and obtains the expected length of the response.
short le = apdu.setOutgoing();
if (le < 18) // 16 bytes nonceR encrypted + 2bytes card id
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
//nonceR is at offset 8 in calcData
Util.arrayCopy(calcData,(short) 8,buff,(short) 0,(short) 8);
Util.arrayFillNonAtomic(buff, (short)8,(short) 16,(byte) 0);
// AES(nonceR)K
encryptAES128(buff,(short)0,(short)16);
// we put CardID in plaintext at the final of the buffer
Util.setShort(buff,(short)16,CARD_ID);
// Sets the actual length of the response data.
apdu.setOutgoingLength((short) (18+8));
signMessage(buff, (short)18, buff, (short)18);
// Send len from buffer to bOff (begin offset)
apdu.sendBytes((short) 0, (short) (18+8));
} else {
state[0] = (byte) 0x00;
TRUSTED_TERMINAL[0] = false;
}
JCSystem.commitTransaction();
}
// Terminal sends (nonceC,nonceT2)K to card -> card trusts terminal after checking nonceC
else if (p1 == (byte) 0x10 && state[0] == (byte) 0x01) {
//Update internal protocol state
state[0] = (byte) 0x10;
//nonceC_T2 from buffer to tempData -- length 16 bytes, offset byte 16
Util.arrayCopy(buff,(short) ISO7816.OFFSET_CDATA,tempData,(short) 16,(short)16);
//AES(nonceC,nonceT2)K and check if it matches nonceC in card
//Check if decrypted nonceC matches nonceC in calcData
decryptAES128(tempData, (short)16);
JCSystem.beginTransaction();
byte nonceCok = Util.arrayCompare(calcData,(short)0,tempData,(short)16,(short)8);
if (nonceCok == (byte) 0) {
state[0] = (byte) 0x11; //Card trusts Terminal, we send the challenge response to terminal
//nonceT^nonceT2=nonceR2; offset nonceT=0,offset nonceT2=16
XOR(tempData,tempData,calcData,(short)0,(short)24,(short)0,(short)8);
//We send AES(nonceR2)K and set TRUSTED_TERMINAL to true
encryptAES128(calcData,(short)0,(short)16);
// Sets the data transfer direction to outbound and obtains the expected length of the response.
short le = apdu.setOutgoing();
if (le < 16) { //8 bytes of response
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
}
Util.arrayCopy(calcData,(short)0,buff,(short)0,(short) 16);
// Sets the actual length of the response data.
apdu.setOutgoingLength((short) (16+8));
signMessage(buff, (short)16, buff, (short)16);
// Send len from buffer to bOff (begin offset)
apdu.sendBytes((short) 0, (short) (16+8));
TRUSTED_TERMINAL[0] = true;
state[0] = (byte) 0x00;
} else {
state[0] = (byte) 0x00; //NonceC not ok
TRUSTED_TERMINAL[0] = false; //Should be already set to false
ISOException.throwIt((short)0xDEAD);
}
JCSystem.commitTransaction();
} else {
state[0] = (byte) 0x00;
TRUSTED_TERMINAL[0] = false;
}
}else
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
/**
* Returns the card balance
*
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void getBalance (APDU apdu){
if(CARD_BLOCKED){
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
return;
}
if (CARD_ISSUED) {
if (TRUSTED_TERMINAL[0]) {
byte[] buff = apdu.getBuffer();
short le = apdu.setOutgoing();
if (le < 16+8)
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
// Copy balance to buffer
Util.setShort(buff, (short) 0, balance);
encryptAES128(buff,(short)0,(short)16);
//Sign&send encrypted data
apdu.setOutgoingLength((short) (short)(16+8));
signMessage(buff, (short)16, buff, (short)16);
apdu.sendBytes((short) 0, (short) (short)(16+8));
} else
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
}else
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
/**
* Modifies the card balance
*
* We receive a buffer like that: YY XX XX
* op b1 b2
* Where op=00 for -
* op=01 for +
*
* XX XX is the balance (short number)
*
* @author Eduardo Novella & Rafael Boix
* @param apdu The apdu to be processed
*/
private void modifyBalance (APDU apdu){
if(CARD_BLOCKED){
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
return;
}
if (CARD_ISSUED) {
if (TRUSTED_TERMINAL[0]) {
byte[] buff = apdu.getBuffer();
//Failed signature = untrusted terminal + throw exception
if(!verifySignature(buff, ISO7816.OFFSET_CDATA, (short)16, (short)(ISO7816.OFFSET_CDATA+16))){
TRUSTED_TERMINAL[0]=false;
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
decryptAES128(buff,ISO7816.OFFSET_CDATA);
byte op = buff[ISO7816.OFFSET_CDATA];
short newAmount = Util.makeShort(buff[ISO7816.OFFSET_CDATA + 1],buff[ISO7816.OFFSET_CDATA + 2]);
short finalBalance = (short) 0;
JCSystem.beginTransaction();
if (newAmount == 0)
return;
if (newAmount < 0) {
JCSystem.abortTransaction();
ISOException.throwIt((short) ISO7816.SW_COMMAND_NOT_ALLOWED);
}
if (op == (byte) 0x00) { // We subtract amount of money
finalBalance = (short) (balance - newAmount);
if (finalBalance < 0) {
JCSystem.abortTransaction();
ISOException.throwIt((short) ISO7816.SW_DATA_INVALID);
} else{
addLogEntry(LOG_OP_PAY, balance, finalBalance);
balance = finalBalance;
}
} else if (op == (byte) 0x01) { // We add amount of money
finalBalance = (short) (balance + newAmount);
if (finalBalance > MAX_BALANCE) {
JCSystem.abortTransaction();
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
else{
addLogEntry(LOG_OP_TOPUP, balance, finalBalance);
balance = finalBalance;
}
} else {
JCSystem.abortTransaction();
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
}
// Prepare buffer to reply
short le = apdu.setOutgoing();
if (le < 16+8)
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
// Copy balance to buffer
Util.setShort(buff, (short) 0, balance);
//Encrypt, sign & send back
encryptAES128(buff,(short)0,(short)16);
apdu.setOutgoingLength((short) (short)(16+8));
signMessage(buff, (short)16, buff, (short)16);
apdu.sendBytes((short) 0, (short) (short)(16+8));
JCSystem.commitTransaction();
} else
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
}else
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
/**
* Sends the user data in the card
* User details: name, address ,birthday 100 Bytes
* Name == 40 bytes
* Address == 54 bytes
* Birth date == 6 bytes
* @author Eduardo Novella & Rafael Boix
* @param apdu
*/
private void getOwnerInfo (APDU apdu){
if (CARD_ISSUED) {
if (TRUSTED_TERMINAL[0]) {
byte[] buff = apdu.getBuffer();
// Prepare buffer to reply
short le = apdu.setOutgoing();
if (le < 100) {
ISOException.throwIt((short) (ISO7816.SW_WRONG_LENGTH));
}
// Copy UserData to buffer
Util.arrayCopy(userData, (short) 0, buff, (short) 0,
(short) 100);
// Sets the actual length of the response data.
apdu.setOutgoingLength((short) 100);
// Send len from buffer to bOff (begin offset)
apdu.sendBytes((short) 0, (short) 100);
} else
ISOException.throwIt((short) ISO7816.SW_INS_NOT_SUPPORTED);
}else
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
//Utility functions:xor, crypto, signatures, logging
/**
* This function performs a XOR of length bytes of buffer1, buffer2 and writes output to bufferDest (all in their respective offsets)
*
* @author Eduardo Novella & Rafael Boix
*/
private void XOR(byte[] buffer1, byte[] buffer2,byte[] bufferDest, short offset1, short offset2, short offsetDest, short length){
short n=(short) 0;
while(n<length){
bufferDest[offsetDest+n] = (byte) (buffer1[offset1+n] ^ buffer2[offset2+n]);
n++;
}
}
/**
* Decrypt 16 bytes using AES128
* @author Eduardo Novella & Rafael Boix
* @param buff The buffer to decrypt 16 bytes
* @param offset The offset in buffer buff
*/
private void decryptAES128(byte[] buff,short offset){
Util.arrayCopy(buff, offset, cryptoTemp, (short)0, (short)16);
try{
c.init(k, Cipher.MODE_DECRYPT);
c.doFinal(cryptoTemp,(short)0,(short)16,buff,(short)offset);
}catch(CryptoException e){
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}
}
/**
* Encrypt a buffer using AES128
* @author Eduardo Novella & Rafael Boix
* @param buff The buffer to encrypt
* @param offset The offset in buffer buff
* @param len The length of the message to be encrypted
*/
private void encryptAES128(byte[] buff,short offset,short len){
Util.arrayCopy(buff, offset, cryptoTemp, (short)0, len);
try{
c.init(k, Cipher.MODE_ENCRYPT);
c.doFinal(cryptoTemp,(short)0,len,buff,(short)offset);
}catch(CryptoException e){
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}
}
/**
* Sign a message with 8-byte MAC using 3DES
* @author Eduardo Novella & Rafael Boix
* @param msg The buffer to sign
* @param msgLength The length of data to be signed
* @param signature The byte[] where the 8byte signature will be written
* @param signOffset The offset in the signature byte[]
*/
private void signMessage(byte[] msg, short msgLength, byte[] signature, short signOffset){
sg.init(sk, Signature.MODE_SIGN);
sg.sign(msg, (short)0, msgLength, signature, signOffset);
}
/**
* Verify a 8-byte MAC signature generated using 3DES
* @author Eduardo Novella & Rafael Boix
* @param buff The buffer to encrypt
* @param offset The offset in buffer buff
* @param len The length of the message to be encrypted
*/
private boolean verifySignature(byte[] msgPlusSignature,short messageOffset, short msgLength, short signatureOffset){
sg.init(sk, Signature.MODE_VERIFY);
return sg.verify(msgPlusSignature, messageOffset, msgLength, msgPlusSignature,signatureOffset,(short)8);
}
/**
* Add an entry to the log in the card; the log is signed, and behaves like a circular buffer when adding entries (10 entries max)
* @author Eduardo Novella & Rafael Boix
* @param op The operation that has been performed
* @param prevBalance The previous balance in the card
* @param newBalance The new balance in the card
*/
private void addLogEntry(byte op,short prevBalance,short newBalance){
short count=Util.getShort(transLog, (short)0);
//Prevent transaction counter overflow: block card (should happen in ~4 years with 20 transactions/day)
if(count==(short)32766)
CARD_BLOCKED=true;
short index=(short)(((count%10)*5)+2);
transLog[index]=op;
Util.setShort(transLog, (short)(index+1), prevBalance);
Util.setShort(transLog, (short)(index+3), newBalance);
Util.setShort(transLog, (short)0,(short)(count+1));
signMessage(transLog, (short)52, transLog, (short)52);
}
}