/**
* This file is part of CardAgent-VCBP which is card agent implementation
* of V Cloud-Based Payments for SimplyTapp mobile platform.
* Copyright 2014 SimplyTapp, Inc.
*
* CardAgent-VCBP 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 3 of the License, or
* (at your option) any later version.
*
* CardAgent-VCBP 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 CardAgent-VCBP. If not, see <http://www.gnu.org/licenses/>.
*/
package com.simplytapp.cardagent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import javacard.framework.APDU;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.simplytapp.cardagent.vcbp.crypto.CryptogramGeneration;
import com.simplytapp.cardagent.vcbp.crypto.OfflineDataAuthentication;
import com.simplytapp.virtualcard.Agent;
import com.simplytapp.virtualcard.ApprovalData;
import com.simplytapp.virtualcard.CardAgentConnector;
import com.simplytapp.virtualcard.TransceiveData;
import com.st.vcbp.data.AccountParamsDynamic;
import com.st.vcbp.data.AccountParamsStatic;
import com.st.vcbp.data.LinkedHashMapFixedSize;
import com.st.vcbp.data.TransactionVerificationLog;
/**
* Implementation of Card Agent based on V Cloud-Based Payments Contactless
* Specifications Version 1.3 July 2014.
*
* @author SimplyTapp, Inc.
* @version 1.3.2 GPL
*/
public final class CardAgent extends Agent {
private static final String LOG_TAG = CardAgent.class.getSimpleName();
private static final long serialVersionUID = 1L;
// 1.3.2
private static final byte[] VERSION = { 0x31, 0x2E, 0x33, 0x2E, 0x32 };
private static final String GCM_MSG_ACCOUNT_PARAMETERS_UPDATE = "apupdate";
private static final String GCM_MSG_DEACTIVATE = "deactivate";
private static final String GCM_MSG_TERMINATE = "terminate";
// Supported APDU commands.
private static final byte INS_SELECT = (byte) 0xA4;
private static final byte INS_GPO = (byte) 0xA8;
private static final byte INS_RR = (byte) 0xB2;
// APDU state definitions.
private static final byte APDU_SENT = (byte) 0x00;
private static final byte APDU_SENDING = (byte) 0x01;
private static final byte APDU_SENDING_LAST = (byte) 0x02;
// Transaction state definitions.
private static final byte TRANSACTION_START = (byte) 0x00;
private static final byte TRANSACTION_SELECT = (byte) 0x01;
private static final byte TRANSACTION_GPO = (byte) 0x02;
private static final byte TRANSACTION_RR = (byte) 0x03;
// V Payment AID
private static final byte[] V_PAYMENT_AID = {
(byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x10
};
//================================================================
// APDUs to communicate with remote card applet.
//================================================================
private static final byte[] APDU_SELECT_CARDAPPLET = {
(byte) 0x00, (byte) 0xA4, (byte) 0x04, (byte) 0x00, (byte) 0x07,
(byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x10,
(byte) 0x00
};
// NOTE: Use extended APDU format.
private static final byte[] APDU_GET_STATIC_ACCOUNT_PARAMETERS = {
(byte) 0x80, (byte) 0x30, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00
};
// NOTE: Use extended APDU format.
private static final byte[] APDU_GET_DYNAMIC_ACCOUNT_PARAMETERS = {
(byte) 0x80, (byte) 0x32, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00
};
// NOTE: Use extended APDU format. APDU header does not include 2-byte Lc.
private static final byte[] APDU_HEADER_PUT_TRANSACTION_VERIFICATION_LOG = {
(byte) 0x80, (byte) 0x34, (byte) 0x00, (byte) 0x00, (byte) 0x00
};
//================================================================
private transient byte apduState = APDU_SENT;
private transient byte transactionState = TRANSACTION_START;
private transient boolean selected = false;
private transient boolean transactionFailed = false;
private transient boolean transactionStartFailed = false;
private transient boolean disabled = false;
private transient boolean terminated = false;
private transient boolean invalidVersion = false;
// Threads to access remote card applet.
private transient Thread tGetAccountParams;
private transient Thread tGetDynamicAccountParams;
private transient Thread tPutTransactionVerificationLog;
private static final int MAX_CONNECT_RETRY = 3;
private transient int connectRetryCounter;
private static final int MAX_TRANSCEIVE_RETRY = 3;
private transient int transceiveRetryCounter;
// Card data.
private AccountParamsStatic accountParamsStatic;
private ArrayDeque<AccountParamsDynamic> arrayAccountParamsDynamic;
// Card data for ODA.
private RSAPrivateCrtKey iccPrivKey;
private LinkedHashMapFixedSize<String, TransactionVerificationLog> transactionVerificationLogs;
private boolean readyToPay;
// Transaction data to keep track of across different APDUs.
private transient ByteBuffer afl;
private transient int aflRecords;
private transient int readRecordCounter;
private transient byte[] dynamicSfi2Record4; // For ODA.
// Transaction data to save in Transaction Verification Log.
private transient String accountParametersIndex;
private transient byte transactionType;
private transient String unpredictableNumber;
private transient int checkInternalTimeToExpire = 0;
private transient long startTime; // DEBUG
private transient Thread tTimeToExpire;
private transient Handler handlerTimeToExpire;
private transient Runnable runnableTimeToExpire;
// DEBUG
private transient long transactionStartTime;
public CardAgent() {
allowNfcTransactions();
denySoftTransactions();
denySocketTransactions();
setAidCategory(AID_CATEGORY_PAYMENT);
try {
registerAid(V_PAYMENT_AID);
}
catch (IOException e) {
}
}
public static void install(CardAgentConnector cardAgentConnector) {
new CardAgent().register(cardAgentConnector);
}
@Override
public void create() {
try {
this.tTimeToExpire = new Thread(new Runnable() {
public void run() {
try {
// preparing a looper on current thread
// the current thread is being detected implicitly
Looper.prepare();
// the handler will automatically bind to the Looper that is attached to the current thread
handlerTimeToExpire = new Handler();
// the thread will start running the message loop and will not normally exit the loop
// unless a problem happens or you quit() the looper
Looper.loop();
}
catch (Throwable t) {
Log.e(LOG_TAG, "tTimeToExpire run Exception Log", t);
}
}
});
this.tTimeToExpire.start();
this.runnableTimeToExpire = new Runnable() {
public void run() {
// Check if Dynamic Account Parameters are expired.
checkTimeToLive();
handlerTimeToExpire.postDelayed(this, checkInternalTimeToExpire);
}
};
}
catch (Exception e) {
Log.e(LOG_TAG, "create Exception Log", e);
}
// Retrieve Account Parameters when card is created.
this.connectRetryCounter = 0;
this.transceiveRetryCounter= 0;
getAccountParams();
}
private synchronized void checkTimeToLive() {
// DEBUG
long millis = System.currentTimeMillis() - this.startTime;
int seconds = (int) (millis / 1000);
int minutes = seconds / 60;
seconds = seconds % 60;
Log.i(LOG_TAG, "checkTimeToLive Timestamp=" + System.currentTimeMillis() +
String.format(" %d:%02d since provision", minutes, seconds));
if ((this.accountParamsStatic != null) &&
(this.arrayAccountParamsDynamic != null) &&
!this.arrayAccountParamsDynamic.isEmpty()) {
final long nextCheckTimestamp = System.currentTimeMillis() + this.checkInternalTimeToExpire;
boolean removedAccountParamsDynamic = false;
Iterator<AccountParamsDynamic> iteratorAccountParamsDynamic = this.arrayAccountParamsDynamic.iterator();
while (iteratorAccountParamsDynamic.hasNext()) {
final long expirationTimestamp = iteratorAccountParamsDynamic.next().getExpirationTimestamp();
// Check if Dynamic Account Parameters will expire before the next check.
if ((expirationTimestamp != 0) &&
(nextCheckTimestamp >= expirationTimestamp)) {
this.arrayAccountParamsDynamic.remove();
removedAccountParamsDynamic = true;
// DEBUG
Log.v(LOG_TAG, "Removed soon to expire dynamic account parameters, ExpirationTimestamp=" + expirationTimestamp);
}
}
if (removedAccountParamsDynamic) {
// Provision additional Dynamic Account Parameters.
this.connectRetryCounter = 0;
this.transceiveRetryCounter = 0;
getDynamicAccountParams(false);
}
}
}
// Called when press "Pay" button (for Activate On Touch) or when selecting Card Always Activated setting.
@Override
public void activated() {
//Log.i(LOG_TAG, "activated");
if (this.tGetAccountParams != null) {
// Block until 'tGetAccountParams' thread has stopped before performing transaction checks.
blockCondition(true, false, false, 100, "activated");
// Provide enough time for message generated in 'tGetAccountParams' thread to be displayed on screen.
try {
Thread.sleep(3000);
}
catch (InterruptedException e) {
}
}
performTransactionChecks(true);
}
// Perform transaction initialization checks.
private void performTransactionChecks(boolean activating) {
if ((this.accountParamsStatic == null) ||
(this.arrayAccountParamsDynamic == null) ||
(this.arrayAccountParamsDynamic.size() == 0)) {
// If transaction started, set flag immediately so 'process' method can check flag in time.
if (!activating) {
this.transactionStartFailed = true;
this.readyToPay = false;
}
// NOTE: Kludge to delay processing so message can be posted.
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
}
try {
if (this.invalidVersion) {
postMessage("Incompatible Card Applet", false, null);
}
else if (this.terminated) {
postMessage("Account is Terminated", false, null);
}
else if (this.disabled) {
postMessage("Account is Disabled", false, null);
}
else if ((this.accountParamsStatic == null) || (this.arrayAccountParamsDynamic == null)) {
postMessage("Missing Account Parameters\n" +
"Please Check Connection is Available and Refresh Card",
false, null);
}
else {
postMessage("No Dynamic Account Parameters\n" +
"to Perform Transactions\n" +
"Attempting to Replenish Account Parameter...",
false, null);
// Provision additional Dynamic Account Parameters.
this.connectRetryCounter = 0;
this.transceiveRetryCounter = 0;
getDynamicAccountParams(false);
}
}
catch (IOException e) {
}
}
}
// Called when press "DONE" button or back (for Activate On Touch setting) or when deselecting Card Always Activated setting.
@Override
public void deactivated() {
}
@Override
public void disconnected() {
}
@Override
public void sentApdu() {
// Check if last APDU sent.
if (this.apduState == APDU_SENDING_LAST) {
// Reset parameter.
this.selected = false;
if (APDU.getCurrentAPDU().getTransactionSuccess()) {
// DEBUG
long transactionStopTime = System.currentTimeMillis();
Log.i(LOG_TAG, "Transaction Timestamp=" + transactionStopTime +
" Elapsed=" + (transactionStopTime - this.transactionStartTime) + "ms");
if (this.transactionVerificationLogs != null) {
// Save transaction data in Transaction Verification Log.
TransactionVerificationLog transactionVerificationLog = new TransactionVerificationLog(this.accountParametersIndex,
this.transactionType,
this.unpredictableNumber);
this.transactionVerificationLogs.put(String.valueOf(transactionVerificationLog.getUtcTimestamp()),
transactionVerificationLog);
// Attempt to save Transaction Verification Log in remote card applet.
putTransactionVerificationLog();
}
}
}
this.apduState = APDU_SENT;
}
/*
* Called when first acceptable Selected APDU (contained in aid_list.xml) is received.
* Note: Not called for subsequent transactions while device is still in reader field.
*
* (non-Javadoc)
* @see com.simplytapp.virtualcard.Agent#transactionStarted()
*/
@Override
public void transactionStarted() {
// DEBUG
this.transactionStartTime = System.currentTimeMillis();
Log.i(LOG_TAG, "transactionStarted Timestamp=" + this.transactionStartTime);
this.transactionState = TRANSACTION_START;
// Initialize transaction data.
this.afl = null;
this.aflRecords = 0;
this.readRecordCounter = 0;
this.dynamicSfi2Record4 = null;
this.accountParametersIndex = null;
this.transactionType = (byte) 0;
this.unpredictableNumber = null;
// NOTE: Workaround for non-VCP specific apps.
this.readyToPay = true;
// Perform transaction checks.
performTransactionChecks(false);
}
/*
* Called when device is removed from field.
*
* (non-Javadoc)
* @see com.simplytapp.virtualcard.Agent#transactionFinished()
*/
@Override
public void transactionFinished() {
// Reset parameters.
this.selected = false;
// If 'transactFailed' remains 'true' in subsequent transaction, it will continue to generate errors in 'process' method.
this.transactionFailed = false;
// Reset transaction data.
this.afl = null;
this.aflRecords = 0;
this.readRecordCounter = 0;
this.dynamicSfi2Record4 = null;
this.accountParametersIndex = null;
this.transactionType = (byte) 0;
this.unpredictableNumber = null;
// NOTE: Workaround for non-VCP specific apps.
this.readyToPay = false;
this.apduState = APDU_SENT;
// Provision additional Dynamic Account Parameters if minimum threshold is reached.
this.connectRetryCounter = 0;
this.transceiveRetryCounter = 0;
getDynamicAccountParams(true);
// Update the state of the class.
try {
saveState();
}
catch (IOException e) {
}
}
private void blockCondition(boolean waitGetAccountParams,
boolean waitGetDynamicAccountParams,
boolean waitPutTransactionVerificationLog,
int sleepInterval,
String caller) {
if (caller == null) {
caller = "blockCondition";
}
// Block until the specified thread(s) has stopped and no longer accessing remote card applet.
while (((this.tGetAccountParams != null) && waitGetAccountParams) ||
((this.tGetDynamicAccountParams != null) && waitGetDynamicAccountParams) ||
((this.tPutTransactionVerificationLog != null) && waitPutTransactionVerificationLog)) {
if ((this.tGetAccountParams != null) && waitGetAccountParams) {
Log.i(LOG_TAG, caller + ", tGetAccountParams is still accessing remote card applet, waiting...");
}
if ((this.tGetDynamicAccountParams != null) && waitGetDynamicAccountParams) {
Log.i(LOG_TAG, caller + ", tGetDynamicAccountParams is still accessing remote card applet, waiting...");
}
if ((this.tPutTransactionVerificationLog != null) && waitPutTransactionVerificationLog) {
Log.i(LOG_TAG, caller + ", tPutTransactionVerificationLog is still accessing remote card applet, waiting...");
}
try {
Thread.sleep(sleepInterval);
}
catch (InterruptedException e) {
}
}
}
@Override
public void messageApproval(boolean approved,
ApprovalData approvalData) {
Log.i(LOG_TAG, "messageApproval");
if (approvalData == null) {
this.readyToPay = approved;
}
}
@Override
public void messageFromRemoteCard(String msg) {
Log.i(LOG_TAG, "messageFromRemoteCard: " + msg);
// Block until there is no thread accessing remote card applet before processing remote message.
blockCondition(true, true, true, 50, "messageFromRemoteCard");
if (msg.equalsIgnoreCase(GCM_MSG_ACCOUNT_PARAMETERS_UPDATE)) {
// Delete existing card data.
this.accountParamsStatic = null;
this.arrayAccountParamsDynamic = null;
this.iccPrivKey = null;
// NOTE: Kludge to delay processing in case there is STBridge connection.
try {
Thread.sleep(100);
if (this.disabled) {
postMessage("Account Has Been Enabled\n" +
"Updating Card",
false, null);
}
else {
postMessage("Account Parameters Has Changed\n" +
"Updating Card",
false, null);
}
Thread.sleep(500);
}
catch (Exception e) {
}
this.connectRetryCounter = 0;
this.transceiveRetryCounter= 0;
getAccountParams();
}
else if (msg.equalsIgnoreCase(GCM_MSG_DEACTIVATE)) {
this.disabled = true;
// Delete existing card data.
this.accountParamsStatic = null;
this.arrayAccountParamsDynamic = null;
this.iccPrivKey = null;
this.handlerTimeToExpire.removeCallbacks(this.runnableTimeToExpire);
try {
postMessage("Account Has Been Disabled", false, null);
}
catch (IOException e) {
}
}
else if (msg.equalsIgnoreCase(GCM_MSG_TERMINATE)) {
this.terminated = true;
this.disabled = true;
// Delete existing card data.
this.accountParamsStatic = null;
this.arrayAccountParamsDynamic = null;
this.iccPrivKey = null;
this.handlerTimeToExpire.removeCallbacks(this.runnableTimeToExpire);
try {
postMessage("Account Has Been Terminated", false, null);
}
catch (IOException e) {
}
}
else {
Log.e(LOG_TAG, "Unknown Remote Message");
}
}
/*
* Used to send a C-APDU to the payment component.
* Returns R-APDU + SW or SW.
*
* (non-Javadoc)
* @see com.simplytapp.virtualcard.Agent#process(javacard.framework.APDU)
*/
@Override
public void process(APDU apdu) throws ISOException {
while (this.apduState != APDU_SENT) { // wait for previous one to complete (thread safe)
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
}
try {
if (getTransactionFinished()) {
this.apduState = APDU_SENDING_LAST;
throw new ISOException(ISO7816.SW_UNKNOWN);
}
}
catch (IOException e) {
}
}
// Check if transaction has already failed.
if (this.transactionFailed) {
this.apduState = APDU_SENDING_LAST;
throw new ISOException(ISO7816.SW_UNKNOWN);
}
// Check if APDU protocol is allowed.
byte protocol = APDU.getProtocol();
if ((protocol != APDU.PROTOCOL_MEDIA_CONTACTLESS_TYPE_A) &&
(protocol != APDU.PROTOCOL_MEDIA_SOFT)) {
sendApduCFailure();
}
// Check if transaction initialization checks has failed.
if (this.transactionStartFailed) {
this.transactionStartFailed = false;
sendApduCFailure(ISO7816.SW_FUNC_NOT_SUPPORTED);
}
byte[] apduBuffer = apdu.getBuffer();
byte claByte = apduBuffer[ISO7816.OFFSET_CLA];
byte insByte = apduBuffer[ISO7816.OFFSET_INS];
// Process C-APDU.
if (claByte == ISO7816.CLA_ISO7816) {
if (insByte == INS_SELECT) { // Select
// Receive C-APDU data.
short apduAidLength = apdu.setIncomingAndReceive();
// DEBUG
Log.v(LOG_TAG, "C-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, apduAidLength + 6));
if ((this.transactionState != TRANSACTION_START) &&
(this.transactionState != TRANSACTION_SELECT)) {
Log.e(LOG_TAG, "Transaction Failure: Out-of-order transaction flow.");
sendApduCFailure(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Check if Lc=[number of data bytes read].
// Check if Le=0x00.
if ((apduAidLength != apdu.getIncomingLength()) ||
(apdu.setOutgoing() != (short) 256)) {
sendApduCFailure(ISO7816.SW_WRONG_LENGTH);
}
ByteBuffer apduByteBuffer = ByteBuffer.wrap(apduBuffer);
// Check if P1=0x04 and P2=0x00.
if (apduByteBuffer.getShort(ISO7816.OFFSET_P1) != (short) 0x0400) {
sendApduCFailure(ISO7816.SW_INCORRECT_P1P2);
}
this.selected = false;
// Check if matching AID.
byte[] aid = this.accountParamsStatic.getAid();
if ((aid != null) &&
(aid.length == apduAidLength) &&
Arrays.equals(aid, Arrays.copyOfRange(apduBuffer, ISO7816.OFFSET_CDATA, ISO7816.OFFSET_CDATA + apduAidLength))) {
// Select, matching AID.
// Build response.
apduByteBuffer.put(PayWConstants.TAG_FCI_TEMPLATE);
// Skip FCI template length.
apduByteBuffer.put((byte) 0);
apduByteBuffer.put(PayWConstants.TAG_DF_NAME);
apduByteBuffer.put((byte) this.accountParamsStatic.getAid().length);
apduByteBuffer.put(this.accountParamsStatic.getAid());
apduByteBuffer.put(this.accountParamsStatic.getTagA5Data());
// Set FCI template length.
apduByteBuffer.put(1, (byte) (apduByteBuffer.position() - 2));
this.selected = true;
}
else {
byte[] ppseAid = this.accountParamsStatic.getAidPpse();
if ((ppseAid != null) &&
(ppseAid.length == apduAidLength) &&
Arrays.equals(ppseAid, Arrays.copyOfRange(apduBuffer, ISO7816.OFFSET_CDATA, ISO7816.OFFSET_CDATA + apduAidLength))) {
// Select, PPSE AID.
// Build response.
apduByteBuffer.put(this.accountParamsStatic.getPpseResponse());
}
else {
sendApduCFailure(ISO7816.SW_FILE_NOT_FOUND);
}
}
this.apduState = APDU_SENDING;
this.transactionState = TRANSACTION_SELECT;
// DEBUG
Log.v(LOG_TAG, "R-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, apduByteBuffer.position()) + "9000");
apdu.setOutgoingLength((short) apduByteBuffer.position());
apdu.sendBytes((short) 0, (short) apduByteBuffer.position());
}
else if (insByte == INS_RR) { // Read Record
if ((this.transactionState != TRANSACTION_GPO) &&
(this.transactionState != TRANSACTION_RR)) {
Log.e(LOG_TAG, "Transaction Failure: Out-of-order transaction flow.");
sendApduCFailure(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
else if (this.selected && this.readyToPay) {
try {
readRecord(apdu);
this.transactionState = TRANSACTION_RR;
}
catch (ISOException isoe) {
sendApduCFailure(isoe.getReason());
}
}
else {
sendApduCFailure();
}
}
else {
sendApduCFailure(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
else if (claByte == (byte) 0x80) {
if (insByte == INS_GPO) { // Get Processing Options
if (this.transactionState != TRANSACTION_SELECT) {
Log.e(LOG_TAG, "Transaction Failure: Out-of-order transaction flow.");
sendApduCFailure(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
else if (this.selected && this.readyToPay) {
try {
getProcessingOptions(apdu);
this.transactionState = TRANSACTION_GPO;
}
catch (ISOException isoe) {
sendApduCFailure(isoe.getReason());
}
}
else {
sendApduCFailure();
}
}
else {
// DEBUG
Log.v(LOG_TAG, "C-APDU Header: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5));
sendApduCFailure(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
else {
// DEBUG
Log.v(LOG_TAG, "C-APDU Header: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5));
sendApduCFailure(ISO7816.SW_CLA_NOT_SUPPORTED);
}
}
/**
* Handle Get Processing Options command.
*
* @param apdu
* the incoming <code>APDU</code> object
* @throws ISOException
*/
private synchronized void getProcessingOptions(APDU apdu) throws ISOException {
byte[] apduBuffer = apdu.getBuffer();
ByteBuffer apduByteBuffer = ByteBuffer.wrap(apduBuffer);
// Check if P1=0x00 and P2=0x00.
if (apduByteBuffer.getShort(ISO7816.OFFSET_P1) != (short) 0x0000) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
short cdataLength = apdu.setIncomingAndReceive();
// DEBUG
Log.v(LOG_TAG, "C-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, cdataLength + 6));
// Check if Lc=[number of data bytes read].
// Check if Lc=0x23.
// Check if Le=0x00.
if ((cdataLength != (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0x00FF)) ||
(cdataLength != (short) 0x23) ||
(apdu.setOutgoing() != (short) 256)) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Check PDOL data.
apduByteBuffer.position(ISO7816.OFFSET_CDATA);
if (apduByteBuffer.getShort() != (short) 0x8321) {
ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED);
}
byte ttqByte1 = apduByteBuffer.get();
byte ttqByte2 = apduByteBuffer.get();
byte ttqByte3 = apduByteBuffer.get();
// Determine CVN from IAD.
byte[] issuerApplicationData = this.accountParamsStatic.getIssuerApplicationData().clone();
byte cvn = (byte) 0xFF;
if (issuerApplicationData[AccountParamsStatic.IAD_VALUE_OFFSET] == (byte) 0x1F) {
cvn = issuerApplicationData[AccountParamsStatic.IAD_VALUE_OFFSET + 1];
}
if (cvn != (byte) 0x43) {
// CVN not supported.
Log.e(LOG_TAG, "Transaction Failure: CVN " + String.format("%02X", cvn) + " not supported.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Default is support MSD.
boolean msd = true;
// Determine transaction type.
if ((ttqByte1 & (byte) 0x20) == (byte) 0x20) {
// Terminal supports qVSDC.
msd = false;
// Set transaction type to save in Transaction Verification Log.
this.transactionType = TransactionVerificationLog.TRANSACTION_TYPE_QVSDC;
// Shift reader data used as input to the cryptogram to offset 0.
// '9F02' 6 bytes Amount, Authorized
// '9F03' 6 bytes Amount, Other
// '9F1A' 2 bytes Terminal Country Code
// '95' 5 bytes Terminal Verification Results (TVR)
// '5F2A' 2 bytes Transaction Currency Code
// '9A' 3 bytes Transaction Date
// '9C' 1 byte Transaction Type
// '9F37' 4 bytes Unpredictable Number
// Total: 29 bytes
System.arraycopy(apduBuffer, ISO7816.OFFSET_CDATA + 6, apduBuffer, 0, 29);
// Re-position offset to later append card data used as input to the cryptogram.
apduByteBuffer.position(29);
// Set unpredictable number to later save in Transaction Verification Log.
this.unpredictableNumber = DataUtil.byteArrayToHexString(apduBuffer, apduByteBuffer.position() - 4, 4);
}
else if ((ttqByte1 & (byte) 0x80) != (byte) 0x80) {
// Terminal does not support MSD.
Log.e(LOG_TAG, "Transaction Failure: Terminal does not support MSD.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
else {
// Check if card supports MSD.
if ((this.accountParamsStatic.getSfiRecord((short) 0x0101) == null) ||
(this.accountParamsStatic.getGpoResponseMsd() == null)) {
Log.e(LOG_TAG, "Transaction Failure: Card does not support MSD.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Set transaction type to save in Transaction Verification Log.
this.transactionType = TransactionVerificationLog.TRANSACTION_TYPE_MSD;
// Set unpredictable number to later save in Transaction Verification Log.
this.unpredictableNumber = "00000000";
}
// Check if Dynamic Account Parameters are available.
if (this.arrayAccountParamsDynamic.isEmpty()) {
Log.e(LOG_TAG, "Transaction Failure: Dynamic Account Parameters not available.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
AccountParamsDynamic accountParamsDynamic = this.arrayAccountParamsDynamic.remove();
long expirationTimestamp = accountParamsDynamic.getExpirationTimestamp();
final long currentTimestamp = System.currentTimeMillis();
// Check if Dynamic Account Parameters are expired.
while ((expirationTimestamp != 0) &&
(currentTimestamp > expirationTimestamp)) {
// Check if additional Dynamic Account Parameters are available.
if (this.arrayAccountParamsDynamic.isEmpty()) {
Log.e(LOG_TAG, "Transaction Failure: Dynamic Account Parameters not available.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
accountParamsDynamic = this.arrayAccountParamsDynamic.remove();
expirationTimestamp = accountParamsDynamic.getExpirationTimestamp();
}
this.accountParametersIndex = accountParamsDynamic.getAccountParamtersIndex();
// Generate MSD cryptogram.
String msdCryptogram = CryptogramGeneration.generateCvn43MsdCryptogram(accountParamsDynamic);
if (msdCryptogram.length() != 6) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Inject Account Parameters Index and MSD Cryptogram into Track 2 Equivalent Data after the Service Code.
byte[] track2EquivalentData = this.accountParamsStatic.getTrack2EquivalentData().clone();
String derivationDataString = this.accountParametersIndex + msdCryptogram + "F";
byte[] derivationData = DataUtil.stringToCompressedByteArray(derivationDataString);
System.arraycopy(derivationData, 0,
track2EquivalentData, AccountParamsStatic.TRACK2_OFFSET_DD, derivationData.length);
boolean transactionSuccess = false;
if (msd) {
// NOTE: TTQ Byte 2 Bit 8, Online Cryptogram Required is ignored.
// Overwrite Track 2 Equivalent Data in record.
byte[] sfi1Record1 = this.accountParamsStatic.getSfiRecord((short) 0x0101);
System.arraycopy(track2EquivalentData, 0, sfi1Record1, 2, track2EquivalentData.length);
// MSD Transaction: Format 1 response.
apduByteBuffer.rewind();
// Build response.
apduByteBuffer.put(PayWConstants.TAG_RESPONSE_MESSAGE_TEMPLATE_FORMAT_1);
byte[] gpoResponseMsd = this.accountParamsStatic.getGpoResponseMsd();
apduByteBuffer.put((byte) (gpoResponseMsd.length - 4));
apduByteBuffer.put(gpoResponseMsd, AccountParamsStatic.GPO_RESPONSE_OFFSET_AIP, 2);
apduByteBuffer.put(gpoResponseMsd,
AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL,
(int) (gpoResponseMsd[AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL_LENGTH] & 0xFF));
// Set AFL for Read Record processing.
try {
this.afl = ByteBuffer.wrap(gpoResponseMsd,
AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL,
(int) (gpoResponseMsd[AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL_LENGTH] & 0xFF));
}
catch (Exception e) {
// AFL not available.
this.afl = null;
}
}
else {
byte[] gpoResponseQvsdc = this.accountParamsStatic.getGpoResponseQvsdc();
final short aip = ByteBuffer.wrap(gpoResponseQvsdc).getShort(AccountParamsStatic.GPO_RESPONSE_OFFSET_AIP);
// Check 'DDA is supported' bit in AIP to determine if ODA is supported.
if ((short) (aip & (short) 0x2000) == (short) 0x2000) {
if (this.iccPrivKey == null) {
Log.e(LOG_TAG, "Transaction Failure: Missing ICC Private Key for ODA.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
if (this.accountParamsStatic.getIccKeyModulusLength() <= 0) {
Log.e(LOG_TAG, "Transaction Failure: Missing ICC Key Modulus Length for ODA.");
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
byte[] cardTransactionQualifier = this.accountParamsStatic.getCardTransactionQualifier().clone();
// Set CTQ Byte 1, bits 8-7 to 00b and Byte 2, bit 8 to 0b.
//byte[] cardTransactionQualifier = this.accountParamsStatic.getCardTransactionQualifier();
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_1] &= (byte) 0x3F;
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_2] &= (byte) 0xEF;
// Set CVR Byte 1 to 00000000b.
issuerApplicationData[AccountParamsStatic.IAD_OFFSET_CVR_BYTE_1] = (byte) 0x00;
// qVSDC CVM Processing.
if ((ttqByte2 & (byte) 0x40) == (byte) 0x40) {
try {
ByteBuffer cvmListBuffer = ByteBuffer.wrap(this.accountParamsStatic.getCvmList());
// Skip amount fields.
cvmListBuffer.position(8);
// Parse CVM List to determine which CVM(s) is/are supported.
while (cvmListBuffer.hasRemaining()) {
// Process CVM Code.
byte cvmCode = cvmListBuffer.get();
boolean applyNext = ((byte) (cvmCode & (byte) 0x40) == (byte) 0x40);
cvmCode = (byte) (cvmCode & (byte) 0x3F);
if ((cvmCode == (byte) 0x02) &&
((ttqByte1 & (byte) 0x04) == (byte) 0x04)) {
// Online PIN supported by account and Online PIN supported by reader.
// Set CTQ Byte 1 bit 8 to 1b.
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_1] |= (byte) 0x80;
// Set CVR Byte 1 to 01101110b.
issuerApplicationData[AccountParamsStatic.IAD_OFFSET_CVR_BYTE_1] = (byte) 0x6E;
}
else if ((cvmCode == (byte) 0x1E) &&
((ttqByte1 & (byte) 0x02) == (byte) 0x02)) {
// Signature supported by account and Signature supported by reader.
// Set CTQ Byte 1 bit 7 to 1b.
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_1] |= (byte) 0x40;
// Set CVR Byte 1 to 01101101b.
issuerApplicationData[AccountParamsStatic.IAD_OFFSET_CVR_BYTE_1] = (byte) 0x6D;
}
else if ((ttqByte3 & (byte) 0x40) == (byte) 0x40) {
// TODO: Consumer Device CVM option.
}
else if (!applyNext) {
// No common CVM found.
cvmListBuffer.position(cvmListBuffer.limit());
}
else {
// Continue processing CVM List, skip CVM Condition.
cvmListBuffer.get();
continue;
}
break;
}
if (!cvmListBuffer.hasRemaining()) {
// No common CVM found.
// Set CTQ Byte 2 bit 8 to 1b.
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_2] |= (byte) 0x80;
// Set CVR Byte 1 to 00000000b. [already done]
}
}
catch (Exception e) {
// No common CVM found due to exception.
// Set CTQ Byte 2 bit 8 to 1b.
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_2] |= (byte) 0x80;
// Set CVR Byte 1 to 00000000b. [already done]
}
}
else {
// CVM not required by reader.
// Set CTQ Byte 2 bit 8 to 1b.
cardTransactionQualifier[AccountParamsStatic.CTQ_OFFSET_BYTE_2] |= (byte) 0x80;
// Set CVR Byte 1 to 00000000b. [already done]
}
// Update IAD.
derivationDataString = "0" + this.accountParametersIndex;
derivationData = DataUtil.stringToCompressedByteArray(derivationDataString);
System.arraycopy(derivationData, 0,
issuerApplicationData, AccountParamsStatic.IAD_OFFSET_DERIVATION_DATA, derivationData.length);
// Check 'DDA is supported' bit in AIP to determine if ODA is supported.
if ((short) (aip & (short) 0x2000) == (short) 0x2000) {
// Process ODA.
final int cardAuthRelatedDataOffset = 256;
// 'generateSdad' requirements:
// - Unpredictable Number, 4 bytes starting at offset 25 in 'apduBuffer'
// - Amount Authorized, 6 bytes starting at offset 0 in 'apduBuffer'
// - Transaction Currency Code, 2 bytes starting at offset 19 in 'apduBuffer'
int sdadEndOffset = OfflineDataAuthentication.generateSdad(accountParamsDynamic,
apduBuffer,
cardAuthRelatedDataOffset,
cardTransactionQualifier,
this.accountParamsStatic.getIccKeyModulusLength(),
this.iccPrivKey);
if (sdadEndOffset == -1) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Construct dynamic SFI 2 Record 4 data.
int dynamicSfi2Record4Offset = cardAuthRelatedDataOffset - 1;
apduBuffer[dynamicSfi2Record4Offset] = (byte) (sdadEndOffset - cardAuthRelatedDataOffset);
if ((int) (apduBuffer[dynamicSfi2Record4Offset] & 0xFF) > 128) {
apduBuffer[--dynamicSfi2Record4Offset] = (byte) 0x81;
}
apduBuffer[--dynamicSfi2Record4Offset] = (byte) 0x70;
this.dynamicSfi2Record4 = Arrays.copyOfRange(apduBuffer, dynamicSfi2Record4Offset, sdadEndOffset);
}
// Append card data used as input to the cryptogram.
// '82' 2 bytes Application Interchange Profile (AIP)
// '9F36' 2 bytes Application Transaction Counter (ATC)
// '9F10' 32 bytes Issuer Application Data (IAD)
// Append AIP.
apduByteBuffer.putShort(aip);
// Append ATC.
apduByteBuffer.putShort(accountParamsDynamic.getAtc());
// Append IAD.
apduByteBuffer.put(issuerApplicationData, AccountParamsStatic.IAD_VALUE_OFFSET,
(int) (issuerApplicationData[AccountParamsStatic.IAD_VALUE_OFFSET - 1] & 0xFF));
// Generate AC.
byte[] ac = CryptogramGeneration.generateCvn43Cryptogram(accountParamsDynamic, apduBuffer, 0, apduByteBuffer.position());
if ((ac == null) || (ac.length != PayWConstants.LENGTH_AC)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// qVSDC Transaction: Format 2 response.
apduByteBuffer.rewind();
// Build response.
apduByteBuffer.put(PayWConstants.TAG_RESPONSE_MESSAGE_TEMPLATE_FORMAT_2);
// Skip response message template length.
apduByteBuffer.put((byte) 0);
// Add to response: AIP and AFL.
apduByteBuffer.put(gpoResponseQvsdc);
// Add to response: IAD.
apduByteBuffer.put(issuerApplicationData);
// Add to response: Track 2 Equivalent Data.
apduByteBuffer.put(track2EquivalentData);
// Add to response: PSN.
apduByteBuffer.put(this.accountParamsStatic.getPanSequenceNumber());
// Add to response: ATC.
apduByteBuffer.putShort(PayWConstants.TAG_APPLICATION_TRANSACTION_COUNTER);
apduByteBuffer.put((byte) 0x02);
apduByteBuffer.putShort(accountParamsDynamic.getAtc());
// Add to response: Application Cryptogram.
apduByteBuffer.putShort(PayWConstants.TAG_APPLICATION_CRYPTOGRAM);
apduByteBuffer.put((byte) PayWConstants.LENGTH_AC);
apduByteBuffer.put(ac);
// Add to response: CTQ.
apduByteBuffer.put(cardTransactionQualifier);
// Add to response: Fixed CID and Form Factor Indicator.
apduByteBuffer.put(DataUtil.stringToCompressedByteArray("9F2701809F6E04238C0000"));
// Set response template message length.
apduByteBuffer.put(1, (byte) (apduByteBuffer.position() - 2));
// Set AFL for Read Record processing.
try {
this.afl = ByteBuffer.wrap(gpoResponseQvsdc,
AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL,
(int) (gpoResponseQvsdc[AccountParamsStatic.GPO_RESPONSE_OFFSET_AFL_LENGTH] & 0xFF));
}
catch (Exception e) {
// AFL not available.
this.afl = null;
}
}
if (transactionSuccess) {
this.apduState = APDU_SENDING_LAST;
}
else {
this.apduState = APDU_SENDING;
}
// DEBUG
Log.v(LOG_TAG, "R-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, apduByteBuffer.position()) + "9000");
apdu.setOutgoingLength((short) apduByteBuffer.position());
apdu.sendBytes((short) 0, (short) apduByteBuffer.position());
if (transactionSuccess) {
// Success triggers a successful transaction.
apdu.setTransactionSuccess();
}
else {
// Set number of records specified in AFL for Read Record processing.
this.afl.mark();
while (this.afl.hasRemaining()) {
// Skip the SFI byte.
this.afl.get();
int aflFirstRecord = (int) (this.afl.get() & 0xFF);
int aflLastRecord = (int) (this.afl.get() & 0xFF);
this.aflRecords += (aflLastRecord - aflFirstRecord + 1);
// Skip the next byte.
this.afl.get();
}
}
}
/**
* Handle Read Record command.
*
* @param apdu
* the incoming <code>APDU</code> object
*/
private void readRecord(APDU apdu) throws ISOException {
byte[] apduBuffer = apdu.getBuffer();
// DEBUG
Log.v(LOG_TAG, "C-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5));
final byte recordNumber = apduBuffer[ISO7816.OFFSET_P1];
// Check P1/P2.
if ((recordNumber == (byte) 0x00) ||
((apduBuffer[ISO7816.OFFSET_P2] & (byte) 0x07) != (byte) 0x04)) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
// Check if Lc is not present.
// Check if Le=0x00.
if ((apdu.setIncomingAndReceive() != (short) 0) ||
(apdu.setOutgoing() != (short) 256)) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
// Check if AFL saved in Get Processing Options or
// if Read Records counter is greater than number of records indicated in AFL.
if ((this.afl == null) ||
(this.readRecordCounter > this.aflRecords)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
// Check if SFI and record supported in AFL.
final byte sfi = (byte) ((apduBuffer[ISO7816.OFFSET_P2] & (byte) 0xF8));
boolean aflSupported = false;
this.afl.reset();
while (this.afl.hasRemaining()) {
byte aflSfi = this.afl.get();
byte aflFirstRecord = this.afl.get();
byte aflLastRecord = this.afl.get();
if ((aflSfi == sfi) &&
(aflFirstRecord <= recordNumber) &&
(aflLastRecord >= recordNumber)) {
aflSupported = true;
break;
}
// Skip the next byte.
this.afl.get();
}
if (!aflSupported) {
Log.e(LOG_TAG, "Transaction Failure: SFI and record not supported in AFL.");
ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND);
}
// Retrieve record.
short sfiRecord = (short) ((sfi << 5) | recordNumber);
byte[] recordData = this.accountParamsStatic.getSfiRecord(sfiRecord);
if (recordData == null) {
if (sfiRecord == (short) 0x0204) {
recordData = this.dynamicSfi2Record4;
}
else {
// Req 7.23
Log.e(LOG_TAG, "Transaction Failure: SFI and record not found.");
ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND);
}
}
// Increment Read Record counter.
this.readRecordCounter++;
// Copy record data to APDU response buffer.
short rdataLength = (short) recordData.length;
System.arraycopy(recordData, 0, apduBuffer, 0, rdataLength);
// Determine if this is the last Read Record command.
if (this.readRecordCounter == this.aflRecords) {
this.apduState = APDU_SENDING_LAST;
}
else {
this.apduState = APDU_SENDING;
}
// DEBUG
Log.v(LOG_TAG, "R-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, rdataLength) + "9000");
apdu.setOutgoingLength(rdataLength);
apdu.sendBytes((short) 0, rdataLength);
// Determine if this is the last Read Record command.
if (this.readRecordCounter == this.aflRecords) {
// Success triggers a successful transaction.
apdu.setTransactionSuccess();
}
}
private void sendApduCFailure() throws ISOException {
sendApduCFailure(ISO7816.SW_COMMAND_NOT_ALLOWED);
}
private void sendApduCFailure(short sw) throws ISOException {
this.apduState = APDU_SENDING_LAST;
// DEBUG
Log.v(LOG_TAG, "R-APDU: " + String.format("%04X", sw));
try {
transactionFailure();
}
catch (IOException e) {
}
this.transactionFailed = true;
throw new ISOException(sw);
}
private String getNonNullMessage(Exception e) {
String exceptionMessage = e.getMessage();
if (exceptionMessage == null) {
exceptionMessage = "null";
}
return exceptionMessage;
}
private void getAccountParams() {
if (this.tGetAccountParams != null) {
Log.i(LOG_TAG, "getAccountParams, tGetAccountParams is still accessing remote card applet.");
return;
}
// Block until 'tGetDynamicAccountParams' and 'tPutTransactionVerificationLog' threads have stopped before continuing.
blockCondition(false, true, true, 200, "getAccountParams");
// NOTE: This thread calls 'setBusy' method when it starts and 'clearBusy' when it stops to
// block agent from processing contactless transaction while the thread is running.
this.tGetAccountParams = new Thread(new Runnable() {
public void run() {
try {
setBusy();
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetAccountParams setBusy IOException Log", e);
try {
postMessage("Card Agent Not Available to\n" +
"Get Account Parameters\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
tGetAccountParams = null;
return;
}
try {
connect();
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetAccountParams connect IOException Log", e);
try {
disconnect();
}
catch (IOException e1) {
}
// Retry connect if error is not NO_CARD.
if (!getNonNullMessage(e).equalsIgnoreCase("NO_CARD")) {
if (connectRetryCounter < MAX_CONNECT_RETRY) {
connectRetryCounter++;
try {
clearBusy();
}
catch (IOException e1) {
}
tGetAccountParams = null;
getAccountParams();
return;
}
}
try {
if (accountParamsStatic == null) {
postMessage("No Connection Available to\n" +
"Get Account Parameters\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
else {
postMessage("No Connection Available to\n" +
"Sync Account Parameters\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
}
catch (IOException e1) {
}
try {
clearBusy();
}
catch (IOException e1) {
}
tGetAccountParams = null;
return;
}
TransceiveData tranceiveDataGetAccountParams = null;
while (true) {
tranceiveDataGetAccountParams = new TransceiveData(TransceiveData.SOFT_CHANNEL);
tranceiveDataGetAccountParams.packCardReset(false);
tranceiveDataGetAccountParams.packApdu(APDU_SELECT_CARDAPPLET, true);
tranceiveDataGetAccountParams.packApdu(APDU_GET_STATIC_ACCOUNT_PARAMETERS, true);
try {
transceive(tranceiveDataGetAccountParams);
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetAccountParams transceive(tranceiveDataGetAccountParams) IOException Log", e);
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
continue;
}
try {
if (accountParamsStatic == null) {
postMessage("Get Account Parameters Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
}
catch (IOException e1) {
}
break;
}
byte[] selectResponse = tranceiveDataGetAccountParams.getNextResponse();
boolean selectError = false;
Short selectSw = null;
if ((selectResponse == null) ||
(selectResponse.length < 2)) {
selectError = true;
}
else {
selectSw = ByteBuffer.wrap(selectResponse).getShort(selectResponse.length - 2);
if ((selectSw != ISO7816.SW_NO_ERROR) ||
((selectResponse.length - 2) != VERSION.length)) {
// Note: Assume length error is due to communication error.
selectError = true;
}
else if (!Arrays.equals(Arrays.copyOf(selectResponse, VERSION.length), VERSION)) {
invalidVersion = true;
}
else {
invalidVersion = false;
}
}
if (selectError || invalidVersion) {
if (invalidVersion) {
try {
Log.e(LOG_TAG, "Incompatible agent version " + new String(VERSION, "UTF-8") +
" and applet version " + new String(Arrays.copyOf(selectResponse, VERSION.length), "UTF-8"));
}
catch (Exception e) {
Log.e(LOG_TAG, "Incompatible agent version " + DataUtil.byteArrayToHexString(VERSION) +
" and applet version " + DataUtil.byteArrayToHexString(selectResponse, 0, VERSION.length));
}
}
else {
Log.e(LOG_TAG, "Invalid selectResponse: " + DataUtil.byteArrayToHexString(selectResponse));
}
try {
if (((selectSw != null) && (selectSw == ISO7816.SW_FUNC_NOT_SUPPORTED)) ||
invalidVersion) {
terminated = true;
disabled = true;
// Delete existing card data.
accountParamsStatic = null;
arrayAccountParamsDynamic = null;
iccPrivKey = null;
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
if (invalidVersion) {
postMessage("Incompatible Card Applet", false, null);
}
else {
postMessage("Account is Terminated", false, null);
}
}
else {
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
continue;
}
if (accountParamsStatic == null) {
postMessage("Account Not Available", false, null);
}
}
}
catch (IOException e) {
}
break;
}
byte[] accountParamsStaticData = tranceiveDataGetAccountParams.getNextResponse();
if ((accountParamsStaticData != null) &&
(accountParamsStaticData.length > 2) &&
(ByteBuffer.wrap(accountParamsStaticData).getShort(accountParamsStaticData.length - 2) == ISO7816.SW_NO_ERROR)) {
// Reset 'disabled' in case agent was in disabled state.
disabled = false;
// Extract Static Account Parameters data without SW.
accountParamsStaticData = Arrays.copyOf(accountParamsStaticData, accountParamsStaticData.length - 2);
ByteArrayInputStream bis = new ByteArrayInputStream(accountParamsStaticData);
ObjectInput in = null;
try {
in = new ObjectInputStream(bis);
accountParamsStatic = (AccountParamsStatic) in.readObject();
}
catch (Exception e) {
Log.e(LOG_TAG, "Cannot serialize accountParamsStaticData: " + DataUtil.byteArrayToHexString(accountParamsStaticData));
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
continue;
}
try {
if (accountParamsStatic == null) {
postMessage("Static Account Parameters Format Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
}
catch (IOException e1) {
}
}
finally {
try {
bis.close();
}
catch (IOException ioe) {
}
try {
if (in != null) {
in.close();
}
}
catch (IOException ioe) {
}
}
}
else {
String invalidResponse = DataUtil.byteArrayToHexString(accountParamsStaticData);
Log.e(LOG_TAG, "Invalid accountParamsStaticData: " + invalidResponse);
try {
if ((invalidResponse.length() == 4) &&
invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_COMMAND_NOT_ALLOWED))) {
disabled = true;
// Delete existing card data.
accountParamsStatic = null;
arrayAccountParamsDynamic = null;
iccPrivKey = null;
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
postMessage("Account is Disabled", false, null);
}
else {
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
continue;
}
if (accountParamsStatic == null) {
postMessage("Invalid Static Account Parameters Data", false, null);
}
}
}
catch (IOException e) {
}
}
break;
} // while (true)
if (accountParamsStatic == null) {
try {
disconnect();
}
catch (IOException e) {
}
try {
clearBusy();
}
catch (IOException e) {
}
tGetAccountParams = null;
return;
}
else {
// DEBUG
try {
Log.v(LOG_TAG, "accountParamsStatic Aid: " + DataUtil.byteArrayToHexString(accountParamsStatic.getAid()));
Log.v(LOG_TAG, "accountParamsStatic AidPpse: " + DataUtil.byteArrayToHexString(accountParamsStatic.getAidPpse()));
Log.v(LOG_TAG, "accountParamsStatic PpseResponse: " + DataUtil.byteArrayToHexString(accountParamsStatic.getPpseResponse()));
Log.v(LOG_TAG, "accountParamsStatic TagA5Data: " + DataUtil.byteArrayToHexString(accountParamsStatic.getTagA5Data()));
Log.v(LOG_TAG, "accountParamsStatic GpoResponseMsd: " + DataUtil.byteArrayToHexString(accountParamsStatic.getGpoResponseMsd()));
Log.v(LOG_TAG, "accountParamsStatic GpoResponseQvsdc: " + DataUtil.byteArrayToHexString(accountParamsStatic.getGpoResponseQvsdc()));
Log.v(LOG_TAG, "accountParamsStatic IssuerApplicationData: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIssuerApplicationData()));
Log.v(LOG_TAG, "accountParamsStatic PanSequenceNumber: " + DataUtil.byteArrayToHexString(accountParamsStatic.getPanSequenceNumber()));
Log.v(LOG_TAG, "accountParamsStatic CardTransactionQualifier: " + DataUtil.byteArrayToHexString(accountParamsStatic.getCardTransactionQualifier()));
Log.v(LOG_TAG, "accountParamsStatic Track2EquivalentData: " + DataUtil.byteArrayToHexString(accountParamsStatic.getTrack2EquivalentData()));
Log.v(LOG_TAG, "accountParamsStatic CardholderName: " + DataUtil.byteArrayToHexString(accountParamsStatic.getCardholderName()));
Log.v(LOG_TAG, "accountParamsStatic CvmList: " + DataUtil.byteArrayToHexString(accountParamsStatic.getCvmList()));
//Log.v(LOG_TAG, "accountParamsStatic IccPrivKeyCrtCoefficient: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIccPrivKeyCrtCoefficient()));
//Log.v(LOG_TAG, "accountParamsStatic IccPrivKeyPrimeExponentQ: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIccPrivKeyPrimeExponentQ()));
//Log.v(LOG_TAG, "accountParamsStatic IccPrivKeyPrimeExponentP: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIccPrivKeyPrimeExponentP()));
//Log.v(LOG_TAG, "accountParamsStatic IccPrivKeyPrimeQ: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIccPrivKeyPrimeQ()));
//Log.v(LOG_TAG, "accountParamsStatic IccPrivKeyPrimeP: " + DataUtil.byteArrayToHexString(accountParamsStatic.getIccPrivKeyPrimeP()));
Log.v(LOG_TAG, "accountParamsStatic IccKeyModulusLength: " + accountParamsStatic.getIccKeyModulusLength());
Log.v(LOG_TAG, "accountParamsStatic MaxNumberAccountParamsDynamic: " + accountParamsStatic.getMaxNumberAccountParamsDynamic());
Log.v(LOG_TAG, "accountParamsStatic MinThresholdNumberAccountParamsDynamic: " + accountParamsStatic.getMinThresholdNumberAccountParamsDynamic());
Log.v(LOG_TAG, "accountParamsStatic CheckIntervalTimeToExpire: " + accountParamsStatic.getCheckIntervalTimeToExpire());
Log.v(LOG_TAG, "accountParamsStatic MaxTransactionVerificationLogs: " + accountParamsStatic.getMaxTransactionVerificationLogs());
}
catch (Exception e) {
Log.e(LOG_TAG, "accountParamsStatic Debug Exception Log", e);
}
}
final int sizeTransactionVerificationLogs = accountParamsStatic.getMaxTransactionVerificationLogs();
if (sizeTransactionVerificationLogs <= 0) {
transactionVerificationLogs = null;
}
else {
if (transactionVerificationLogs == null) {
transactionVerificationLogs = new LinkedHashMapFixedSize<String, TransactionVerificationLog>(sizeTransactionVerificationLogs);
}
else {
transactionVerificationLogs.updateSize(sizeTransactionVerificationLogs);
}
}
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
checkInternalTimeToExpire = accountParamsStatic.getCheckIntervalTimeToExpire() * 60000;
// TEST: Use seconds instead of minutes for testing.
//checkInternalTimeToExpire = accountParamsStatic.getCheckIntervalTimeToExpire() * 1000;
if (checkInternalTimeToExpire > 0) {
// DEBUG
startTime = System.currentTimeMillis();
handlerTimeToExpire.postDelayed(runnableTimeToExpire, checkInternalTimeToExpire);
}
final int maxNumberAccountParamsDynamic = accountParamsStatic.getMaxNumberAccountParamsDynamic();
arrayAccountParamsDynamic = new ArrayDeque<AccountParamsDynamic>(maxNumberAccountParamsDynamic);
final int addNumberAccountParamsDynamic = maxNumberAccountParamsDynamic - arrayAccountParamsDynamic.size();
if (addNumberAccountParamsDynamic <= 0) {
try {
disconnect();
}
catch (IOException e) {
}
try {
clearBusy();
}
catch (IOException e) {
}
tGetAccountParams = null;
return;
}
TransceiveData tranceiveDataGetDynamicAccountParams = null;
int numberAccountParamsDynamic = 0;
while (true) {
tranceiveDataGetDynamicAccountParams = new TransceiveData(TransceiveData.SOFT_CHANNEL);
while (numberAccountParamsDynamic < addNumberAccountParamsDynamic) {
tranceiveDataGetDynamicAccountParams.packApdu(APDU_GET_DYNAMIC_ACCOUNT_PARAMETERS, true);
numberAccountParamsDynamic++;
}
try {
transceive(tranceiveDataGetDynamicAccountParams);
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetAccountParams transceive(tranceiveDataGetDynamicAccountParams) IOException Log", e);
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
numberAccountParamsDynamic = 0;
continue;
}
try {
postMessage("Get Dynamic Account Parameters Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
}
break;
}
try {
disconnect();
}
catch (IOException e) {
}
numberAccountParamsDynamic = 0;
while (numberAccountParamsDynamic < addNumberAccountParamsDynamic) {
syncGetDynamicAccountParams(tranceiveDataGetDynamicAccountParams.getNextResponse());
numberAccountParamsDynamic++;
}
// NOTE: One or more 'syncGetDynamicAccountParams' calls could fail.
// Only display error if all 'syncGetDynamicAccountParams' calls fail.
if (arrayAccountParamsDynamic.size() == 0) {
try {
postMessage("Need to Provision\n" +
"Dynamic Account Parameters\n" +
"to Perform Transactions",
false, null);
}
catch (IOException e) {
}
}
try {
clearBusy();
}
catch (IOException e) {
}
// Initialize ICC Private Key if available.
if ((accountParamsStatic.getIccPrivKeyCrtCoefficient() != null) ||
(accountParamsStatic.getIccPrivKeyPrimeExponentQ() != null) ||
(accountParamsStatic.getIccPrivKeyPrimeExponentP() != null) ||
(accountParamsStatic.getIccPrivKeyPrimeQ() != null) ||
(accountParamsStatic.getIccPrivKeyPrimeP() != null)) {
try {
BigInteger crtCoefficient = new BigInteger(1, accountParamsStatic.getIccPrivKeyCrtCoefficient());
BigInteger primeExponentQ = new BigInteger(1, accountParamsStatic.getIccPrivKeyPrimeExponentQ());
BigInteger primeExponentP = new BigInteger(1, accountParamsStatic.getIccPrivKeyPrimeExponentP());
BigInteger primeQ = new BigInteger(1, accountParamsStatic.getIccPrivKeyPrimeQ());
BigInteger primeP = new BigInteger(1, accountParamsStatic.getIccPrivKeyPrimeP());
BigInteger modulus = primeP.multiply(primeQ);
RSAPrivateCrtKeySpec iccPrivKeySpec = new RSAPrivateCrtKeySpec(modulus,
null,
null,
primeP,
primeQ,
primeExponentP,
primeExponentQ,
crtCoefficient);
// Note: Need to use "BC" provider.
iccPrivKey = (RSAPrivateCrtKey) KeyFactory.getInstance("RSA", "BC").generatePrivate(iccPrivKeySpec);
}
catch (Exception e) {
Log.e(LOG_TAG, "Failed to Initialize ICC Private Key Exception Log", e);
}
// Clear secret data in serializable class.
accountParamsStatic.setIccPrivKeyCrtCoefficient(null, (short) 0, (short) 0);
accountParamsStatic.setIccPrivKeyPrimeExponentQ(null, (short) 0, (short) 0);
accountParamsStatic.setIccPrivKeyPrimeExponentP(null, (short) 0, (short) 0);
accountParamsStatic.setIccPrivKeyPrimeQ(null, (short) 0, (short) 0);
accountParamsStatic.setIccPrivKeyPrimeP(null, (short) 0, (short) 0);
}
tGetAccountParams = null;
}
});
this.tGetAccountParams.start();
}
private void getDynamicAccountParams(final boolean checkMinThreshold) {
// Perform these checks in case user still attempts transactions in these error states.
if (this.invalidVersion || this.terminated || this.disabled) {
Log.e(LOG_TAG, "getDynamicAccountParams not allowed in current agent state.");
return;
}
// Block until 'tGetDynamicAccountParams' thread has stopped before continuing.
blockCondition(false, true, false, 200, "getDynamicAccountParams");
if ((this.accountParamsStatic == null) && !this.disabled) {
try {
postMessage("Missing Account Parameters\n" +
"Please Check Connection is Available and Refresh Card",
false, null);
}
catch (IOException e) {
}
return;
}
if (checkMinThreshold &&
(this.arrayAccountParamsDynamic.size() > this.accountParamsStatic.getMinThresholdNumberAccountParamsDynamic())) {
//Log.i(LOG_TAG, "Not yet minimum threshold number of dynamic account parameters.");
return;
}
final int addNumberAccountParamsDynamic = this.accountParamsStatic.getMaxNumberAccountParamsDynamic() - this.arrayAccountParamsDynamic.size();
if (addNumberAccountParamsDynamic <= 0) {
Log.i(LOG_TAG, "Already maximum number of dynamic account parameters.");
return;
}
// Block until 'tGetAccountParams' and 'tPutTransactionVerificationLog' threads have stopped before continuing.
blockCondition(true, false, true, 200, "getDynamicAccountParams");
// NOTE: This thread does not call 'setBusy' method so agent is not blocked from processing
// contactless transaction while the thread is running.
this.tGetDynamicAccountParams = new Thread(new Runnable() {
public void run() {
try {
connect();
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetDynamicAccountParams connect IOException Log", e);
try {
disconnect();
}
catch (IOException e1) {
}
// Retry connect if error is not NO_CARD.
if (!getNonNullMessage(e).equalsIgnoreCase("NO_CARD")) {
if (connectRetryCounter < MAX_CONNECT_RETRY) {
connectRetryCounter++;
tGetDynamicAccountParams = null;
getDynamicAccountParams(checkMinThreshold);
return;
}
}
try {
postMessage("No Connection Available to\n" +
"Replenish Account Parameter\n" +
arrayAccountParamsDynamic.size() + " Transactions Remaining\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
tGetDynamicAccountParams = null;
return;
}
TransceiveData tranceiveDataGetDynamicAccountParams = null;
int numberAccountParamsDynamic = 0;
while (true) {
tranceiveDataGetDynamicAccountParams = new TransceiveData(TransceiveData.SOFT_CHANNEL);
tranceiveDataGetDynamicAccountParams.packCardReset(false);
tranceiveDataGetDynamicAccountParams.packApdu(APDU_SELECT_CARDAPPLET, true);
while (numberAccountParamsDynamic < addNumberAccountParamsDynamic) {
tranceiveDataGetDynamicAccountParams.packApdu(APDU_GET_DYNAMIC_ACCOUNT_PARAMETERS, true);
numberAccountParamsDynamic++;
}
try {
transceive(tranceiveDataGetDynamicAccountParams);
}
catch (IOException e) {
Log.e(LOG_TAG, "tGetDynamicAccountParams transceive IOException Log", e);
// Retry transceive.
if (transceiveRetryCounter < MAX_TRANSCEIVE_RETRY) {
transceiveRetryCounter++;
numberAccountParamsDynamic = 0;
continue;
}
// Indicate exception occurred.
numberAccountParamsDynamic = -1;
try {
postMessage("Get Dynamic Account Parameters Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
}
break;
}
try {
disconnect();
}
catch (IOException e) {
}
// Check if error already occurred.
if (numberAccountParamsDynamic != -1) {
byte[] selectResponse = tranceiveDataGetDynamicAccountParams.getNextResponse();
if ((selectResponse == null) ||
(selectResponse.length <= 2) ||
(ByteBuffer.wrap(selectResponse).getShort(selectResponse.length - 2) != ISO7816.SW_NO_ERROR)) {
String invalidResponse = DataUtil.byteArrayToHexString(selectResponse);
Log.e(LOG_TAG, "tranceiveDataGetDynamicAccountParams invalid selectResponse: " + invalidResponse);
try {
if ((invalidResponse.length() == 4) &&
invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_FUNC_NOT_SUPPORTED))) {
terminated = true;
disabled = true;
// Delete existing card data.
accountParamsStatic = null;
arrayAccountParamsDynamic = null;
iccPrivKey = null;
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
postMessage("Account is Terminated", false, null);
}
else {
postMessage("Account Not Available", false, null);
}
}
catch (IOException e) {
}
tGetDynamicAccountParams = null;
return;
}
numberAccountParamsDynamic = 0;
while (numberAccountParamsDynamic < addNumberAccountParamsDynamic) {
syncGetDynamicAccountParams(tranceiveDataGetDynamicAccountParams.getNextResponse());
numberAccountParamsDynamic++;
}
// NOTE: One or more 'syncGetDynamicAccountParams' calls could fail.
// Only display error if enough 'syncGetDynamicAccountParams' calls fail to replenish
// Dynamic Account Parameters above minimum threshold.
if (arrayAccountParamsDynamic.size() <= accountParamsStatic.getMinThresholdNumberAccountParamsDynamic()) {
try {
postMessage("Failed to Fully Replenish\n" +
"Dynamic Account Parameter\n" +
arrayAccountParamsDynamic.size() + " Transactions Remaining\n",
false, null);
}
catch (IOException e) {
}
}
}
tGetDynamicAccountParams = null;
}
});
this.tGetDynamicAccountParams.start();
}
private synchronized void syncGetDynamicAccountParams(byte[] accountParamsDynamicData) {
if ((accountParamsDynamicData != null) &&
(accountParamsDynamicData.length > 2) &&
(ByteBuffer.wrap(accountParamsDynamicData).getShort(accountParamsDynamicData.length - 2) == ISO7816.SW_NO_ERROR)) {
// Extract Dynamic Account Parameters data without SW.
accountParamsDynamicData = Arrays.copyOf(accountParamsDynamicData, accountParamsDynamicData.length - 2);
AccountParamsDynamic accountParamsDynamic = null;
ByteArrayInputStream bis = new ByteArrayInputStream(accountParamsDynamicData);
ObjectInput in = null;
try {
in = new ObjectInputStream(bis);
accountParamsDynamic = (AccountParamsDynamic) in.readObject();
}
catch (Exception e) {
Log.e(LOG_TAG, "Cannot serialize accountParamsDynamicData: " + DataUtil.byteArrayToHexString(accountParamsDynamicData));
// Ignore badly formatted Dynamic Account Parameters data.
/*
try {
postMessage("Dynamic Account Parameters Format Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
*/
}
finally {
try {
bis.close();
}
catch (IOException ioe) {
}
try {
if (in != null) {
in.close();
}
}
catch (IOException ioe) {
}
}
if (accountParamsDynamic != null) {
// Set received timestamp.
accountParamsDynamic.setReceivedTimestamp(System.currentTimeMillis());
// DEBUG
try {
Log.v(LOG_TAG, "accountParamsDynamic AccountParamtersIndex: " + accountParamsDynamic.getAccountParamtersIndex());
//Log.v(LOG_TAG, "accountParamsDynamic Luk: " + DataUtil.byteArrayToHexString(accountParamsDynamic.getLuk()));
Log.v(LOG_TAG, "accountParamsDynamic ExpirationTimestamp: " + accountParamsDynamic.getExpirationTimestamp());
Log.v(LOG_TAG, "accountParamsDynamic ReceivedTimestamp: " + accountParamsDynamic.getReceivedTimestamp());
Log.v(LOG_TAG, "accountParamsDynamic Atc: " + String.format("%04X", accountParamsDynamic.getAtc()));
//Log.v(LOG_TAG, "accountParamsDynamic LukMsd: " + DataUtil.byteArrayToHexString(accountParamsDynamic.getLukMsd()));
}
catch (Exception e) {
Log.e(LOG_TAG, "accountParamsDynamic Debug Exception Log", e);
}
arrayAccountParamsDynamic.add(accountParamsDynamic);
}
}
else {
String invalidResponse = DataUtil.byteArrayToHexString(accountParamsDynamicData);
Log.e(LOG_TAG, "Invalid accountParamsDynamicData: " + invalidResponse);
try {
if ((invalidResponse.length() == 4) &&
invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_COMMAND_NOT_ALLOWED))) {
disabled = true;
// Delete existing card data.
accountParamsStatic = null;
arrayAccountParamsDynamic = null;
iccPrivKey = null;
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
postMessage("Account is Disabled", false, null);
}
// Ignore invalid Dynamic Account Parameters data.
/*
else {
postMessage("Invalid Dynamic Account Parameters Data", false, null);
}
*/
}
catch (IOException e) {
}
}
}
private void putTransactionVerificationLog() {
// Do not block if 'tPutTransactionVerificationLog' thread is already running.
// It allows more than 2 transactions to be performed while 'tPutTransactionVerificationLog' thread is already running.
if (this.tPutTransactionVerificationLog != null) {
Log.i(LOG_TAG, "Do not start another 'tPutTransactionVerificationLog' thread.");
return;
}
if ((this.transactionVerificationLogs == null) ||
this.transactionVerificationLogs.isEmpty()) {
Log.i(LOG_TAG, "No Transaction Verification Log to save.");
return;
}
// Block until 'tGetAccountParams' and 'tGetDynamicAccountParams' threads have stopped before continuing.
blockCondition(true, true, false, 200, "putTransactionVerificationLog");
// NOTE: This thread calls 'setBusy' method when it starts and 'clearBusy' when it stops to
// block agent from processing contactless transaction while the thread is running.
this.tPutTransactionVerificationLog = new Thread(new Runnable() {
public void run() {
try {
connect();
}
catch (IOException e) {
Log.e(LOG_TAG, "tPutTransactionVerificationLog connect IOException Log", e);
// No connect retry. Attempt again after next transaction.
try {
disconnect();
}
catch (IOException e1) {
}
tPutTransactionVerificationLog = null;
return;
}
TransceiveData tranceiveDataPutTransactionVerificationLog = new TransceiveData(TransceiveData.SOFT_CHANNEL);
tranceiveDataPutTransactionVerificationLog.packCardReset(false);
tranceiveDataPutTransactionVerificationLog.packApdu(APDU_SELECT_CARDAPPLET, true);
boolean tranceiveTransactionVerificationLog = false;
Iterator<Map.Entry<String, TransactionVerificationLog>> iteratorTransactionVerificationLog = transactionVerificationLogs.entrySet().iterator();
while (iteratorTransactionVerificationLog.hasNext()) {
final Map.Entry<String, TransactionVerificationLog> entry = iteratorTransactionVerificationLog.next();
TransactionVerificationLog transactionVerificationLog = entry.getValue();
// DEBUG
Log.v(LOG_TAG, "Save Transaction Verification Log - "
+ "\n UtcTimestamp: " + transactionVerificationLog.getUtcTimestamp()
//+ "\n AccountParametersIndex: " + transactionVerificationLog.getAccountParametersIndex()
//+ "\n TransactionType: " + transactionVerificationLog.getTransactionType()
//+ "\n UnpredictableNumber: " + transactionVerificationLog.getUnpredictableNumber()
);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutput out = null;
byte[] transactionVerificationLogBytes = null;
try {
out = new ObjectOutputStream(bos);
out.writeObject(transactionVerificationLog);
transactionVerificationLogBytes = bos.toByteArray();
}
catch (Exception e) {
Log.e(LOG_TAG, "Cannot serialize transactionVerificationLog.");
// Remove Transaction Verification Log that fails to serialize.
iteratorTransactionVerificationLog.remove();
}
finally {
try {
if (out != null) {
out.close();
}
}
catch (IOException ioe) {
}
try {
bos.close();
}
catch (IOException ioe) {
}
}
if (transactionVerificationLogBytes != null) {
// NOTE: Including Le in C-APDU causes Lc to be processed incorrectly in remote card applet.
ByteBuffer transactionVerificationLogBuffer = ByteBuffer.allocate(APDU_HEADER_PUT_TRANSACTION_VERIFICATION_LOG.length +
2 +
transactionVerificationLogBytes.length);
transactionVerificationLogBuffer.put(APDU_HEADER_PUT_TRANSACTION_VERIFICATION_LOG);
transactionVerificationLogBuffer.putShort((short) transactionVerificationLogBytes.length);
transactionVerificationLogBuffer.put(transactionVerificationLogBytes);
tranceiveDataPutTransactionVerificationLog.packApdu(transactionVerificationLogBuffer.array(), true);
// Indicate tranceive needs to be performed.
tranceiveTransactionVerificationLog = true;
}
}
if (tranceiveTransactionVerificationLog) {
try {
transceive(tranceiveDataPutTransactionVerificationLog);
}
catch (IOException e) {
Log.e(LOG_TAG, "tPutTransactionVerificationLog transceive IOException Log", e);
// Indicate exception occurred.
tranceiveTransactionVerificationLog = false;
// No error recovery for now. Attempt again after next transaction.
/*
try {
postMessage("Put Transaction Verification Log Error\n" +
"Exception: " + getNonNullMessage(e),
false, null);
}
catch (IOException e1) {
}
*/
}
}
try {
disconnect();
}
catch (IOException e) {
}
if (tranceiveTransactionVerificationLog) {
byte[] selectResponse = tranceiveDataPutTransactionVerificationLog.getNextResponse();
if ((selectResponse == null) ||
(selectResponse.length <= 2) ||
(ByteBuffer.wrap(selectResponse).getShort(selectResponse.length - 2) != ISO7816.SW_NO_ERROR)) {
String invalidResponse = DataUtil.byteArrayToHexString(selectResponse);
Log.e(LOG_TAG, "tranceiveDataPutTransactionVerificationLog invalid selectResponse: " + invalidResponse);
try {
if ((invalidResponse.length() == 4) &&
invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_FUNC_NOT_SUPPORTED))) {
terminated = true;
disabled = true;
// Delete existing card data.
accountParamsStatic = null;
arrayAccountParamsDynamic = null;
iccPrivKey = null;
handlerTimeToExpire.removeCallbacks(runnableTimeToExpire);
postMessage("Account is Terminated", false, null);
}
// No error recovery for now. Attempt again after next transaction.
/*
else {
postMessage("Account Not Available", false, null);
}
*/
}
catch (IOException e) {
}
tPutTransactionVerificationLog = null;
return;
}
byte[] putTransactionVerificationResponse = tranceiveDataPutTransactionVerificationLog.getNextResponse();
while (putTransactionVerificationResponse != null) {
// DEBUG
Log.i(LOG_TAG, "putTransactionVerificationResponse=" + DataUtil.byteArrayToHexString(putTransactionVerificationResponse));
if ((putTransactionVerificationResponse.length > 2) &&
(ByteBuffer.wrap(putTransactionVerificationResponse).getShort(putTransactionVerificationResponse.length - 2) == ISO7816.SW_NO_ERROR)) {
// Extract transaction timestamp for Transaction Verification Log successfully saved in remote card applet without SW.
String transactionTimestamp = DataUtil.byteArrayToHexString(putTransactionVerificationResponse,
0,
putTransactionVerificationResponse.length - 2);
if (transactionTimestamp.endsWith("F")) {
// Remove padding.
transactionTimestamp = transactionTimestamp.substring(0,transactionTimestamp.length() - 1);
}
// Remove Transaction Verification Log successfully saved in remote card applet.
transactionVerificationLogs.remove(transactionTimestamp);
}
putTransactionVerificationResponse = tranceiveDataPutTransactionVerificationLog.getNextResponse();
}
}
tPutTransactionVerificationLog = null;
}
});
this.tPutTransactionVerificationLog.start();
}
}