// ---------------------------------------------------------------------------
// Protocol for Lightweight Authentication of Identity (PLAID)
//
// Cardholder applet
//
// Reference Implementation compliant with AS 5185 - Javacard 2.x source code
//
// ---------------------------------------------------------------------------
// This implementation: � Copyright Australian Government
// PLAID: � Copyright Australian Government
//
// A copy of the entire Licence is available upon email request from
// plaid@humanservices.gov.au or by download from https://www.plaid.gov.au
//
// Subject to the terms of the Licence, the Australian Government grants to
// the User a perpetual, irrevocable, world-wide, non-exclusive, royalty free and
// no-charge licence to use, reproduce, adapt, modify, enhance, communicate,
// sub-license and distribute PLAID and/or its source code. Clause 2.1 includes
// the right to incorporate PLAID into any Product developed by the User.
//
// By using PLAID and/or its source code you agree to be bound by the Licence.
//
// ---------------------------------------------------------------------------
// Status: Prototype 0.804
// Issue Date: October 2011
//
// Author: Glenn Mitchell (Australian Government)
//
// Incorporating suggestions by Petr Novak (HID Global)
//
package plaid804;
import javacard.framework.*;
import javacard.security.*;
import javacardx.crypto.*;
/**
* <b>Class: PLAID</b><p>
* <b>Generic Description</b><p>
* This javacard applet implements PLAID authentication and the associated
* management routines as specified in Australian Standard AS 5185.<p>
* <b>Design Considerations</b><p>
* The following has been implemented in this reference model: <ul>
* <li> There are 8 keypairs (RSA(IA) and AES(FA)) instantiated. This amount is
* mandated by TOTAL_KEY_SETS.
* <li> The variable "keyData" manages the correlation between the keySetID
* values and the corresponding index position.
* <li> The Shillkeys (decoy keys) are stored in their own cipher objects.
* <li> The Shillkeys are generated/emulated in the constructor.
* <li> PLAID authentication in Admin mode (using keysetID 0x0000) can only be
* performed through the contact interface (unless the contactInterface method
* is modified to always return true).
* <li> Before the applet is secured, the body of a "set data" command is DER
* encoded.
* <li> After the applet is secured, the body of a "set data" command is DER
* encoded and then encrypted using AES.
* <li> The method "processGetData" is blank in this reference implementation
* as no additional user fields have been specified.
*/
public class PLAID804 extends Applet
{
//CLA constants
static final byte CLA_PROPRIETARY_CMD =(byte)0x80;
//INS constants
static final byte INS_INITIAL_AUTHENTICATE =(byte)0x8A;
static final byte INS_FINAL_AUTHENTICATE =(byte)0x8C;
static final byte INS_SET_DATA =(byte)0xDB;
static final byte INS_GET_DATA =(byte)0xCB;
//TAG constants
static final byte DIVERSIFICATION_DATA =(byte)0x01;
static final byte ACS_RECORD =(byte)0x02;
static final byte PIN =(byte)0x03;
static final byte SECURE_ICC =(byte)0x04;
static final byte MINUTIAE =(byte)0x06;
static final byte IA_KEY =(byte)0x07;
static final byte FA_KEY =(byte)0x08;
static final byte REINITIALISE_CARD =(byte)0x0A;
//Opmode consts
static final short OPMODE_1FACTOR = 0x0000;
static final short OPMODE_2FACTOR_PIN = 0x0001;
static final short OPMODE_2FACTOR_MINUTIAE = 0x0002;
//State constants
static final byte STATE_IDLE =(byte)0x00;
static final byte STATE_IA_COMPLETED =(byte)0x01;
static final byte STATE_FA_COMPLETED =(byte)0x02;
static final byte STATE_INITIAL =(byte)0x00;
static final byte STATE_SECURED =(byte)0x01;
//Offset constants - persistant (cardData)
static final short OFFSET_VERSION =(short)0;
static final short OFFSET_SECURITY_STATE =(short)1;
static final short OFFSET_DIVERSIFICATION_DATA =(short)2;
static final short OFFSET_ACS_RECORD =(short)10;
static final short OFFSET_PIN_HASH =(short)18;
static final short OFFSET_MINUTIAE =(short)38;
//Length constants
static final short LENGTH_VERSION =(short)1;
static final short LENGTH_DIVERSIFICATION_DATA =(short)8;
static final short LENGTH_ACS_RECORD =(short)8;
static final short LENGTH_KEYSETID =(short)2;
static final short LENGTH_OPMODEID =(short)2;
static final short LENGTH_PIN =(short)8;
static final short LENGTH_PIN_HASH =(short)20;
static final short LENGTH_PIN_HASH_EXTENDED =(short)32;
static final short LENGTH_MINUTIAE =(short)224;
static final short LENGTH_BUFFER128 =(short)128;
static final short LENGTH_BUFFER16 =(short)16;
static final short LENGTH_RND =(short)16;
static final short LENGTH_CURRENT_SESSION =(short)3;
static final short LENGTH_CARDDATA =(short)268;
static final short LENGTH_PUBLIC_EXPONENT =(short)3;
static final short LENGTH_FA_RESP_1F =(short)16;
static final short LENGTH_FA_RESP_2F_PIN =(short)48;
static final short LENGTH_FA_RESP_2F_MINUTIAE =(short)240;
static final short LENGTH_RSA1024 =(short)128;
//Offset constants - transient (currentSession)
static final short OFFSET_CURRENT_STATE = (short)0;
static final short OFFSET_KEYSETID = (short)1;
//Misc
static final byte[] PUBLIC_EXPONENT = {0x01,0x00,0x01};
static final byte[] ADMIN_KEYSETID = {0x00,0x00};
static final short ADMIN_KEYSET_SHORT = (short)0;
static final short TOTAL_KEY_SETS = (short)8;
static final byte OCTET_STRING_ASN1 = (byte)0x04;
static final byte SEQUENCE_ASN1 = (byte)0x10;
static final byte EXTENDED_LENGTH_ASN1 = (byte)0x80;
static final byte NULL_VALUE = (byte)0x00;
static final byte PLAID_VERSION = (byte)0x83;
static final short FIRST_ASN1_VALUE = (short)1;
static final short SECOND_ASN1_VALUE = (short)2;
static final short THIRD_ASN1_VALUE = (short)3;
//Persistant objects
private final byte[] cardData = new byte[LENGTH_CARDDATA];
private final byte[] keyData = new byte[TOTAL_KEY_SETS*2];
private final RSAPublicKey[] IAKey = new RSAPublicKey[TOTAL_KEY_SETS];
private final AESKey[] FAKey = new AESKey[TOTAL_KEY_SETS];
private final RSAPublicKey IAShillKey;
private final AESKey FAShillKey;
private final Cipher AESCipher;
private final Cipher RSACipher;
private final MessageDigest SHA1;
private final RandomData rnd;
//Transient objects
private final byte[] currentSession;
private final byte[] Buffer128, Buffer16;
private final AESKey sessionKey;
/**
* <b>Description</b><p>
* This method invokes the variables required for the PLAID applet.<p>
* <b>Design Considerations</b><p>
* The Shillkeys are set during applet instantiation.
* The padding mode used for the RSA object is PKCS#1.5. OAEP padding can
* be supported however this approach has a significant performance hit
* and may not be more secure for a protocol that does not expose the
* RSA modulus.
*/
private PLAID804()
{
Buffer128=JCSystem.makeTransientByteArray(LENGTH_BUFFER128,
JCSystem.CLEAR_ON_DESELECT);
Buffer16=JCSystem.makeTransientByteArray(LENGTH_BUFFER16,
JCSystem.CLEAR_ON_DESELECT);
sessionKey=(AESKey)KeyBuilder.buildKey(
KeyBuilder.TYPE_AES_TRANSIENT_RESET,KeyBuilder.LENGTH_AES_128,false);
Util.arrayFillNonAtomic(cardData,(short)0,LENGTH_CARDDATA,NULL_VALUE);
Util.arrayFillNonAtomic(keyData,(short)0,(short)(TOTAL_KEY_SETS*2),
NULL_VALUE);
currentSession=JCSystem.makeTransientByteArray(LENGTH_CURRENT_SESSION,
JCSystem.CLEAR_ON_RESET);
rnd=RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
AESCipher=Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD,false);
RSACipher=Cipher.getInstance(Cipher.ALG_RSA_PKCS1,false);
SHA1 = MessageDigest.getInstance(MessageDigest.ALG_SHA,false);
for (short Index=0;Index<TOTAL_KEY_SETS;Index++)
{
IAKey[Index]=(RSAPublicKey)KeyBuilder.buildKey(
KeyBuilder.TYPE_RSA_PUBLIC,KeyBuilder.LENGTH_RSA_1024,false);
IAKey[Index].setExponent(PUBLIC_EXPONENT,(short)0,LENGTH_PUBLIC_EXPONENT);
FAKey[Index]=(AESKey)KeyBuilder.buildKey(KeyBuilder.TYPE_AES,
KeyBuilder.LENGTH_AES_128,false);
}
IAShillKey=(RSAPublicKey)KeyBuilder.buildKey(
KeyBuilder.TYPE_RSA_PUBLIC,KeyBuilder.LENGTH_RSA_1024,false);
IAShillKey.setExponent(PUBLIC_EXPONENT,(short)0,LENGTH_PUBLIC_EXPONENT);
Buffer128[0] = (byte)0x80;
rnd.generateData(Buffer128,(short)1,(short)(LENGTH_RSA1024-2));
Buffer128[(short)(LENGTH_RSA1024-1)] = (byte)0x01;
IAShillKey.setModulus(Buffer128,(short)0,LENGTH_RSA1024);
rnd.generateData(Buffer16,(short)0,LENGTH_RND);
FAShillKey=(AESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_AES,KeyBuilder.LENGTH_AES_128,false);
FAShillKey.setKey(Buffer16,(short)0);
}
/**
* <b>Description</b><p>
* This method registers this applet instance with ICC's JCRE.
*/
public static void install(byte[] params, short offset, byte length)
throws ISOException
{
(new PLAID804()).register(params,(short)(offset+1),params[offset]);
}
/**
* <b>Return Type:</b> boolean<p
* <b>Generic Description</b><p>
* This method returns true iff the currently active communication channel
* is through a contact interface as determined by the protocol byte.
*
* Note: Personalisation and administration authentication through 14443 can
* be achieved by modifying this function to always return true.
*/
private static boolean contactInterface(APDU apdu)
{
return ((apdu.getProtocol()&APDU.PROTOCOL_MEDIA_MASK)==
APDU.PROTOCOL_MEDIA_DEFAULT);
//return true;
}
/**
* <b>Return Type:</b> byte[]<p>
* <b>Generic Description</b><p>
* This method parses the DER encoded ASN1 in to a format and returns
* the index of the requested object.
*/
private short getASN1Value(byte[] Buffer, short tagNo)
{
short indexASN1 = 0;
short tagCount = 0;
short traverse = 0;
if (Buffer[indexASN1++] != SEQUENCE_ASN1)
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
if ((Buffer[++indexASN1]&EXTENDED_LENGTH_ASN1)!=NULL_VALUE)
indexASN1++;
while (true)
{
if (Buffer[indexASN1++] != OCTET_STRING_ASN1)
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
if ((Buffer[indexASN1]&EXTENDED_LENGTH_ASN1)==NULL_VALUE)
traverse = Util.makeShort(NULL_VALUE,Buffer[indexASN1++]);
else
{
indexASN1++;
traverse = Util.makeShort(NULL_VALUE,Buffer[indexASN1++]);
}
tagCount++;
if (tagCount == tagNo)
break;
indexASN1+=traverse;
}
return indexASN1;
}
/**
* <b>Description</b><p>
* This method determines the nature of the APDU as determined by the
* instruction byte and invokes the corresponding method.
*
* <b>Description - case INS_INITIAL_AUTHENTICATE</b><p>
* This method performs the ICC side of Initial Authenticate command as
* specified in Australian Standard AS-5185<p>
* <b>Design Considerations</b><p>
* 1 - The ICC will allow allow authentication in administration mode through
* the contact interface.<p>
* 2 - After selecting an RSA key to use, the ICC will continue to "search"
* through the list of remaining keys to avoid potential timing attacks.<p>
* 3 - If no appropriate key is found, the ICC will use the Shillkey.<p>
* <b>Description - case INS_FINAL_AUTHENTICATE</b><p>
* This method performs the ICC side of Final Authenticate command as
* specified in Australian Standard AS-5185<p>
* <b>Design Considerations</b><p>
* 1 - In the case where the opmodeID cannot be determined, the ICC
* responds as if opmodeID was 1-factor.
*
* <b>Description - case INS_SET_DATA</b><p>
* This method manages the setting of PLAID variables.<p>
* <b>Design Considerations</b><p>
* Before a card is secured, variables may be set by storing the value in
* the body of an APDU in a DER format. Once a card is secured, data can
* only be set by completing PLAID authentication in administrator mode and
* encrypting the DER encoded body of the APDU with the session key.
* The setting of the keys requires placement in next available space as
* indicated by the array "keyData".
*/
public void process(APDU apdu)
{
if (selectingApplet())
return;
byte[] APDUBuffer=apdu.getBuffer();
short Length = apdu.setIncomingAndReceive();
short Location = 0;
short currentKey = 0;
switch (APDUBuffer[ISO7816.OFFSET_INS])
{
case INS_INITIAL_AUTHENTICATE:
if (cardData[OFFSET_SECURITY_STATE] != STATE_SECURED)
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
short APDUIndex = 9;
Util.arrayCopyNonAtomic(keyData,(short)0,Buffer16,(short)0,
LENGTH_BUFFER16);
while (APDUBuffer[(short)(APDUIndex-1)] == LENGTH_KEYSETID)
{
currentKey = Util.makeShort(APDUBuffer[APDUIndex],
APDUBuffer[(short)(APDUIndex+1)]);
for (short i=0;i<(TOTAL_KEY_SETS*2);i+=LENGTH_KEYSETID)
{
if ((currentSession[OFFSET_CURRENT_STATE]!=STATE_IA_COMPLETED)&&
(currentKey==Util.getShort(Buffer16,i)))
{
RSACipher.init(IAKey[Location],Cipher.MODE_ENCRYPT);
AESCipher.init(FAKey[Location],Cipher.MODE_DECRYPT);
currentSession[OFFSET_CURRENT_STATE] = STATE_IA_COMPLETED;
Util.arrayCopyNonAtomic(Buffer16,(short)(Location*2),Buffer128,
(short)0,LENGTH_KEYSETID);
Util.arrayCopyNonAtomic(APDUBuffer,APDUIndex,currentSession,
OFFSET_KEYSETID,LENGTH_KEYSETID);
if ((currentKey == ADMIN_KEYSET_SHORT) && (!contactInterface(apdu)))
{
currentSession[OFFSET_CURRENT_STATE] = STATE_IDLE;
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
}
}
Location++;
}
Location = 0;
APDUIndex+=4;
}
if (currentSession[OFFSET_CURRENT_STATE] != STATE_IA_COMPLETED)
{
RSACipher.init(IAShillKey,Cipher.MODE_ENCRYPT);
AESCipher.init(FAShillKey,Cipher.MODE_DECRYPT);
currentSession[OFFSET_CURRENT_STATE] = STATE_IDLE;
Util.arrayCopyNonAtomic(Buffer16,(short)0,Buffer128,(short)0,
LENGTH_KEYSETID);
Util.arrayCopyNonAtomic(APDUBuffer,APDUIndex,currentSession,
OFFSET_KEYSETID,LENGTH_KEYSETID);
}
Util.arrayCopyNonAtomic(cardData,OFFSET_DIVERSIFICATION_DATA,Buffer128,
LENGTH_KEYSETID,LENGTH_DIVERSIFICATION_DATA);
rnd.generateData(Buffer128,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA),LENGTH_RND);
Util.arrayCopyNonAtomic(Buffer128,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA),Buffer128,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA+LENGTH_RND),LENGTH_RND);
RSACipher.doFinal(Buffer128,(short)0,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA+LENGTH_RND+LENGTH_RND),
APDUBuffer,(short)0);
apdu.setOutgoingAndSend((short)0,LENGTH_RSA1024);
return;
case INS_FINAL_AUTHENTICATE:
if (currentSession[OFFSET_CURRENT_STATE] != STATE_IA_COMPLETED)
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
AESCipher.doFinal(APDUBuffer,ISO7816.OFFSET_CDATA,Length,
APDUBuffer,(short)0);
Util.arrayCopyNonAtomic(APDUBuffer,LENGTH_OPMODEID,Buffer128,(short)
(LENGTH_KEYSETID+LENGTH_DIVERSIFICATION_DATA+LENGTH_RND),(short)
(LENGTH_RND+LENGTH_RND));
SHA1.doFinal(Buffer128,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA),(short)(LENGTH_RND+LENGTH_RND),Buffer128,
(short)(LENGTH_KEYSETID+LENGTH_DIVERSIFICATION_DATA+(3*LENGTH_RND)));
byte[] source;
if ((Util.arrayCompare(Buffer128,(short)(LENGTH_KEYSETID+
LENGTH_DIVERSIFICATION_DATA+LENGTH_RND+LENGTH_RND),Buffer128,
(short)(LENGTH_KEYSETID+LENGTH_DIVERSIFICATION_DATA+(3*LENGTH_RND)),
LENGTH_BUFFER16)==0)&&(currentSession[OFFSET_CURRENT_STATE]
== STATE_IA_COMPLETED))
{
sessionKey.setKey(APDUBuffer,(short)(LENGTH_OPMODEID+LENGTH_BUFFER16));
AESCipher.init(sessionKey,Cipher.MODE_ENCRYPT);
source = cardData;
currentSession[OFFSET_CURRENT_STATE] = STATE_FA_COMPLETED;
}
else
{
sessionKey.setKey(Buffer16,(short)0);
AESCipher.init(FAShillKey,Cipher.MODE_ENCRYPT);
source = Buffer128;
currentSession[OFFSET_CURRENT_STATE] = STATE_IDLE;
}
switch (Util.getShort(APDUBuffer,(short)0))
{
case OPMODE_1FACTOR:
Location = AESCipher.doFinal(source,OFFSET_DIVERSIFICATION_DATA,
(short)(LENGTH_DIVERSIFICATION_DATA+LENGTH_ACS_RECORD),
APDUBuffer, Location);
break;
case OPMODE_2FACTOR_PIN:
Location = AESCipher.doFinal(source,OFFSET_DIVERSIFICATION_DATA,
(short)(LENGTH_DIVERSIFICATION_DATA+LENGTH_ACS_RECORD+
LENGTH_PIN_HASH_EXTENDED), APDUBuffer, Location);
break;
case OPMODE_2FACTOR_MINUTIAE:
Location = AESCipher.update(source,OFFSET_DIVERSIFICATION_DATA,
(short)(LENGTH_DIVERSIFICATION_DATA+LENGTH_ACS_RECORD),
APDUBuffer, Location);
Location += AESCipher.doFinal(source,OFFSET_MINUTIAE,
LENGTH_MINUTIAE, APDUBuffer, Location);
break;
default:
Location = AESCipher.doFinal(source,OFFSET_DIVERSIFICATION_DATA,
(short)(LENGTH_DIVERSIFICATION_DATA+LENGTH_ACS_RECORD),
APDUBuffer, Location);
break;
}
apdu.setOutgoingAndSend((short)0,Location);
return;
case INS_SET_DATA:
boolean adminKeyset;
if ((currentSession[OFFSET_CURRENT_STATE]==STATE_FA_COMPLETED)&&
(Util.arrayCompare(currentSession,OFFSET_KEYSETID,ADMIN_KEYSETID,
(short)0,LENGTH_KEYSETID)==0))
{
try
{
AESCipher.init(sessionKey,Cipher.MODE_DECRYPT);
AESCipher.doFinal(APDUBuffer,ISO7816.OFFSET_CDATA,(short)Length,
APDUBuffer,(short)0);
}
catch (CryptoException ex)
{
ISOException.throwIt(ex.getReason());
}
}
else if (cardData[OFFSET_SECURITY_STATE]==STATE_INITIAL)
Util.arrayCopyNonAtomic(APDUBuffer,ISO7816.OFFSET_CDATA,APDUBuffer,
(short)0,Length);
else
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
switch (APDUBuffer[getASN1Value(APDUBuffer,FIRST_ASN1_VALUE)])
{
case DIVERSIFICATION_DATA:
Util.arrayCopy(APDUBuffer,getASN1Value(APDUBuffer,
SECOND_ASN1_VALUE),cardData,OFFSET_DIVERSIFICATION_DATA,
LENGTH_DIVERSIFICATION_DATA);
return;
case ACS_RECORD:
Util.arrayCopy(APDUBuffer,getASN1Value(APDUBuffer,
SECOND_ASN1_VALUE),cardData,OFFSET_ACS_RECORD,LENGTH_ACS_RECORD);
return;
case MINUTIAE:
Util.arrayCopy(APDUBuffer,getASN1Value(APDUBuffer,
SECOND_ASN1_VALUE),cardData,OFFSET_MINUTIAE,LENGTH_MINUTIAE);
return;
case PIN:
SHA1.reset();
SHA1.doFinal(APDUBuffer,getASN1Value(APDUBuffer,SECOND_ASN1_VALUE),
LENGTH_PIN,cardData,OFFSET_PIN_HASH);
return;
case SECURE_ICC:
cardData[OFFSET_SECURITY_STATE] = STATE_SECURED;
return;
case REINITIALISE_CARD:
Util.arrayFillNonAtomic(cardData,(short)0,LENGTH_CARDDATA,
NULL_VALUE);
Util.arrayFillNonAtomic(keyData,(short)0,(short)(TOTAL_KEY_SETS*2),
NULL_VALUE);
return;
case IA_KEY:
Location = getASN1Value(APDUBuffer,SECOND_ASN1_VALUE);
adminKeyset = (Util.arrayCompare(APDUBuffer,Location,
ADMIN_KEYSETID,(short)0,LENGTH_KEYSETID)==0);
for (short i=2;i<(TOTAL_KEY_SETS*2);i+=2)
{
if ((Util.arrayCompare(keyData,i,APDUBuffer,Location,
LENGTH_KEYSETID) == 0)||((keyData[i]==NULL_VALUE)&&
(keyData[(short)(i+1)]==NULL_VALUE))||(adminKeyset))
{
Util.arrayCopy(APDUBuffer,Location,keyData,(short)(i),
LENGTH_KEYSETID);
Location = getASN1Value(APDUBuffer,THIRD_ASN1_VALUE);
if (adminKeyset)
currentKey=0;
else
currentKey=(short)(i/2);
IAKey[currentKey].setModulus(APDUBuffer,Location,
LENGTH_RSA1024);
return;
}
}
case FA_KEY:
Location = getASN1Value(APDUBuffer,SECOND_ASN1_VALUE);
adminKeyset = (Util.arrayCompare(APDUBuffer,Location,
ADMIN_KEYSETID,(short)0,LENGTH_KEYSETID)==0);
for (short i=2;i<(TOTAL_KEY_SETS*2);i+=2)
{
if ((Util.arrayCompare(keyData,i,APDUBuffer,Location,
LENGTH_KEYSETID) == 0)||(adminKeyset))
{
Location = getASN1Value(APDUBuffer,THIRD_ASN1_VALUE);
if (adminKeyset)
currentKey=0;
else
currentKey=(short)(i/2);
FAKey[currentKey].setKey(APDUBuffer,Location);
return;
}
}
default:
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}