package de.persosim.simulator.securemessaging;
import static org.globaltester.logging.BasicLogger.DEBUG;
import static org.globaltester.logging.BasicLogger.ERROR;
import static org.globaltester.logging.BasicLogger.TRACE;
import static org.globaltester.logging.BasicLogger.log;
import static org.globaltester.logging.BasicLogger.logException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import org.globaltester.simulator.LogTags;
import de.persosim.simulator.apdu.CommandApdu;
import de.persosim.simulator.apdu.IsoSecureMessagingCommandApdu;
import de.persosim.simulator.apdu.ResponseApdu;
import de.persosim.simulator.crypto.CryptoSupport;
import de.persosim.simulator.crypto.CryptoUtil;
import de.persosim.simulator.platform.Iso7816;
import de.persosim.simulator.platform.Layer;
import de.persosim.simulator.processing.UpdatePropagation;
import de.persosim.simulator.secstatus.SecStatus.SecContext;
import de.persosim.simulator.secstatus.SecStatusEventUpdatePropagation;
import de.persosim.simulator.secstatus.SecStatusMechanismUpdatePropagation;
import de.persosim.simulator.secstatus.SecurityEvent;
import de.persosim.simulator.tlv.PrimitiveTlvDataObject;
import de.persosim.simulator.tlv.TlvConstants;
import de.persosim.simulator.tlv.TlvDataObject;
import de.persosim.simulator.tlv.TlvDataObjectContainer;
import de.persosim.simulator.tlv.TlvValue;
import de.persosim.simulator.utils.HexString;
import de.persosim.simulator.utils.Utils;
/**
* This layer implements secure messaging according to ISO7816-4. Ascending
* APDUs are checked and unwrapped and descending APDUs are wrapped (if the
* matching command APDU was secured).
*
* Each behavior is controlled by state stored within this layer as well as the
* UpdatePropagations provided along the APDU.
*
* @author amay
* @author slutters
*
*/
public class SecureMessaging extends Layer implements TlvConstants{
public static final String SECUREMESSAGING = "SecureMessaging";
/*--------------------------------------------------------------------------------*/
protected SmDataProvider dataProvider = null;
/*--------------------------------------------------------------------------------*/
@Override
public String getLayerName() {
return SECUREMESSAGING;
}
/*--------------------------------------------------------------------------------*/
@Override
public void powerOn() {
super.powerOn();
discardSecureMessagingSession();
}
@Override
public void processAscending() {
if(processingData.getCommandApdu() instanceof IsoSecureMessagingCommandApdu) {
if (((IsoSecureMessagingCommandApdu) processingData.getCommandApdu()).getSecureMessaging() != SM_OFF_OR_NO_INDICATION) {
if (dataProvider != null) {
processIncomingSmApdu();
//propagate changes in SM status
SmDataProviderGenerator smDataProviderGenerator = dataProvider.getSmDataProviderGenerator();
processingData.addUpdatePropagation(this, "init SM after successful PACE", new SecStatusMechanismUpdatePropagation(SecContext.APPLICATION, smDataProviderGenerator));
log(HexString.encode(processingData.getCommandApdu().toByteArray()), LogTags.APDU_TAG_DEC_IN);
log(this, "successfully processed ascending secured APDU", TRACE);
return;
} else {
log(HexString.encode(processingData.getCommandApdu().toByteArray()), LogTags.APDU_TAG_DEC_IN);
log(this, "No SmDataProvider available", ERROR);
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6985_CONDITIONS_OF_USE_NOT_SATISFIED);
processingData.updateResponseAPDU(this, "SecureMessaging not properly initialized", resp);
return;
}
} else {
log(this, "don't process ascending unsecured APDU", TRACE);
}
} else{
log(this, "don't process non interindustry APDU", TRACE);
}
log(HexString.encode(processingData.getCommandApdu().toByteArray()), LogTags.APDU_TAG_DEC_IN);
// if this line is reached the key material needs to be discarded
if (dataProvider != null) {
log(this, "discard key material", DEBUG);
discardSecureMessagingSession();
}
}
private void discardSecureMessagingSession() {
if (dataProvider != null) {
log(this, "discard key material", DEBUG);
dataProvider = null;
} else {
log(this, "no data provider present, nothing to discard", TRACE);
}
if (processingData != null) {
processingData.addUpdatePropagation(this, "Inform the SecStatus about the ended secure messaging session",
new SecStatusEventUpdatePropagation(
SecurityEvent.SECURE_MESSAGING_SESSION_ENDED));
}
}
/**
* Layer specific processing of descending APDUs.
* @param commandApdu.apdu The APDU received by the PICC
* @param processingData data collected during processing of the APDU
*/
@Override
public void processDescending() {
log(HexString.encode(getProcessingData().getResponseApdu().toByteArray()), LogTags.APDU_TAG_DEC_OUT);
if (isSmWrappingApplicable()){
processOutgoingSmApdu();
}
log(this, "successfully processed descending APDU", TRACE);
handleUpdatePropagations();
}
public boolean isSmWrappingApplicable(){
CommandApdu cApdu = processingData.getCommandApdu();
if (!(cApdu instanceof IsoSecureMessagingCommandApdu)) {
log(this, "descending APDU is does not support iso secure messaging",
TRACE);
return false;
}
if (((IsoSecureMessagingCommandApdu) cApdu).wasSecureMessaging()
&& ((IsoSecureMessagingCommandApdu) cApdu).getSecureMessaging() != SM_OFF_OR_NO_INDICATION) {
log(this,
"descending APDU was sm secured but not unwrapped properly",
TRACE);
return false;
}
if (dataProvider == null){
log(this,
"no secure messaging session is established (no secure messaging data provider is set)",
TRACE);
return false;
}
return true;
}
public void handleUpdatePropagations() {
LinkedList<UpdatePropagation> dataProviderList = processingData.getUpdatePropagations(SmDataProvider.class);
for (UpdatePropagation curDataProvider : dataProviderList) {
if (curDataProvider != null && curDataProvider instanceof SmDataProvider) {
setDataProvider((SmDataProvider) curDataProvider);
}
}
}
/**
* This method performs the SM operations for outgoing APDUs if they are needed
*/
public void processOutgoingSmApdu() {
log(this, "START encryption of outgoing SM APDU");
dataProvider.nextOutgoing();
TlvDataObjectContainer container = new TlvDataObjectContainer();
TlvValue dataObject = this.processingData.getResponseApdu().getData();
if((dataObject != null) && (dataObject.getLength() > 0)) {
log(this, "APDU to be sent contains data", TRACE);
byte[] data, postpaddedData, paddedData, encryptedData;
PrimitiveTlvDataObject primitive87;
data = dataObject.toByteArray();
log(this, "data to be padded is: " + HexString.encode(data), TRACE);
paddedData = this.padData(data);
log(this, "padded data is: " + HexString.encode(paddedData), DEBUG);
log(this, "block size is: " + dataProvider.getCipher().getBlockSize(), DEBUG);
encryptedData = CryptoSupport.encrypt(dataProvider.getCipher(), paddedData, dataProvider.getKeyEnc(), dataProvider.getCipherIv());
log(this, "encrypted data is: " + HexString.encode(encryptedData), DEBUG);
postpaddedData = new byte[paddedData.length + 1];
System.arraycopy(encryptedData, 0, postpaddedData, 1, encryptedData.length);
postpaddedData[0] = (byte) 0x01;
primitive87 = new PrimitiveTlvDataObject(TAG_87, postpaddedData);
container.addTlvDataObject(primitive87);
} else{
log(this, "APDU to be sent contains NO data", DEBUG);
}
//add status word
byte[] sw = Utils.toUnsignedByteArray(this.processingData.getResponseApdu().getStatusWord());
PrimitiveTlvDataObject primitive99 = new PrimitiveTlvDataObject(TAG_99, sw);
container.addTlvDataObject(primitive99);
//add MAC
byte[] macedData = this.padAndMac(container);
PrimitiveTlvDataObject primitive8E = new PrimitiveTlvDataObject(TAG_8E, macedData);
container.addTlvDataObject(primitive8E);
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(container, this.processingData.getResponseApdu().getStatusWord());
this.processingData.updateResponseAPDU(this, "Encrypted outgoing SM APDU", resp);
}
protected byte [] padData(byte[] data) {
return CryptoUtil.padData(data, dataProvider.getCipher().getBlockSize());
}
/**
* This method performs the SM operations for incoming APDUs
*/
public void processIncomingSmApdu() {
log(this, "start processing SM APDU", TRACE);
dataProvider.nextIncoming();
CommandApdu smApdu = processingData.getCommandApdu();
log(this, "Incoming SM APDU is: " + smApdu.toString(), DEBUG);
log(this, "Incoming SM APDU is ISO case: " + smApdu.getIsoCase(), DEBUG);
try {
//create new CommandAPDU
CommandApdu plainCommand = extractPlainTextAPDU();
log(this, "plain text APDU is " + plainCommand, DEBUG);
if (verifyMac()) {
log(this, "verification of mac: correct", DEBUG);
//propagate new CommandAPDU
processingData.updateCommandApdu(this, "SM APDU extracted", plainCommand);
} else {
log(this, "verification of mac: failed", ERROR);
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6988_INCORRECT_SM_DATA_OBJECTS);
processingData.updateResponseAPDU(this, "MAC verification failed", resp);
}
} catch (RuntimeException e) {
log(this, "failure while processing incoming APDU", ERROR);
logException(this, e, ERROR);
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6988_INCORRECT_SM_DATA_OBJECTS);
processingData.updateResponseAPDU(this, "decoding sm APDU failed", resp);
}
log(this, "completed processing SM APDU");
}
/**
* This method returns a plain APDU.
* @return a byte array representation of an SM secured APDU
*/
public CommandApdu extractPlainTextAPDU() {
TlvDataObject cryptogram, tlvObject8E, tlvObject97;
byte[] encryptedData, paddedData, data, le, plainApduCommandData, dbgIv;
int isoCaseOfPlainAPDU;
ByteArrayOutputStream apduStream;
log(this, "started extracting SM APDU", TRACE);
if(processingData.getCommandApdu().getIsoCase() != ISO_CASE_4) {
throw new IllegalArgumentException("SM APDU is expected to be ISO case 4");
}
if (!(processingData.getCommandApdu() instanceof IsoSecureMessagingCommandApdu)){
throw new IllegalArgumentException("SM APDU is expected to be an IsoSecureMessagingCommandApdu");
}
TlvDataObjectContainer constructedCommandDataField = processingData.getCommandApdu().getCommandDataObjectContainer();
tlvObject8E = constructedCommandDataField .getTlvDataObject(TAG_8E);
log(this, "TLV object 8E is: " + tlvObject8E, TRACE);
if(tlvObject8E == null) {
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6987_EXPECTED_SM_DATA_OBJECTS_MISSING);
processingData.updateResponseAPDU(this, "SM APDU is expected to contain tag 8E (mac)", resp);
throw new IllegalArgumentException("SM APDU is expected to contain tag 8E (mac)");
}
if (processingData.getCommandApdu().getIns() %2 == 0) {
cryptogram = constructedCommandDataField.getTlvDataObject(TAG_87);
} else {
cryptogram = constructedCommandDataField.getTlvDataObject(TAG_85);
}
tlvObject97 = constructedCommandDataField.getTlvDataObject(TAG_97);
if(cryptogram == null) {
if(tlvObject97 == null) {
isoCaseOfPlainAPDU = 1;
} else{
isoCaseOfPlainAPDU = 2;
}
} else{
if(tlvObject97 == null) {
isoCaseOfPlainAPDU = 3;
} else{
isoCaseOfPlainAPDU = 4;
}
}
apduStream = new ByteArrayOutputStream();
// append extendedLengthIndicator if needed
if (processingData.getCommandApdu().isExtendedLength()) {
apduStream.write(0x00);
}
// append data if present
if(isoCaseOfPlainAPDU > 2) {
log(this, "Cryptogram is: " + cryptogram);
encryptedData = this.getEncryptedDataFromFormattedEncryptedData(cryptogram);
log(this, "encrypted data is: " + HexString.encode(encryptedData));
log(this, "used cipher iv is : " + HexString.encode(dataProvider.getCipherIv().getIV()));
dbgIv = CryptoSupport.decryptWithIvZero(dataProvider.getCipher(), dataProvider.getCipherIv().getIV(), dataProvider.getKeyEnc());
log(this, "decrypted cipher iv is: " + HexString.encode(dbgIv));
paddedData = CryptoSupport.decrypt(dataProvider.getCipher(), encryptedData, dataProvider.getKeyEnc(), dataProvider.getCipherIv());
//TODO should padding be handled differently for odd instruction/tag 85 contents?
log(this, "padded data is: " + HexString.encode(paddedData));
data = this.unpadPlainTextData(paddedData);
log(this, "plain text data is: " + HexString.encode(data));
try {
if (processingData.getCommandApdu().isExtendedLength()) {
apduStream.write(Utils.toUnsignedByteArray((short) data.length));
} else {
apduStream.write(data.length);
}
apduStream.write(data);
} catch (IOException e) {
logException(this, e);
}
}
// append le if present
if((isoCaseOfPlainAPDU == 2) || (isoCaseOfPlainAPDU == 4)) {
log(this, "TLV object 97 is: " + tlvObject97, TRACE);
le = tlvObject97.getValueField();
//ensure correct length of le field
if (processingData.getCommandApdu().isExtendedLength()) {
if (le.length == 1) {
le = new byte[]{0, le[0]};
}
}
try {
apduStream.write(le);
} catch (IOException e) {
logException(this, e);
}
}
plainApduCommandData = apduStream.toByteArray();
CommandApdu result = ((IsoSecureMessagingCommandApdu)this.processingData.getCommandApdu()).rewrapApdu(Iso7816.SM_OFF_OR_NO_INDICATION, plainApduCommandData);
log(this, "completed extracting SM APDU", TRACE);
return result;
}
/**
* This method performs the mac verification for an SM secured APDU.
* @return the result of mac verification: true iff verified, false otherwise
*/
public boolean verifyMac() {
TlvDataObject cryptogram, tlvObject8E, tlvObject97;
byte[] extractedMac, tlv97Plain, tlv87Plain;
byte[] header, paddingHeader, paddingMacInput, macResult;
int paddingLengthHeader, blockSize, lengthOfMacInputData, paddingLengthMacInput;
ByteArrayOutputStream macInputStream;
int isoCaseOfPlainAPDU;
log(this, "started verifying SM APDU", TRACE);
header = this.processingData.getCommandApdu().getHeader();
if(processingData.getCommandApdu().getIsoCase() != ISO_CASE_4) {
throw new IllegalArgumentException("SM APDU is expected to be ISO case 4");
}
TlvDataObjectContainer constructedCommandDataField = processingData.getCommandApdu().getCommandDataObjectContainer();
tlvObject8E = constructedCommandDataField.getTlvDataObject(TAG_8E);
log(this, "TLV object 8E is: " + tlvObject8E, TRACE);
if(tlvObject8E == null) {
throw new IllegalArgumentException("SM APDU is expected to contain tag 8E (mac)");
}
if (processingData.getCommandApdu().getIns() %2 == 0) {
cryptogram = constructedCommandDataField.getTlvDataObject(TAG_87);
} else {
cryptogram = constructedCommandDataField.getTlvDataObject(TAG_85);
}
tlvObject97 = constructedCommandDataField.getTlvDataObject(TAG_97);
if(cryptogram == null) {
if(tlvObject97 == null) {
isoCaseOfPlainAPDU = 1;
} else{
isoCaseOfPlainAPDU = 2;
}
} else{
if(tlvObject97 == null) {
isoCaseOfPlainAPDU = 3;
} else{
isoCaseOfPlainAPDU = 4;
}
}
if((isoCaseOfPlainAPDU == 2) || (isoCaseOfPlainAPDU == 4)) {
log(this, "TLV object 97 is: " + tlvObject97, TRACE);
}
if(isoCaseOfPlainAPDU > 2) {
log(this, "Cryptogram is: " + cryptogram, TRACE);
}
/* verify mac */
blockSize = dataProvider.getCipher().getBlockSize();
macInputStream = new ByteArrayOutputStream();
/* header must be padded to match block size */
paddingLengthHeader = blockSize - header.length;
paddingHeader = new byte[paddingLengthHeader];
Arrays.fill(paddingHeader, (byte) 0x00);
paddingHeader[0] = (byte) 0x80;
try {
macInputStream.write(header);
macInputStream.write(paddingHeader);
} catch (IOException e) {
logException(this, e);
}
lengthOfMacInputData = header.length + paddingHeader.length;
if(isoCaseOfPlainAPDU > 2) {
tlv87Plain = cryptogram.toByteArray();
lengthOfMacInputData += tlv87Plain.length;
try {
macInputStream.write(tlv87Plain);
} catch (IOException e) {
logException(this, e);
}
}
if((isoCaseOfPlainAPDU == 2) || (isoCaseOfPlainAPDU == 4)) {
tlv97Plain = tlvObject97.toByteArray();
lengthOfMacInputData += tlvObject97.getLength();
try {
macInputStream.write(tlv97Plain);
} catch (IOException e) {
logException(this, e);
}
}
if(isoCaseOfPlainAPDU > 1) {
/* mac input must be padded to match block size */
log(this, "length of mac input data is " + lengthOfMacInputData + " bytes", TRACE);
paddingLengthMacInput = blockSize - ((lengthOfMacInputData + 1) % blockSize) + 1;
log(this, "mac input data needs " + paddingLengthMacInput + " bytes padding to match multiple of blockSize " + blockSize, TRACE);
paddingMacInput = new byte[paddingLengthMacInput];
Arrays.fill(paddingMacInput, (byte) 0x00);
paddingMacInput[0] = (byte) 0x80;
log(this, "padding of mac input data is " + HexString.encode(paddingMacInput), TRACE);
try {
macInputStream.write(paddingMacInput);
} catch (IOException e) {
logException(this, e);
}
}
log(this, "padded mac input is " + HexString.encode(macInputStream.toByteArray()), TRACE);
macResult = CryptoSupport.mac(dataProvider.getMac(), dataProvider.getMacAuxiliaryData(),
dataProvider.getCipher(), macInputStream.toByteArray(), dataProvider.getKeyMac(), dataProvider.getMacLength());
log(this, "expected mac is : " + HexString.encode(macResult), DEBUG);
extractedMac = tlvObject8E.getValueField();
log(this, "extracted mac is: " + HexString.encode(extractedMac), DEBUG);
if(Arrays.equals(macResult, extractedMac)) {
log(this, "mac match", DEBUG);
return true;
} else {
log(this, "mac mismatch", ERROR);
return false;
}
}
/**
* This method extracts the encrypted data from the formatted encrypted data
* @param tlvDataObject the formatted encrypted data
* @return the encrypted data
*/
public byte[] getEncryptedDataFromFormattedEncryptedData(TlvDataObject tlvDataObject) {
byte[] encryptedData, tlvDataObjectValuePlain;
tlvDataObjectValuePlain = tlvDataObject.getValueField();
if (tlvDataObject.getTlvTag().equals(TAG_87)) {
encryptedData = Arrays.copyOfRange(tlvDataObjectValuePlain, 1, tlvDataObjectValuePlain.length);
} else {
encryptedData = Arrays.copyOf(tlvDataObjectValuePlain, tlvDataObjectValuePlain.length);
}
return encryptedData;
}
/**
* This method delegates padding of data for mac computation.
* @param unpaddedData the data to be padded
* @return the padded data
*/
public byte[] padDataForMac(byte[] unpaddedData) {
return CryptoUtil.padData(unpaddedData, dataProvider.getCipher().getBlockSize());
}
/**
* This method delegates padding and mac computation of input data.
* @param input the data to be padded and maced
* @return the padded and maced data
*/
public byte[] padAndMac(TlvDataObjectContainer input) {
byte[] dataToBePadded, dataToBeMaced, macedData;
dataToBePadded = input.toByteArray();
dataToBeMaced = padDataForMac(dataToBePadded);
log(this, "data to be maced is: " + HexString.encode(dataToBeMaced));
macedData = CryptoSupport.mac(dataProvider.getMac(), dataProvider.getMacAuxiliaryData(),
dataProvider.getCipher(), dataToBeMaced, dataProvider.getKeyMac(), dataProvider.getMacLength());
return macedData;
}
/**
* This method delegates the unpadding of padded data
* @param paddedData the data to remove the padding from
* @return the unpadded data
*/
public byte[] unpadPlainTextData(byte[] paddedData) {
return unpadData(paddedData, dataProvider.getCipher().getBlockSize());
}
/**
* This method removes the padding from padded data.
* @param paddedData paddedData the data to remove the padding from
* @param blockSize the block size
* @return the unpadded data
*/
public static byte[] unpadData(byte[] paddedData, int blockSize) {
if(paddedData == null) {throw new NullPointerException("padded data must not be null");}
if(blockSize < 1) {throw new IllegalArgumentException("block size must be > 0");}
if(paddedData.length < 1) {throw new IllegalArgumentException("padded data is too short");}
byte[] unpaddedData;
byte currentByte;
int offsetStart, offsetEnd;
offsetStart = 0;
offsetEnd = paddedData.length - 1;
for (int i = 0; i < blockSize; i++) {
currentByte = paddedData[offsetEnd];
if(currentByte == (byte) 0x00) {
offsetEnd--;
} else{
if(currentByte == (byte) 0x80) {
unpaddedData = new byte[offsetEnd - offsetStart];
System.arraycopy(paddedData, offsetStart, unpaddedData, 0, unpaddedData.length);
return unpaddedData;
} else{
throw new IllegalArgumentException("invalid padding");
}
}
}
throw new IllegalArgumentException("invalid padding");
}
private void setDataProvider(SmDataProvider newProvider) {
newProvider.init(dataProvider);
dataProvider = newProvider;
}
@Override
public void initializeForUse() {
// nothing to do here
}
}