package org.nick.passman.sim;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.JCSystem;
import javacard.framework.Util;
public class PasswordManagerApplet extends Applet {
private static final short OFFSET_ZERO = 0;
private static final byte CLA = (byte) 0x80;
private static final byte INS_GET_STATUS = (byte) 0x1;
private static final byte INS_GEN_RANDOM = (byte) 0x2;
private static final byte INS_GEN_KEY = (byte) 0x03;
private static final byte INS_ENCRYPT = (byte) 0x4;
private static final byte INS_DECRYPT = (byte) 0x5;
private static final byte INS_CLEAR = (byte) 0x6;
// in bytes
// AES 128
private static final short KEY_LENGTH = 128 / 8;
private static final short AES_BLOCK_LEN = 16;
// nonce || short ctr
private static final short PRNG_NONCE_LEN = AES_BLOCK_LEN - 2;
private static final short MAX_DATA_LEN = (short) 208;
// IV(16) + ENCRYPTED DATA
private static final short MAX_ENCYPTED_DATA_LEN = (short) (MAX_DATA_LEN + 2 * AES_BLOCK_LEN);
// AES 128
private byte[] keyBytes;
private boolean keysGenerated = false;
// AES PRNG
private byte[] prngKey;
private byte[] prngNonce;
private short prngCounter;
// transient
// for AES
private byte[] iv;
private byte[] cbcV;
private byte[] cbcNextV;
private byte[] cipherBuff;
private byte[] roundKeysBuff;
private JavaCardAES aesCipher;
private PasswordManagerApplet(byte[] bArray, short bOffset, byte bLength) {
keyBytes = new byte[KEY_LENGTH];
// XXX sample values for easier testing
// always initialize from install parameters!
prngKey = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8,
0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf };
prngNonce = new byte[] { 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa,
0xb, 0xc, 0xd, 0xe, 0xf };
if (bArray != null) {
short Li = bArray[bOffset];
short Lc = bArray[(short) (bOffset + Li + 1)];
short seedLength = bArray[(short) (bOffset + Li + Lc + 2)];
if (seedLength > 0) {
if (seedLength != (KEY_LENGTH + PRNG_NONCE_LEN)) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
short seedOffset = (short) (bOffset + Li + Lc + 3);
Util.arrayCopy(bArray, seedOffset, prngKey, OFFSET_ZERO,
KEY_LENGTH);
Util.arrayCopy(bArray, (short) (seedOffset + KEY_LENGTH),
prngNonce, OFFSET_ZERO, PRNG_NONCE_LEN);
}
}
prngCounter = 0;
iv = JCSystem.makeTransientByteArray(AES_BLOCK_LEN,
JCSystem.CLEAR_ON_DESELECT);
cbcV = JCSystem.makeTransientByteArray(AES_BLOCK_LEN,
JCSystem.CLEAR_ON_DESELECT);
cbcNextV = JCSystem.makeTransientByteArray(AES_BLOCK_LEN,
JCSystem.CLEAR_ON_DESELECT);
// account for padding
cipherBuff = JCSystem.makeTransientByteArray(
(short) (MAX_DATA_LEN + AES_BLOCK_LEN),
JCSystem.CLEAR_ON_DESELECT);
roundKeysBuff = JCSystem.makeTransientByteArray(
(short) (AES_BLOCK_LEN * 11), JCSystem.CLEAR_ON_DESELECT);
aesCipher = new JavaCardAES();
}
public static void install(byte bArray[], short bOffset, byte bLength)
throws ISOException {
new PasswordManagerApplet(bArray, bOffset, bLength).register();
}
public void process(APDU apdu) throws ISOException {
byte[] buff = apdu.getBuffer();
if (selectingApplet()) {
return;
}
// account for logical channels
if (((byte) (buff[ISO7816.OFFSET_CLA] & (byte) 0xFC)) != CLA) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
switch (buff[ISO7816.OFFSET_INS]) {
case INS_GET_STATUS:
getInitStatus(apdu);
break;
case INS_GEN_RANDOM:
prng(apdu);
break;
case INS_GEN_KEY:
generateKeys(apdu);
break;
case INS_ENCRYPT:
encrypt(apdu);
break;
case INS_DECRYPT:
decrypt(apdu);
break;
case INS_CLEAR:
clear(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
private void getInitStatus(APDU apdu) {
byte[] buff = apdu.getBuffer();
buff[0] = keysGenerated ? (byte) 0x01 : (byte) 0x00;
apdu.setOutgoingAndSend(OFFSET_ZERO, (short) 1);
}
private void prng(APDU apdu) {
byte[] buff = apdu.getBuffer();
prng(buff, OFFSET_ZERO, AES_BLOCK_LEN);
apdu.setOutgoingAndSend(OFFSET_ZERO, AES_BLOCK_LEN);
}
// use RandomData.getInstance(ALG_SECURE_RANDOM) on real cards!
private void prng(byte[] buff, short offset, short len) {
if (len > AES_BLOCK_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
Util.arrayCopyNonAtomic(prngNonce, OFFSET_ZERO, cipherBuff,
OFFSET_ZERO, (short) prngNonce.length);
Util.setShort(cipherBuff, (short) (AES_BLOCK_LEN - 2), prngCounter);
try {
aesCipher.RoundKeysSchedule(prngKey, (short) 0, roundKeysBuff);
// encrypts in place
boolean success = aesCipher.AESEncryptBlock(cipherBuff,
OFFSET_ZERO, roundKeysBuff);
if (!success) {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
prngCounter++;
Util.arrayCopyNonAtomic(cipherBuff, OFFSET_ZERO, buff, offset, len);
} finally {
clearCipherState();
}
}
private void generateKeys(APDU apdu) {
if (keysGenerated) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
try {
JCSystem.beginTransaction();
prng(keyBytes, OFFSET_ZERO, KEY_LENGTH);
keysGenerated = true;
} finally {
JCSystem.commitTransaction();
}
}
private void encrypt(APDU apdu) {
if (!keysGenerated) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] buff = apdu.getBuffer();
short len = apdu.setIncomingAndReceive();
if (len > MAX_DATA_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
prng(iv, OFFSET_ZERO, AES_BLOCK_LEN);
initAes();
try {
aesCipher.RoundKeysSchedule(keyBytes, (short) 0, roundKeysBuff);
short offset = Util.arrayCopyNonAtomic(buff, ISO7816.OFFSET_CDATA,
cipherBuff, OFFSET_ZERO, len);
short padSize = addPadding(cipherBuff, offset, len);
short paddedLen = (short) (len + padSize);
short blocks = (short) (paddedLen / AES_BLOCK_LEN);
for (short i = 0; i < blocks; i++) {
short cipherOffset = (short) (i * AES_BLOCK_LEN);
for (short j = 0; j < AES_BLOCK_LEN; j++) {
cbcV[j] ^= cipherBuff[(short) (cipherOffset + j)];
}
// encrypts in place
boolean success = aesCipher.AESEncryptBlock(cbcV, OFFSET_ZERO,
roundKeysBuff);
if (!success) {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
Util.arrayCopyNonAtomic(cbcV, OFFSET_ZERO, cipherBuff,
cipherOffset, AES_BLOCK_LEN);
}
offset = Util.arrayCopyNonAtomic(iv, OFFSET_ZERO, buff,
OFFSET_ZERO, AES_BLOCK_LEN);
offset = Util.arrayCopyNonAtomic(cipherBuff, OFFSET_ZERO, buff,
AES_BLOCK_LEN, paddedLen);
apdu.setOutgoingAndSend(OFFSET_ZERO,
(short) (AES_BLOCK_LEN + paddedLen));
} finally {
clearCipherState();
}
}
private void clearCipherState() {
Util.arrayFillNonAtomic(roundKeysBuff, OFFSET_ZERO,
(short) roundKeysBuff.length, (byte) 0x0);
Util.arrayFillNonAtomic(cipherBuff, OFFSET_ZERO,
(short) cipherBuff.length, (byte) 0x0);
Util.arrayFillNonAtomic(cbcNextV, OFFSET_ZERO, (short) cbcNextV.length,
(byte) 0);
Util.arrayFillNonAtomic(cbcV, OFFSET_ZERO, (short) cbcV.length,
(byte) 0);
}
private void decrypt(APDU apdu) {
if (!keysGenerated) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] buff = apdu.getBuffer();
short len = apdu.setIncomingAndReceive();
if (len % AES_BLOCK_LEN != 0) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
if (len > MAX_ENCYPTED_DATA_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
Util.arrayCopyNonAtomic(buff, ISO7816.OFFSET_CDATA, iv, OFFSET_ZERO,
AES_BLOCK_LEN);
initAes();
try {
aesCipher.RoundKeysSchedule(keyBytes, (short) 0, roundKeysBuff);
short cipherLen = (short) (len - AES_BLOCK_LEN);
Util.arrayCopyNonAtomic(buff,
(short) (ISO7816.OFFSET_CDATA + AES_BLOCK_LEN), cipherBuff,
OFFSET_ZERO, cipherLen);
short blocks = (short) (cipherLen / AES_BLOCK_LEN);
for (short i = 0; i < blocks; i++) {
short cipherOffset = (short) (i * AES_BLOCK_LEN);
Util.arrayCopyNonAtomic(cipherBuff, cipherOffset, cbcNextV,
OFFSET_ZERO, AES_BLOCK_LEN);
aesCipher.AESDecryptBlock(cipherBuff, cipherOffset,
roundKeysBuff);
// XOR output w/ cbcV
for (short j = 0; j < AES_BLOCK_LEN; j++) {
cipherBuff[(short) (cipherOffset + j)] ^= cbcV[j];
}
// swap
byte[] tmp = cbcV;
cbcV = cbcNextV;
cbcNextV = tmp;
}
short plainLen = (short) (cipherLen - padCount(cipherBuff,
cipherLen));
Util.arrayCopyNonAtomic(cipherBuff, OFFSET_ZERO, buff, OFFSET_ZERO,
plainLen);
apdu.setOutgoingAndSend(OFFSET_ZERO, plainLen);
} finally {
clearCipherState();
}
}
private void initAes() {
Util.arrayCopyNonAtomic(iv, OFFSET_ZERO, cbcV, OFFSET_ZERO,
(short) iv.length);
Util.arrayFillNonAtomic(cbcNextV, OFFSET_ZERO, (short) cbcNextV.length,
(byte) 0);
Util.arrayFillNonAtomic(roundKeysBuff, OFFSET_ZERO,
(short) roundKeysBuff.length, (byte) 0);
}
private static short addPadding(byte[] in, short start, short len) {
short unpaddedBlockLen = 0;
if (len % AES_BLOCK_LEN != 0) {
short blocks = len < AES_BLOCK_LEN ? (short) 1
: (short) (len / AES_BLOCK_LEN);
unpaddedBlockLen = len < AES_BLOCK_LEN ? len
: (short) (len - blocks * AES_BLOCK_LEN);
}
byte code = (byte) (AES_BLOCK_LEN - unpaddedBlockLen);
for (short i = 0; i < code; i++) {
in[(short) (start + i)] = code;
}
return code;
}
private static short padCount(byte[] in, short len) {
short count = (short) (in[(short) (len - 1)] & 0xff);
if (count > len || count == 0) {
// corrupted pad block
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
for (short i = 1; i <= count; i++) {
if (in[(short) (len - i)] != count) {
// corrupted pad block
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
}
return count;
}
private void clear(APDU apdu) {
try {
JCSystem.beginTransaction();
for (short i = 0; i < KEY_LENGTH; i++) {
keyBytes[i] = 0;
}
keysGenerated = false;
prngCounter = 0;
} finally {
JCSystem.commitTransaction();
}
}
}