/**
* Java Card implementation of the OpenPGP card
*
* Copyright (C) 2011 Joeri de Ruiter <joeri@cs.ru.nl>
* Copyright (C) 2011 Pim Vullers, Radboud University Nijmegen
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package openpgpcard;
import javacard.framework.APDU;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.DESKey;
import javacard.security.KeyBuilder;
import javacard.security.Signature;
import javacardx.crypto.Cipher;
/**
* OV secure messaging functionality.
*
* OVSecureMessaging is based on PassportCrypto which is part of the
* e-passport Java Card applet from the JMRTD project (http://jmrtd.org/).
*/
public class OpenPGPSecureMessaging {
private static final short SW_INTERNAL_ERROR = (short) 0x6D66;
private static final byte[] PAD_DATA = {(byte) 0x80, 0, 0, 0, 0, 0, 0, 0};
private static final short SSC_SIZE = 8;
private static final short TMP_SIZE = 256;
private static final short MAC_SIZE = 8;
private static final short KEY_SIZE = 16;
private static final byte[] EMPTY_KEY = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
/**
* The needed cryptographic functionality.
*/
private Signature signer;
private Signature verifier;
private Cipher cipher;
private Cipher decipher;
/**
* The needed keys.
*/
private DESKey keyMAC;
private DESKey keyENC;
/**
* The send sequence counter.
*/
private byte[] ssc;
/**
* Storage for temporary data.
*/
private byte[] tmp;
private boolean[] ssc_set;
/**
* Construct a new secure messaging wrapper.
*/
public OpenPGPSecureMessaging() {
ssc = JCSystem.makeTransientByteArray(SSC_SIZE,
JCSystem.CLEAR_ON_DESELECT);
tmp = JCSystem.makeTransientByteArray(TMP_SIZE,
JCSystem.CLEAR_ON_DESELECT);
ssc_set = JCSystem.makeTransientBooleanArray((short)1,
JCSystem.CLEAR_ON_DESELECT);
signer = Signature.getInstance(
Signature.ALG_DES_MAC8_ISO9797_1_M2_ALG3, false);
verifier = Signature.getInstance(
Signature.ALG_DES_MAC8_ISO9797_1_M2_ALG3, false);
cipher = Cipher.getInstance(
Cipher.ALG_DES_CBC_ISO9797_M2, false);
decipher = Cipher.getInstance(
Cipher.ALG_DES_CBC_ISO9797_M2, false);
keyMAC = (DESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_DES_TRANSIENT_DESELECT,
KeyBuilder.LENGTH_DES3_2KEY, false);
keyENC = (DESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_DES_TRANSIENT_DESELECT,
KeyBuilder.LENGTH_DES3_2KEY, false);
init();
}
public void init() {
ssc_set[0] = false;
Util.arrayFillNonAtomic(ssc, (short)0, SSC_SIZE, (byte) 0);
Util.arrayFillNonAtomic(tmp, (short)0, TMP_SIZE, (byte) 0);
clearSessionKeys();
}
/**
* Set the MAC and encryption (and decryption) session keys. Each key is a
* 16 byte 3DES EDE key. This method may be called at any time and will
* immediately replace the session key.
*
* @param buffer byte array containing the session keys.
* @param offset location of the session keys in the buffer.
*/
public void setSessionKeys(byte[] buffer, short offset) {
// Check for empty keys
if (Util.arrayCompare(buffer, (short)0, EMPTY_KEY, (short)0, KEY_SIZE) == 0 ||
Util.arrayCompare(buffer, KEY_SIZE, EMPTY_KEY, (short)0, KEY_SIZE) == 0) {
keyMAC.clearKey();
keyENC.clearKey();
} else {
keyMAC.setKey(buffer, offset);
keyENC.setKey(buffer, (short) (offset + KEY_SIZE));
signer.init(keyMAC, Signature.MODE_SIGN);
verifier.init(keyMAC, Signature.MODE_VERIFY);
cipher.init(keyENC, Cipher.MODE_ENCRYPT);
decipher.init(keyENC, Cipher.MODE_DECRYPT);
}
}
/**
* Set the MAC session key. Each key is a 16 byte 3DES EDE key. This method
* may be called at any time and will immediately replace the session key.
*
* @param buffer byte array containing the session key.
* @param offset location of the session key in the buffer.
*/
public void setSessionKeyMAC(byte[] buffer, short offset) {
// Check for empty keys
if (Util.arrayCompare(buffer, (short)0, EMPTY_KEY, (short)0, KEY_SIZE) == 0) {
keyMAC.clearKey();
keyENC.clearKey();
}
else {
keyMAC.setKey(buffer, offset);
signer.init(keyMAC, Signature.MODE_SIGN);
verifier.init(keyMAC, Signature.MODE_VERIFY);
}
}
/**
* Set the encryption session key. Each key is a 16 byte 3DES EDE key. This method
* may be called at any time and will immediately replace the session key.
*
* @param buffer byte array containing the session key.
* @param offset location of the session key in the buffer.
*/
public void setSessionKeyEncryption(byte[] buffer, short offset) {
// Check for empty keys
if (Util.arrayCompare(buffer, (short)0, EMPTY_KEY, (short)0, KEY_SIZE) == 0) {
keyMAC.clearKey();
keyENC.clearKey();
} else {
keyENC.setKey(buffer, (short) (offset + KEY_SIZE));
cipher.init(keyENC, Cipher.MODE_ENCRYPT);
decipher.init(keyENC, Cipher.MODE_DECRYPT);
}
}
/**
* Set the MAC and encryption (and decryption) 3DES session keys to zero.
*/
public void clearSessionKeys() {
keyMAC.clearKey();
keyENC.clearKey();
}
/**
* Unwraps (verify and decrypt) the command APDU located in the APDU buffer.
* The command buffer has to be filled by the APDU.setIncomingAndReceive()
* method beforehand. The verified and decrypted command data get placed at
* the start of the APDU buffer.
*
* @return the length value encoded by DO97, 0 if this object is missing.
*/
public short unwrapCommandAPDU() {
byte[] buf = APDU.getCurrentAPDUBuffer();
short apdu_p = (short) (ISO7816.OFFSET_CDATA & 0xff);
short start_p = apdu_p;
short le = 0;
short do87DataLen = 0;
short do87Data_p = 0;
short do87LenBytes = 0;
short hdrLen = 4;
short hdrPadLen = (short) (8 - hdrLen);
incrementSSC();
if (buf[apdu_p] == (byte) 0x87) {
apdu_p++;
// do87
if ((buf[apdu_p] & 0xff) > 0x80) {
do87LenBytes = (short) (buf[apdu_p] & 0x7f);
apdu_p++;
} else {
do87LenBytes = 1;
}
if (do87LenBytes > 2) // sanity check
ISOException.throwIt(SW_INTERNAL_ERROR);
for (short i = 0; i < do87LenBytes; i++) {
do87DataLen += (short) ((buf[(short)(apdu_p + i)] & 0xff) << (short) ((do87LenBytes - 1 - i) * 8));
}
apdu_p += do87LenBytes;
if (buf[apdu_p] != 1) {
ISOException.throwIt(SW_INTERNAL_ERROR);
}
// store pointer to data and defer decrypt to after mac check (do8e)
do87Data_p = (short) (apdu_p + 1);
apdu_p += do87DataLen;
do87DataLen--; // compensate for 0x01 marker
}
if (buf[apdu_p] == (byte) 0x97) {
// do97
if (buf[++apdu_p] != 1)
ISOException.throwIt(SW_INTERNAL_ERROR);
le = (short) (buf[++apdu_p] & 0xff);
apdu_p++;
}
// do8e
if (buf[apdu_p] != (byte) 0x8e) {
ISOException.throwIt(SW_INTERNAL_ERROR);
}
if (buf[++apdu_p] != 8) {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
// verify mac
verifier.update(ssc, (short)0, (short)ssc.length);
verifier.update(buf, (short)0, hdrLen);
verifier.update(PAD_DATA, (short)0, hdrPadLen);
if (!verifier.verify(buf, start_p, (short) (apdu_p - 1 - start_p), buf,
(short)(apdu_p + 1), MAC_SIZE)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
short lc = 0;
if (do87DataLen != 0) {
// decrypt data, and leave room for lc
lc = decipher.doFinal(buf, do87Data_p, do87DataLen, buf,
(short) (hdrLen + 1));
buf[hdrLen] = (byte) (lc & 0xff);
}
return le;
}
/**
* Wraps (encrypts and build MAC) the response data and places it in the
* APDU buffer starting at offset 0. The buffer can be any buffer including
* the APDU buffer itself. If the length is zero the buffer will not be
* addressed and no response data will be present in the wrapped output.
*
* @param buffer byte array containing the data which needs to be wrapped.
* @param offset location of the data in the buffer.
* @param length of the data in the buffer (in bytes).
* @param status word which has to be wrapped in the response APDU.
* @return the length of the wrapped data in the <apdu> buffer
*/
public short wrapResponseAPDU(byte[] buffer, short offset, short length,
short status) {
byte[] apdu = APDU.getCurrentAPDUBuffer();
short apdu_p = 0;
// smallest multiple of 8 strictly larger than plaintextLen (length + padding)
short do87DataLen = (short) ((((short) (length + 8)) / 8) * 8);
// for 0x01 marker (indicating padding is used)
do87DataLen++;
short do87DataLenBytes = (short)(do87DataLen > 0xff? 2 : 1);
short do87HeaderBytes = getApduBufferOffset(length);
short do87Bytes = (short)(do87HeaderBytes + do87DataLen - 1); // 0x01 is counted twice
boolean hasDo87 = length > 0;
incrementSSC();
short ciphertextLength=0;
if (hasDo87) {
// Copy the plain text to temporary buffer to avoid data corruption.
Util.arrayCopyNonAtomic(buffer, offset, tmp, (short) 0, length);
// Put the cipher text in the proper position.
ciphertextLength = cipher.doFinal(tmp, (short) 0, length, apdu,
do87HeaderBytes);
}
//sanity check
//note that this check
// (possiblyPaddedPlaintextLength != (short)(do87DataLen -1))
//does not always hold because some algs do the padding in the final, some in the init.
if (hasDo87 && (((short) (do87DataLen - 1) != ciphertextLength)))
ISOException.throwIt(SW_INTERNAL_ERROR);
if (hasDo87) {
// build do87
apdu[apdu_p++] = (byte) 0x87;
if (do87DataLen < 0x80) {
apdu[apdu_p++] = (byte)do87DataLen;
} else {
apdu[apdu_p++] = (byte) (0x80 + do87DataLenBytes);
for(short i = (short) (do87DataLenBytes - 1); i >= 0; i--) {
apdu[apdu_p++] = (byte) ((do87DataLen >>> (i * 8)) & 0xff);
}
}
apdu[apdu_p++] = 0x01;
}
if (hasDo87) {
apdu_p = do87Bytes;
}
// build do99
apdu[apdu_p++] = (byte) 0x99;
apdu[apdu_p++] = 0x02;
Util.setShort(apdu, apdu_p, status);
apdu_p += 2;
// calculate and write mac
signer.update(ssc, (short) 0, (short) ssc.length);
signer.sign(apdu, (short) 0, apdu_p, apdu, (short) (apdu_p + 2));
// write do8e
apdu[apdu_p++] = (byte) 0x8e;
apdu[apdu_p++] = 0x08;
apdu_p += 8; // for mac written earlier
return apdu_p;
}
/**
* Increment the send sequence counter.
*/
private void incrementSSC() {
if (ssc == null || ssc.length <= 0) {
return;
}
for (short s = (short) (ssc.length - 1); s >= 0; s--) {
if ((short) ((ssc[s] & 0xff) + 1) > 0xff) {
ssc[s] = 0;
} else {
ssc[s]++;
break;
}
}
}
/***
* Get the amount of space to reserve in the buffer when using secure
* messaging.
*
* @param length length of plain text in which this offset depends.
* @return the amount of space to reserve.
*/
private short getApduBufferOffset(short length) {
short do87Bytes = 2; // 0x87 length data 0x01
// smallest multiple of 8 strictly larger than plaintextLen + 1
// byte is probably the length of the cipher text (including do87 0x01)
short do87DataLen = (short) ((((short) (length + 8) / 8) * 8) + 1);
if (do87DataLen < 0x80) {
do87Bytes++;
} else if (do87DataLen <= 0xff) {
do87Bytes += 2;
} else {
do87Bytes += (short) (length > 0xff ? 2 : 1);
}
return do87Bytes;
}
/**
* Set the SSC
*
* @param buffer byte array containing the SSC
* @param offset location of the data in the buffer
*/
public void setSSC(byte[] buffer, short offset) {
Util.arrayCopy(buffer, offset, ssc, (short)0, SSC_SIZE);
ssc_set[0] = true;
}
/**
* @return size in bytes of the SSC
*/
public short getSSCSize() {
return SSC_SIZE;
}
/**
* @return boolean indicating whether SSC has been set
*/
public boolean isSetSSC() {
return ssc_set[0];
}
}