/** * This file is part of CardAgent-RemoteMPP-NoDB which is card agent implementation * of M Remote-SE Mobile PayP for SimplyTapp mobile platform. * Copyright 2014 SimplyTapp, Inc. * * CardAgent-RemoteMPP-NoDB 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-RemoteMPP-NoDB 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-RemoteMPP-NoDB. 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.MessageDigest; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.RSAPrivateCrtKeySpec; import java.util.ArrayDeque; import java.util.Arrays; import javacard.framework.APDU; import javacard.framework.ISO7816; import javacard.framework.ISOException; import android.util.Log; import com.simplytapp.cardagent.remotempp.crypto.CryptogramGeneration; import com.simplytapp.cardagent.remotempp.crypto.DataCipher; import com.simplytapp.cardagent.remotempp.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.mmpp.data.CardProfile; import com.st.mmpp.data.PaymentTokenPayloadSingleUseKey; /** * Implementation of Card Agent based on Remote-SE Mobile PayP - * MPP Remote-SE Lite June 2013 - v1.1. * * This version does not support local database and does not require entering * mobile PIN before every transaction. * * @author SimplyTapp, Inc. * @version 1.2.1 GPL */ public final class CardAgent extends Agent { private static final String LOG_TAG = CardAgent.class.getSimpleName(); private static final long serialVersionUID = 1L; // 1.2.1 private static final byte[] VERSION = { 0x31, 0x2E, 0x32, 0x2E, 0x31 }; // Remote Management Information definitions. private static final byte RMI_VERSION_MASK = (byte) 0xE0; private static final byte RMI_VERSION = (byte) 0x60; private static final byte RMI_FUNCTION_MASK = (byte) 0x1F; private static final byte RMI_FUNCTION_PTP_CP = (byte) 0x01; private static final byte RMI_FUNCTION_PTP_SUK = (byte) 0x02; private static final byte RMI_FUNCTION_MOBILE_CHECK = (byte) 0x1C; private static final byte RMI_FUNCTION_MOBILE_PIN_CHANGE = (byte) 0x1D; private static final byte RMI_FUNCTION_DEACTIVATE = (byte) 0x1E; // Proprietary private static final byte RMI_FUNCTION_REMOTE_WIPE = (byte) 0x1F; private static final byte RMI_FORMAT_DISPLAY = (byte) 0x01; // 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; private static final byte INS_CCC = (byte) 0x2A; private static final byte INS_GENAC = (byte) 0xAE; // 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; private static final byte TRANSACTION_AC = (byte) 0x04; // M Payment AID private static final byte[] MC_PAYMENT_AID = { (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04, (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) 0x04, (byte) 0x10, (byte) 0x10, (byte) 0x00 }; // NOTE: Use extended APDU format. private static final byte[] APDU_GET_CARDPROFILE = { (byte) 0x80, (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; // NOTE: P1=0x01 indicates Mobile PIN not used. private static final byte[] APDU_GET_PTPSUK = { (byte) 0x80, (byte) 0x82, (byte) 0x01, (byte) 0x00, (byte) 0x00 }; private static final byte[] APDU_GET_MOBILE_KEY = { (byte) 0x80, (byte) 0x84, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; //================================================================ private static final byte[] ZEROS = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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 twoTap = 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 tGetCardProfile; private transient Thread tGetPtpSuk; private static final int MAX_CONNECT_RETRY = 3; private transient int connectRetryCounter; private CardProfile cardProfile; private ArrayDeque<PaymentTokenPayloadSingleUseKey> arrayPtpSuk; private byte[] pdolData; // POS Cardholder Interaction Information (Tag 'DF4B') stores indicators. private static final byte OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_1 = (byte) 0; private static final byte OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2 = (byte) (OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_1 + 1); // 1 private static final byte OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_3 = (byte) (OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2 + 1); // 2 private static final byte SIZE_POS_CARDHOLDER_INTERACTION_INFO = (byte) (OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_3 + 1); // 3 private byte[] posCardholderInteractionInfo; // PPMS Transaction Details (Tag 'DF4E') keeps track of magstripe transaction details. private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_VERSION_NUMBER = (byte) 0; private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_ATC = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_VERSION_NUMBER + 1); // 1 private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_CID = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_ATC + 2); // 3 private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_1 = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_CID + 1); // 4 private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2 = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_1 + 1); // 5 private static final byte OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_3 = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2 + 1); // 6 private static final byte SIZE_PPMS_TRANSACTION_DETAILS = (byte) (OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_3 + 1); // 7 private byte[] ppmsTransactionDetails; // Transaction Context (Tag 'DF52') to support two-tap transaction. // TODO: MPP Remote-SE Lite specification indicates length is 15, not 13. private static final byte OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED = (byte) 0; private static final byte OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY = (byte) (OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED + 1); // 1 private static final byte OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT = (byte) (OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY + 2); // 3 private static final byte OFFSET_TRANSACTION_CONTEXT_ACK_STATUS = (byte) (OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT + 6); // 9 private static final byte OFFSET_TRANSACTION_CONTEXT_PIN_STATUS = (byte) (OFFSET_TRANSACTION_CONTEXT_ACK_STATUS + 1); // 10 private static final byte OFFSET_TRANSACTION_CONTEXT_LS_EXCEEDED = (byte) (OFFSET_TRANSACTION_CONTEXT_PIN_STATUS + 1); // 11 private static final byte OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT = (byte) (OFFSET_TRANSACTION_CONTEXT_LS_EXCEEDED + 1); // 12 private static final byte SIZE_TRANSACTION_CONTEXT = (byte) (OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT + 1); // 13 private byte[] transactionContext; private boolean pinVerificationSuccessful; private transient long transactionStartTime; public CardAgent() { allowNfcTransactions(); allowSoftTransactions(); denySocketTransactions(); setAidCategory(AID_CATEGORY_PAYMENT); try { registerAid(MC_PAYMENT_AID); } catch (IOException e) { } this.posCardholderInteractionInfo = new byte[SIZE_POS_CARDHOLDER_INTERACTION_INFO]; this.ppmsTransactionDetails = new byte[SIZE_PPMS_TRANSACTION_DETAILS]; this.transactionContext = new byte[SIZE_TRANSACTION_CONTEXT]; } public static void install(CardAgentConnector cardAgentConnector) { new CardAgent().register(cardAgentConnector); } /* * Similar to MPP Remote-SE Lite interface: * initialize(CardProfile) * Used to initialize the payment component with a Card Profile. * * (non-Javadoc) * @see com.simplytapp.virtualcard.Agent#create() */ @Override public void create() { // Retrieve Card Profile when card is created. this.connectRetryCounter = 0; getCardProfile(); } // 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.tGetCardProfile != null) { // Block until 'tGetCardProfile' thread has stopped before performing transaction checks. blockCondition(true, false, 100, "activated"); // Provide enough time for message generated in 'tGetCardProfile' thread to be displayed on screen. try { Thread.sleep(3000); } catch (InterruptedException e) { } } performTransactionChecks(true); } private void performTransactionChecks(boolean activating) { if ((this.cardProfile == null) || (this.arrayPtpSuk == null) || (this.arrayPtpSuk.size() == 0)) { // If transaction started, set flag immediately so 'process' method can check flag in time. if (!activating) { this.transactionStartFailed = true; } // 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.cardProfile == null) || (this.arrayPtpSuk == null)) { postMessage("Missing Card Data\nPlease Check Connection is Available and Refresh Card", false, null); } else { postMessage("No More PTP_SUK to\nPerform Transactions\nAttempting to Get More PTP_SUK...", false, null); // Provision additional PTP_SUK. this.connectRetryCounter = 0; getPtpSuk(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"); } } this.apduState = APDU_SENT; } /* * Called when first acceptable Selected APDU (contained in aid_list.xml) is received. * * Similar to MPP Remote-SE Lite interface: * start(AbstractSingleKey) * Used to enable the support of a Mobile PayP Transaction flow. * * (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); // NOTE: Workaround for not using Mobile PIN. this.pinVerificationSuccessful = true; // Perform transaction checks. performTransactionChecks(false); } /* * Similar to MPP Remote-SE Lite interface: * stop() * Used to finish the transaction and destroy the CardProfile and AbstractSingleUseKey objects. * * (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; this.apduState = APDU_SENT; if (!this.twoTap) { // Reset transaction data. this.pdolData = null; Arrays.fill(this.posCardholderInteractionInfo, (byte) 0x00); Arrays.fill(this.ppmsTransactionDetails, (byte) 0x00); Arrays.fill(this.transactionContext, (byte) 0x00); // Provision additional PTP_SUK if minimum threshold is reached. this.connectRetryCounter = 0; getPtpSuk(true); } this.twoTap = false; // Update the state of the class. try { saveState(); } catch (IOException e) { } } @Override public void messageApproval(boolean approved, ApprovalData approvalData) { Log.i(LOG_TAG, "messageApproval"); } @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, 50, "messageFromRemoteCard"); try { if (!DataCipher.isMobileKeySet()) { Log.e(LOG_TAG, "Missing M_Key to decrypt remote message."); return; } // Decrypt notification message. byte[] msgData = DataCipher.decryptRemoteMessage(DataUtil.stringToCompressedByteArray(msg)); // DEBUG //Log.i(LOG_TAG, "messageFromRemoteCard decrypted: " + DataUtil.byteArrayToHexString(msgData)); if ((msgData == null) || (msgData.length != 15) || ((msgData[0] & RMI_VERSION_MASK) != RMI_VERSION) || ((msgData[1] & RMI_VERSION_MASK) != RMI_VERSION)) { Log.e(LOG_TAG, "Invalid Remote Notification msg=" + DataUtil.byteArrayToHexString(msgData)); return; } byte remoteNotificationFunction = (byte) (msgData[0] & RMI_FUNCTION_MASK); if (remoteNotificationFunction == RMI_FUNCTION_PTP_CP) { this.cardProfile = null; this.arrayPtpSuk = null; // NOTE: Kludge to delay processing in case there is STBridge connection. try { Thread.sleep(100); if ((msgData[1] & RMI_FORMAT_DISPLAY) == RMI_FORMAT_DISPLAY) { if (this.disabled) { postMessage("Account Has Been Enabled\nUpdating Card", false, null); } else { postMessage("Card Data Has Changed\nUpdating Card", false, null); } } Thread.sleep(500); } catch (Exception e) { } this.connectRetryCounter = 0; getCardProfile(); } else if (remoteNotificationFunction == RMI_FUNCTION_PTP_SUK) { // NOTE: Kludge to delay processing in case there is STBridge connection. try { Thread.sleep(100); if ((msgData[1] & RMI_FORMAT_DISPLAY) == RMI_FORMAT_DISPLAY) { postMessage("Updating PTP_SUK", false, null); } Thread.sleep(500); } catch (Exception e) { } // Provision additional PTP_SUK. this.connectRetryCounter = 0; getPtpSuk(false); } else if (remoteNotificationFunction == RMI_FUNCTION_MOBILE_CHECK) { Log.e(LOG_TAG, "RMI_FUNCTION_MOBILE_CHECK Unsupported"); } else if (remoteNotificationFunction == RMI_FUNCTION_MOBILE_PIN_CHANGE) { Log.e(LOG_TAG, "RMI_FUNCTION_MOBILE_PIN_CHANGE Unsupported"); } else if (remoteNotificationFunction == RMI_FUNCTION_DEACTIVATE) { this.disabled = true; this.cardProfile = null; this.arrayPtpSuk = null; if ((msgData[1] & RMI_FORMAT_DISPLAY) == RMI_FORMAT_DISPLAY) { try { postMessage("Account Has Been Disabled", false, null); } catch (IOException e) { } } } else if (remoteNotificationFunction == RMI_FUNCTION_REMOTE_WIPE) { this.terminated = true; this.disabled = true; this.cardProfile = null; this.arrayPtpSuk = null; if ((msgData[1] & RMI_FORMAT_DISPLAY) == RMI_FORMAT_DISPLAY) { try { postMessage("Account Has Been Terminated", false, null); } catch (IOException e) { } } } else { Log.e(LOG_TAG, "Unknown RMI Function msg=" + DataUtil.byteArrayToHexString(msgData)); } } catch (Exception e) { Log.e(LOG_TAG, "messageFromRemoteCard Exception Log", e); } } /* * Similar to MPP Remote-SE Lite interface: * transceive(C-APDU) * 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)); // 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.cardProfile.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(PayPConstants.TAG_FCI_TEMPLATE); // Skip FCI template length. apduByteBuffer.put((byte) 0); apduByteBuffer.put(PayPConstants.TAG_DF_NAME); apduByteBuffer.put((byte) this.cardProfile.getAid().length); apduByteBuffer.put(this.cardProfile.getAid()); apduByteBuffer.put(this.cardProfile.getTagA5Data()); // Set FCI template length. apduByteBuffer.put(1, (byte) (apduByteBuffer.position() - 2)); this.selected = true; } else { byte[] ppseAid = this.cardProfile.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.cardProfile.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) { 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) { try { getProcessingOptions(apdu); this.transactionState = TRANSACTION_GPO; } catch (ISOException isoe) { sendApduCFailure(isoe.getReason()); } } else { sendApduCFailure(); } } else if (insByte == INS_CCC) { // Compute Cryptographic Checksum if (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) { try { computeCryptographicChecksum(apdu); this.transactionState = TRANSACTION_AC; } catch (ISOException isoe) { sendApduCFailure(isoe.getReason()); } } else { sendApduCFailure(); } } else if (insByte == INS_GENAC) { // Generate AC if (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) { try { generateAc(apdu); this.transactionState = TRANSACTION_AC; } 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 void getProcessingOptions(APDU apdu) throws ISOException { byte[] apduBuffer = apdu.getBuffer(); // DEBUG Log.v(LOG_TAG, "C-APDU Header: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5)); 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); } // Check if Lc=[number of data bytes read]. // Check if Lc=3. // Check if Le=0x00. short len = apdu.setIncomingAndReceive(); // DEBUG Log.v(LOG_TAG, "C-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, len + 6)); if ((len != (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0x00FF)) || (len != (short) 3) || (apdu.setOutgoing() != (short) 256)) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // Check PDOL data. apduByteBuffer.position(ISO7816.OFFSET_CDATA); if (apduByteBuffer.getShort() != (short) 0x8301) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } byte terminalType = apduByteBuffer.get(); // Check if terminal type is offline only. if ((terminalType == (byte) 0x13) || (terminalType == (byte) 0x16) || (terminalType == (byte) 0x23) || (terminalType == (byte) 0x26) || (terminalType == (byte) 0x36)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } this.pdolData = new byte[1]; this.pdolData[0] = terminalType; apduByteBuffer.rewind(); // Build response. apduByteBuffer.put(PayPConstants.TAG_RESPONSE_MESSAGE_TEMPLATE); // Skip response message template length. apduByteBuffer.put((byte) 0); // Skip response message template length. // Append data elements in response: // '82' [2] Application Interchange Profile // '94' [var.] Application File Locator apduByteBuffer.put(PayPConstants.TAG_AIP); apduByteBuffer.put((byte) this.cardProfile.getAip().length); apduByteBuffer.put(this.cardProfile.getAip()); apduByteBuffer.put(PayPConstants.TAG_AFL); apduByteBuffer.put((byte) this.cardProfile.getAfl().length); apduByteBuffer.put(this.cardProfile.getAfl()); int rdataLength = apduByteBuffer.position(); // Set response template message length. apduByteBuffer.put(1, (byte) (rdataLength - 2)); this.apduState = APDU_SENDING; // DEBUG Log.v(LOG_TAG, "R-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, rdataLength) + "9000"); apdu.setOutgoingLength((short) rdataLength); apdu.sendBytes((short) 0, (short) rdataLength); } /** * 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)); short recordNumber = (short) (apduBuffer[ISO7816.OFFSET_P1] & (short) 0x00FF); byte sfi = (byte) ((short) (apduBuffer[ISO7816.OFFSET_P2] & (short) 0x00F8) >> (byte) 3); // Check P1/P2. if ((recordNumber == (short) 0x0000) || ((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); } byte[] recordData = null; if (sfi == (byte) 0x01) { if (recordNumber == (byte) 0x01) { recordData = this.cardProfile.getSfi1Record1(); } } else if (sfi == (byte) 0x02) { if (recordNumber == (byte) 0x01) { recordData = this.cardProfile.getSfi2Record1(); } else if (recordNumber == (byte) 0x02) { recordData = this.cardProfile.getSfi2Record2(); } else if (recordNumber == (byte) 0x03) { recordData = this.cardProfile.getSfi2Record3(); } } else { // SFI not found. ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); } if (recordData == null) { // SFI found, record number not found. ISOException.throwIt(ISO7816.SW_RECORD_NOT_FOUND); } short rdataLength = (short) recordData.length; System.arraycopy(recordData, 0, apduBuffer, 0, rdataLength); if (apduBuffer[(byte) 0] == PayPConstants.TAG_READ_RECORD_RESPONSE_MESSAGE_TEMPLATE) { // EMV file, check if record is referenced in AFL. byte[] afl = this.cardProfile.getAfl(); short aflDataOffset = 0; while (aflDataOffset < afl.length) { if ((sfi == (byte) ((short) (afl[aflDataOffset] & (short) 0x00F8) >> (byte) 3)) && (recordNumber >= (short) (afl[(short) (aflDataOffset + (byte) 1)] & (short) 0x00FF)) && (recordNumber <= (short) (afl[(short) (aflDataOffset + (byte) 2)] & (short) 0x00FF))) { // Record is referenced in AFL. break; } aflDataOffset += (byte) 4; } if (aflDataOffset >= afl.length) { // Record is not referenced in AFL. ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } 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); } /** * Handle Compute Cryptographic Checksum command. * * @param apdu * the incoming <code>APDU</code> object * @throws ISOException */ private void computeCryptographicChecksum(APDU apdu) throws ISOException { byte[] apduBuffer = apdu.getBuffer(); // DEBUG Log.v(LOG_TAG, "C-APDU Header: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5)); ByteBuffer apduByteBuffer = ByteBuffer.wrap(apduBuffer); // Check if P1=0x8E and P2=0x80. if (apduByteBuffer.getShort(ISO7816.OFFSET_P1) != (short) 0x8E80) { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } // Check if Lc=[number of data bytes read]. // Check if Lc=16. // Check if Le=0x00. short cdataLength = apdu.setIncomingAndReceive(); // DEBUG Log.v(LOG_TAG, "C-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, cdataLength + 6)); if ((cdataLength != (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0x00FF)) || (cdataLength != (short) 16) || (apdu.setOutgoing() != (short) 256)) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // IF 'Compute Cryptographic Checksum' in Application Control = Compute Cryptographic Checksum not supported if ((this.cardProfile.getApplicationControl()[2] & PayPConstants.APPLICATION_CONTROL_BYTE_3_BIT_CCC_SUPPORTED) == (byte) 0x00) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } byte[] unpredictableNumber = new byte[PayPConstants.LENGTH_UNPREDICTABLE_NUMBER]; byte[] amountAuthorized = new byte[PayPConstants.LENGTH_AMOUNT]; // Retrieve transaction related data. /* Unpredictable Number := Transaction Related Data[1 : 4] Mobile Support Indicator := Transaction Related Data[5] Amount, Authorized (Numeric) := Transaction Related Data[6 : 12] Transaction Currency Code := Transaction Related Data[12 : 13] Terminal Country Code := Transaction Related Data[14 : 15] Terminal Type := Transaction Related Data[16] */ apduByteBuffer.position(ISO7816.OFFSET_CDATA); apduByteBuffer.get(unpredictableNumber); byte mobileSupportIndicator = apduByteBuffer.get(); apduByteBuffer.get(amountAuthorized); short transactionCurrencyCode = apduByteBuffer.getShort(); short terminalCountryCode = apduByteBuffer.getShort(); byte terminalType = apduByteBuffer.get(); // Check if terminal type is offline only. if ((terminalType == (byte) 0x13) || (terminalType == (byte) 0x16) || (terminalType == (byte) 0x23) || (terminalType == (byte) 0x26) || (terminalType == (byte) 0x36)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } // IF Terminal Country Code = CRM Country Code if (terminalCountryCode == this.cardProfile.getCrmCountryCode()) { // Set 'Domestic Transaction' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= PayPConstants.PPMS_CVR_BYTE_2_BIT_DOMESTIC_TRANSACTION; } else { // Set 'International Transaction' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= PayPConstants.PPMS_CVR_BYTE_2_BIT_INTERNATIONAL_TRANSACTION; } // *** Mobile CVM (Cardholder Verification Method) *** boolean skipCRM = false; boolean accept = false; ByteBuffer transactionContextByteBuffer = ByteBuffer.wrap(this.transactionContext); byte tcContextDefined = this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED]; // IF Transaction Context.Context Defined = Magstripe first tap present OR // Transaction Context.Context Defined = First tap present if ((tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_MAGSTRIPE_FIRST_TAP) || (tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_FIRST_TAP)) { // IF (Transaction Context.Context Currency = Transaction Currency Code) AND // (Transaction Context.Context Amount = Amount, Authorized (Numeric)) AND // (Transaction Context.Context Defined = Magstripe first tap present) if ((transactionContextByteBuffer.getShort(OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY) == transactionCurrencyCode) && (Arrays.equals(amountAuthorized, Arrays.copyOfRange(this.transactionContext, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT + PayPConstants.LENGTH_AMOUNT))) && (tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_MAGSTRIPE_FIRST_TAP)) { // *** Second Tap *** // IF Transaction Context.ACK Status = ACK locked if (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_ACK_STATUS] == PayPConstants.TRANSACTION_CONTEXT_ACK_STATUS_ACK_LOCKED) { // Set 'ACK Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_ACK_REQUIRED; // Set 'CVM Required Is Not Satisfied' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= PayPConstants.PPMS_CVR_BYTE_2_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; } // IF Transaction Context.PIN Status = PIN locked if (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] == PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED) { // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Set 'CVM Required Is Not Satisfied' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= PayPConstants.PPMS_CVR_BYTE_2_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; } // Perform CRM. // 'skipCRM' is already initialized to 'false'. } else { // *** Context Conflict *** // Transaction Context.Context Defined := Invalidated context this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_INVALIDATED_CONTEXT; // Transaction Context.ACK Status := No ACK this.transactionContext[OFFSET_TRANSACTION_CONTEXT_ACK_STATUS] = PayPConstants.TRANSACTION_CONTEXT_ACK_STATUS_NO_ACK; // Transaction Context.PIN Status := No PIN this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_NO_PIN; // Transaction Context.Conflicting Context := Context is conflicting this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT] = PayPConstants.TRUE; // Set 'Context Is Conflicting' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_CONTEXT_CONFLICTING; // Do not perform CRM. skipCRM = true; // Decline. // 'accept' is already initialized to 'false'. } } else { // *** First Tap *** // Transaction Context.Context Defined := Magstripe first tap present this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_MAGSTRIPE_FIRST_TAP; // Transaction Context.Context Currency := Transaction Currency Code transactionContextByteBuffer.putShort(OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY, transactionCurrencyCode); // Transaction Context.Context Amount := Amount, Authorized (Numeric) System.arraycopy(amountAuthorized, 0, this.transactionContext, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT, PayPConstants.LENGTH_AMOUNT); // Transaction Context.ACK Status := No ACK this.transactionContext[OFFSET_TRANSACTION_CONTEXT_ACK_STATUS] = PayPConstants.TRANSACTION_CONTEXT_ACK_STATUS_NO_ACK; // Transaction Context.PIN Status := No PIN this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_NO_PIN; // Transaction Context.L&S Exceeded := Lost & Stolen counters not exceeded this.transactionContext[OFFSET_TRANSACTION_CONTEXT_LS_EXCEEDED] = PayPConstants.FALSE; // Transaction Context.Conflicting Context := Context is not conflicting this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT] = PayPConstants.FALSE; // IF 'PIN Pre-entry Allowed' in Magstripe CVM Issuer Options is set AND // 'Offline PIN Verification Successful' in PIN Verification Status is set if (((this.cardProfile.getMagstripeCvmIssuerOptions() & PayPConstants.CVM_ISSUER_BIT_PIN_PRE_ENTRY_ALLOWED) != (byte) 0x00) && this.pinVerificationSuccessful) { // Transaction Context.PIN Status := PIN entered this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED; } else { // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Transaction Context.PIN Status := PIN locked this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED; // Set 'CVM Required Is Not Satisfied' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= PayPConstants.PPMS_CVR_BYTE_2_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; } // Perform CRM. // 'skipCRM' is already initialized to 'false'. } if (!skipCRM) { // Continue same processing for First Tap and Second Tap. // IF ('Reader supports Mobile' is set in Mobile Support Indicator AND // 'Offline PIN required by reader' is set in Mobile Support Indicator AND // Transaction Context.PIN Status != PIN Entered) if (((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_READER_SUPPORTS_MOBILE) != (byte) 0x00) && ((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_OFFLINE_PIN_REQUIRED_READER) != (byte) 0x00) && (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] != PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED)) { // Transaction Context.PIN Status := PIN locked this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED; // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Set 'CVM Required Is Not Satisfied' in PPMS Card Verification Results // Set 'Terminal Erroneously Considers Offline PIN OK' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] |= (byte) (PayPConstants.PPMS_CVR_BYTE_2_BIT_CVM_REQUIRED_IS_NOT_SATISFIED | PayPConstants.PPMS_CVR_BYTE_2_BIT_TERMINAL_ERRONEOUSLY_CONSIDERS_OFFLINE_PIN_OK); } // IF Transaction Context.PIN Status = PIN Entered if (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] == PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED) { // Set 'Offline PIN Verification Successful' in PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_1] |= PayPConstants.PPMS_CVR_BYTE_1_BIT_OFFLINE_PIN_VERIFICATION_SUCCESSFUL; } // *** CRM (Card Risk Management) *** // IF (PPMS Card Verification Results[2-3] AND Card Issuer Action Code - Decline On PPMS) = '0000' if (((this.cardProfile.getCiacDeclinePpms()[0] & this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2]) == (byte) 0x00) && ((this.cardProfile.getCiacDeclinePpms()[1] & this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_3]) == (byte) 0x00)) { // Accept. accept = true; } // else Decline ('accept' is already initialized to 'false'). } else { // Continue processing for Context Conflict. // checkAccsCntrsLimitsSetPPMSCVR(apdu); } // Perform same processing for Accept and Decline. PaymentTokenPayloadSingleUseKey ptpSuk = this.arrayPtpSuk.removeFirst(); // Reset 'Offline PIN Verification Successful' in PIN Verification Status this.pinVerificationSuccessful = false; // PPMS Transaction Details := '01' | ATC | PPMS Cryptogram Information Data | PPMS Card Verification Results this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_VERSION_NUMBER] = (byte) 0x01; ByteBuffer.wrap(this.ppmsTransactionDetails).putShort(OFFSET_PPMS_TRANSACTION_DETAILS_ATC, ptpSuk.getAtc()); boolean transactionSuccess = false; if (accept) { // *** Accept *** // Transaction Context.Context Defined := Previous transaction this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_PREVIOUS_CONTEXT; // 'Transaction Outcome' in PPMS Cryptogram Information Data := Transaction sent online this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CID] = PayPConstants.PPMS_CID_TRANSACTION_SENT_ONLINE; // IF 'Reader supports Mobile' is set in Mobile Support Indicator if ((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_READER_SUPPORTS_MOBILE) != (byte) 0x00) { // Set Offline PIN verification successful in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_OFFLINE_PIN_VERIFICATION_SUCCESSFUL; } // Generate PIN CVC3Track1. byte[] pinCvc3Track1 = CryptogramGeneration.generateCvc3(ptpSuk, this.cardProfile.getPinIvCvc3Track1(), unpredictableNumber, null); // Generate PIN CVC3Track2. byte[] pinCvc3Track2 = CryptogramGeneration.generateCvc3(ptpSuk, this.cardProfile.getPinIvCvc3Track2(), unpredictableNumber, null); if ((pinCvc3Track1 == null) || (pinCvc3Track2 == null)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } apduByteBuffer.rewind(); // Build response. apduByteBuffer.put(PayPConstants.TAG_RESPONSE_MESSAGE_TEMPLATE); // IF 'Reader supports Mobile' is set in Mobile Support Indicator if ((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_READER_SUPPORTS_MOBILE) != (byte) 0x00) { apduByteBuffer.put((byte) 21); } else { apduByteBuffer.put((byte) 15); } // Append common data elements in response: // '9F61' [2] PIN CVC3track2 // '9F60' [2] PIN CVC3Track1 // '9F36' [2] ATC apduByteBuffer.putShort(PayPConstants.TAG_CVC3_TRACK2); apduByteBuffer.put((byte) 2); apduByteBuffer.put(pinCvc3Track2, pinCvc3Track2.length - 2, 2); apduByteBuffer.putShort(PayPConstants.TAG_CVC3_TRACK1); apduByteBuffer.put((byte) 2); apduByteBuffer.put(pinCvc3Track1, pinCvc3Track1.length - 2, 2); apduByteBuffer.putShort(PayPConstants.TAG_APPLICATION_TRANSACTION_COUNTER); apduByteBuffer.put((byte) 2); apduByteBuffer.putShort(ptpSuk.getAtc()); if ((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_READER_SUPPORTS_MOBILE) != (byte) 0x00) { // Append new terminal data element in response: // 'DF4B' [3] POS Cardholder Interaction Information apduByteBuffer.putShort(PayPConstants.TAG_POS_CARDHOLDER_INTERACTION_INFO); apduByteBuffer.put((byte) this.posCardholderInteractionInfo.length); apduByteBuffer.put(this.posCardholderInteractionInfo); } transactionSuccess = true; } else { // *** Decline *** // IF Transaction Context.Context Defined != Invalidated context AND // 'CVM Required Is Not Satisfied' in PPMS CVR is not set if ((this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] != PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_INVALIDATED_CONTEXT) && ((this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CVR_BYTE_2] & PayPConstants.PPMS_CVR_BYTE_2_BIT_CVM_REQUIRED_IS_NOT_SATISFIED) == (byte) 0x00)) { // Transaction Context.Context Defined := Previous transaction this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_PREVIOUS_CONTEXT; } // 'Transaction Outcome' in PPMS Cryptogram Information Data := Transaction declined this.ppmsTransactionDetails[OFFSET_PPMS_TRANSACTION_DETAILS_CID] = PayPConstants.PPMS_CID_TRANSACTION_DECLINED; // IF 'Reader supports Mobile' is set in Mobile Support Indicator if ((mobileSupportIndicator & PayPConstants.MOBILE_SUPPORT_INDICATOR_BIT_READER_SUPPORTS_MOBILE) != (byte) 0x00) { apduByteBuffer.rewind(); // Build response. apduByteBuffer.put(PayPConstants.TAG_RESPONSE_MESSAGE_TEMPLATE); apduByteBuffer.put((byte) 9); // Append data elements in response: // '9F36' [2] ATC // 'DF4B' [3] POS Cardholder Interaction Information apduByteBuffer.putShort(PayPConstants.TAG_APPLICATION_TRANSACTION_COUNTER); apduByteBuffer.put((byte) 2); // (ATC - 1) is used as the ATC returned in the CCC command. apduByteBuffer.putShort((short) (ptpSuk.getAtc() - 1)); apduByteBuffer.putShort(PayPConstants.TAG_POS_CARDHOLDER_INTERACTION_INFO); apduByteBuffer.put((byte) this.posCardholderInteractionInfo.length); apduByteBuffer.put(this.posCardholderInteractionInfo); this.twoTap = true; // NOTE: This triggers generic "transaction failure" dialog in UI. try { transactionFailure(); } catch (IOException e) { } } else { ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); } } this.apduState = APDU_SENDING_LAST; // 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(); } } /** * Handle Generate Application Cryptogram command. * * @param apdu * the incoming <code>APDU</code> object * @throws ISOException */ private void generateAc(APDU apdu) throws ISOException { byte[] apduBuffer = apdu.getBuffer(); // DEBUG Log.v(LOG_TAG, "C-APDU Header: " + DataUtil.byteArrayToHexString(apduBuffer, 0, 5)); ByteBuffer apduByteBuffer = ByteBuffer.wrap(apduBuffer); byte cryptogramType = (byte) (apduBuffer[ISO7816.OFFSET_P1] & PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE); boolean cdaRequested = ((apduBuffer[ISO7816.OFFSET_P1] & PayPConstants.FIRST_GENERATE_AC_P1_BIT_CDA_REQUESTED) == PayPConstants.FIRST_GENERATE_AC_P1_BIT_CDA_REQUESTED); // Validate cryptogram type. // Validate P1. if ((cryptogramType == PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_RFU) || ((apduBuffer[ISO7816.OFFSET_P1] & (byte) 0x2F) != (byte) 0x00)) { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } // Check if P2=0x00. if (apduBuffer[ISO7816.OFFSET_P2] != (byte) 0x00) { 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>=43. // Check if Lc=[CDOL1 Related Data Length]. // Check if Le=0x00. if ((cdataLength != (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0x00FF)) || (cdataLength < (short) 43) || (cdataLength != this.cardProfile.getCdol1RelatedDataLength()) || (apdu.setOutgoing() != (short) 256)) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // Initialize Cryptogram Information Data to unknown value. byte cid = (byte) 0xFF; byte[] amountAuthorized = new byte[PayPConstants.LENGTH_AMOUNT]; byte[] amountOther = new byte[PayPConstants.LENGTH_AMOUNT]; byte[] tvr = new byte[PayPConstants.LENGTH_TVR]; byte[] transactionDate = new byte[PayPConstants.LENGTH_TRANSACTION_DATE]; byte[] unpredictableNumber = new byte[PayPConstants.LENGTH_UNPREDICTABLE_NUMBER]; byte[] iccDynamicNumberTerminal = new byte[PayPConstants.LENGTH_ICC_DYNAMIC_NUMBER_TERMINAL]; byte[] cvmResults = new byte[PayPConstants.LENGTH_CVM_RESULTS]; // Save transaction related data. /* CDOL1 Related Data := Transaction Related Data Amount, Authorized (Numeric) := Transaction Related Data[1 : 6] Amount, Other (Numeric) := Transaction Related Data[7 : 12] Terminal Country Code := Transaction Related Data[13 : 14] Terminal Verification Results := Transaction Related Data[15 : 19] Transaction Currency Code := Transaction Related Data[20 : 21] Transaction Date := Transaction Related Data[22 : 24] Transaction Type := Transaction Related Data[25] Unpredictable Number := Transaction Related Data[26 : 29] Terminal Type := Transaction Related Data[30] Data Authentication Code := Transaction Related Data[31 : 32] ICC Dynamic Number (Terminal) := Transaction Related Data[33 : 40] CVM Results := Transaction Related Data[41 : 43] CDOL1 Extension := Transaction Related Data[44 : CDOL 1 Related Data Length] Note: CDOL1 Extension may be empty */ byte[] cdol1RelatedData = Arrays.copyOfRange(apduBuffer, ISO7816.OFFSET_CDATA, ISO7816.OFFSET_CDATA + cdataLength); apduByteBuffer.position(ISO7816.OFFSET_CDATA); apduByteBuffer.get(amountAuthorized); apduByteBuffer.get(amountOther); short terminalCountryCode = apduByteBuffer.getShort(); apduByteBuffer.get(tvr); short transactionCurrencyCode = apduByteBuffer.getShort(); apduByteBuffer.get(transactionDate); byte transactionType = apduByteBuffer.get(); apduByteBuffer.get(unpredictableNumber); byte terminalType = apduByteBuffer.get(); short dataAuthenticationCode = apduByteBuffer.getShort(); apduByteBuffer.get(iccDynamicNumberTerminal); apduByteBuffer.get(cvmResults); byte[] cvr = new byte[PayPConstants.LENGTH_CVR]; // IF Terminal Country Code = CRM Country Code if (terminalCountryCode == this.cardProfile.getCrmCountryCode()) { // Set 'Domestic Transaction' in Card Verification Results cvr[3] |= PayPConstants.CVR_BYTE_4_BIT_DOMESTIC_TRANSACTION; } else { // Set 'International Transaction' in Card Verification Results cvr[3] |= PayPConstants.CVR_BYTE_4_BIT_INTERNATIONAL_TRANSACTION; } // IF 'Additional Check Table' in Application Control is set if ((this.cardProfile.getApplicationControl()[1] & PayPConstants.APPLICATION_CONTROL_BYTE_2_BIT_ACTIVATE_ADDITIONAL_CHECK_TABLE) == PayPConstants.APPLICATION_CONTROL_BYTE_2_BIT_ACTIVATE_ADDITIONAL_CHECK_TABLE) { //processAddCheckTable(); } // *** Mobile CVM (Cardholder Verification Method) *** boolean skipCRM = false; ByteBuffer transactionContextByteBuffer = ByteBuffer.wrap(this.transactionContext); byte tcContextDefined = this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED]; // IF Transaction Context.Context Defined = First tap present OR // Transaction Context.Context Defined = Magstripe first tap present if ((tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_FIRST_TAP) || (tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_MAGSTRIPE_FIRST_TAP)) { // IF (Transaction Context.Context Currency = Transaction Currency Code) AND // (Transaction Context.Context Amount = Amount, Authorized (Numeric)) AND // (Transaction Context.Context Defined = First tap present) if ((transactionContextByteBuffer.getShort(OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY) == transactionCurrencyCode) && (Arrays.equals(amountAuthorized, Arrays.copyOfRange(this.transactionContext, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT + PayPConstants.LENGTH_AMOUNT))) && (tcContextDefined == PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_FIRST_TAP)) { // *** Second Tap *** // IF Transaction Context.PIN Status = PIN locked if (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] == PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED) { // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Set 'CVM Required Is Not Satisfied' in Card Verification Results cvr[5] |= PayPConstants.CVR_BYTE_6_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; } // Perform CRM. // 'skipCRM' is already initialized to 'false'. } else { // *** Context Conflict *** // Transaction Context.Context Defined := Invalidated context this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_INVALIDATED_CONTEXT; // Transaction Context.ACK Status := No ACK this.transactionContext[OFFSET_TRANSACTION_CONTEXT_ACK_STATUS] = PayPConstants.TRANSACTION_CONTEXT_ACK_STATUS_NO_ACK; // Transaction Context.PIN Status := No PIN this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_NO_PIN; // Transaction Context.Conflicting Context := Context is conflicting this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT] = PayPConstants.TRUE; // Set 'Context Is Conflicting' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_CONTEXT_CONFLICTING; // AAC decided. cryptogramType = PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_AAC; // Do not perform CRM. skipCRM = true; } } else { // *** First Tap *** // Transaction Context.Context Defined := First tap present this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_FIRST_TAP; // Transaction Context.Context Currency := Transaction Currency Code transactionContextByteBuffer.putShort(OFFSET_TRANSACTION_CONTEXT_CONTEXT_CURRENCY, transactionCurrencyCode); // Transaction Context.Context Amount := Amount, Authorized (Numeric) System.arraycopy(amountAuthorized, 0, this.transactionContext, OFFSET_TRANSACTION_CONTEXT_CONTEXT_AMOUNT, PayPConstants.LENGTH_AMOUNT); // Transaction Context.ACK Status := No ACK this.transactionContext[OFFSET_TRANSACTION_CONTEXT_ACK_STATUS] = PayPConstants.TRANSACTION_CONTEXT_ACK_STATUS_NO_ACK; // Transaction Context.PIN Status := No PIN this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_NO_PIN; // Transaction Context.L&S Exceeded := Lost & Stolen counters not exceeded this.transactionContext[OFFSET_TRANSACTION_CONTEXT_LS_EXCEEDED] = PayPConstants.FALSE; // Transaction Context.Conflicting Context := Context is not conflicting this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONFLICTING_CONTEXT] = PayPConstants.FALSE; // IF 'PIN Pre-entry Allowed' in MChip CVM Issuer Options is set AND // 'Offline PIN Verification Successful' in PIN Verification Status is set if (((this.cardProfile.getMchipCvmIssuerOptions() & PayPConstants.CVM_ISSUER_BIT_PIN_PRE_ENTRY_ALLOWED) != (byte) 0x00) && this.pinVerificationSuccessful) { // Transaction Context.PIN Status := PIN entered this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED; } else { // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Transaction Context.PIN Status := PIN locked this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED; // Set 'CVM Required Is Not Satisfied' in Card Verification Results cvr[5] |= PayPConstants.CVR_BYTE_6_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; } // Perform CRM. // 'skipCRM' is already initialized to 'false'. } if (!skipCRM) { // Continue same processing for First Tap and Second Tap. // IF (CVM Results [1][6 : 1] = 000001b OR CVM Results [1][6 : 1] = 000100b) AND // CVM Results [3] = '02' AND // (Transaction Context.PIN Status != PIN Entered)) byte cvmResultsByte1Bits1to6 = (byte) (cvmResults[0] & (byte) 0x3F); if (((cvmResultsByte1Bits1to6 == (byte) 0x01) || (cvmResultsByte1Bits1to6 == (byte) 0x04)) && (cvmResults[2] == (byte) 0x02) && (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] != PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED)) { // Transaction Context.PIN Status := PIN locked this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] = PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_LOCKED; // Set 'PIN Required' in POS Cardholder Interaction Information this.posCardholderInteractionInfo[OFFSET_POS_CARDHOLDER_INTERACTION_INFO_BYTE_2] |= PayPConstants.POS_CARDHOLDER_INTERACTION_INFO_BYTE_2_BIT_PIN_REQUIRED; // Set 'CVM Required Is Not Satisfied' in Card Verification Results cvr[5] |= PayPConstants.CVR_BYTE_6_BIT_CVM_REQUIRED_IS_NOT_SATISFIED; // Set 'Terminal Erroneously Considers Offline PIN OK' in Card Verification Results cvr[3] |= PayPConstants.CVR_BYTE_4_BIT_TERMINAL_ERRONEOUSLY_CONSIDERS_OFFLINE_PIN_OK; } // IF Transaction Context.PIN Status = PIN Entered if (this.transactionContext[OFFSET_TRANSACTION_CONTEXT_PIN_STATUS] == PayPConstants.TRANSACTION_CONTEXT_PIN_STATUS_PIN_ENTERED) { // Set 'Offline PIN Verification Successful' in Card Verification Results cvr[0] |= PayPConstants.CVR_BYTE_1_BIT_OFFLINE_PIN_VERIFICATION_SUCCESSFUL; } // *** CRM (Card Risk Management) *** if (cryptogramType == PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_ARQC) { // *** ARQC Requested *** // IF ('CVR Decisional Part' in Card Verification Results AND Card Issuer Action Code - Decline On ARQC) != '000000' if (((cvr[3] & this.cardProfile.getCiacDeclineOnlineCapable()[0]) != (byte) 0x00) || ((cvr[4] & this.cardProfile.getCiacDeclineOnlineCapable()[1]) != (byte) 0x00) || ((cvr[5] & this.cardProfile.getCiacDeclineOnlineCapable()[2]) != (byte) 0x00)) { // AAC processing. cryptogramType = PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_AAC; } // else continue ARQC processing. } else if (cryptogramType == PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_TC) { // *** TC Requested *** // Check if terminal type is offline only. if ((terminalType == (byte) 0x13) || (terminalType == (byte) 0x16) || (terminalType == (byte) 0x23) || (terminalType == (byte) 0x26) || (terminalType == (byte) 0x36)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } // IF ('CVR Decisional Part' in Card Verification Results AND Card Issuer Action Code - Go Online) != '000000' if (((cvr[3] & this.cardProfile.getCiacDeclineOnlineCapable()[0]) != (byte) 0x00) || ((cvr[4] & this.cardProfile.getCiacDeclineOnlineCapable()[1]) != (byte) 0x00) || ((cvr[5] & this.cardProfile.getCiacDeclineOnlineCapable()[2]) != (byte) 0x00)) { // AAC processing. cryptogramType = PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_AAC; } else { // ARQC processing. cryptogramType = PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_ARQC; } } else { // *** AAC Requested *** // AAC decided. cryptogramType = PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_AAC; } } // Perform same processing for AAC and ARQC. // Reset 'Offline PIN Verification Successful' in PIN Verification Status this.pinVerificationSuccessful = false; // Perform different processing for AAC and ARQC. if (cryptogramType == PayPConstants.GENERATE_AC_P1_CRYPTOGRAM_TYPE_AAC) { // *** AAC Processing *** // IF Transaction Context.Context Defined != Invalidated context AND // 'CVM Required Is Not Satisfied' in CVR is not set if ((this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] != PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_INVALIDATED_CONTEXT) && ((cvr[5] & PayPConstants.CVR_BYTE_6_BIT_CVM_REQUIRED_IS_NOT_SATISFIED) != PayPConstants.CVR_BYTE_6_BIT_CVM_REQUIRED_IS_NOT_SATISFIED)) { // Transaction Context.Context Defined := Previous transaction this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_PREVIOUS_CONTEXT; } // 'AC Returned In First Generate AC' in Card Verification Results := AAC Returned In First Generate AC // 'AC Returned In Second Generate AC' in Card Verification Results := AC Not Requested In Second Generate AC cvr[0] |= (byte) (PayPConstants.CVR_BYTE_1_AAC_RETURNED_IN_FIRST_GENERATE_AC | PayPConstants.CVR_BYTE_1_AC_NOT_REQUESTED_IN_SECOND_GENERATE_AC); // 'Type Of Cryptogram' in Cryptogram Information Data := AAC cid = PayPConstants.CID_AAC; } else { // *** ARQC Processing *** // Transaction Context.Context Defined := Previous transaction this.transactionContext[OFFSET_TRANSACTION_CONTEXT_CONTEXT_DEFINED] = PayPConstants.TRANSACTION_CONTEXT_CONTEXT_DEFINED_PREVIOUS_CONTEXT; // 'AC Returned In First Generate AC' in Card Verification Results := ARQC Returned In First Generate AC // 'AC Returned In Second Generate AC' in Card Verification Results := AC Not Requested In Second Generate AC cvr[0] |= (byte) (PayPConstants.CVR_BYTE_1_ARQC_RETURNED_IN_FIRST_GENERATE_AC | PayPConstants.CVR_BYTE_1_AC_NOT_REQUESTED_IN_SECOND_GENERATE_AC); // 'Type Of Cryptogram' in Cryptogram Information Data := ARQC cid = PayPConstants.CID_ARQC; // IF 'Combined DDA/AC Generation Requested' in Reference Control Parameter is set if (cdaRequested) { // Set 'Combined DDA/AC Generation Returned In First Generate AC' in Card Verification Results cvr[1] |= PayPConstants.CVR_BYTE_2_BIT_CDA_GENERATION_RETURNED_IN_FIRST_GENERATE_AC; } } // Continue processing for AAC and ARQC. // *** Standard Application Cryptogram Generation *** PaymentTokenPayloadSingleUseKey ptpSuk = this.arrayPtpSuk.removeFirst(); //buildCountersField(apduBuffer); // CVR Byte 1 in sample transaction is 'A5'. Bit 3 is RFU so not sure what it represents. // Set CVR Byte 1 Bit 3 manually. cvr[0] |= (byte) 0x04; // Build the input for Application Cryptogram generation: // Amount, Authorized (Numeric) [6] // Amount, Other (Numeric) [6] // Terminal Country Code [2] // Terminal Verification Results [5] // Transaction Currency Code [2] // Transaction Date [3] // Transaction Type [1] // Unpredictable Number [4] // Application Interchange Profile [2] // Application Transaction Counter [2] // Card Verification Results [6] final int acInputOffset = 256; apduByteBuffer.position(acInputOffset); // Move data buffer Amount Authorized to Unpredictable Number. apduByteBuffer.put(cdol1RelatedData, 0, 29); apduByteBuffer.put(this.cardProfile.getAip()); apduByteBuffer.putShort(ptpSuk.getAtc()); apduByteBuffer.put(cvr); // Generate Application Cryptogram. // Pad first. apduByteBuffer.put((byte) 0x80); byte[] ac = CryptogramGeneration.generateCvn14Cryptogram(ptpSuk, apduByteBuffer.array(), acInputOffset, apduByteBuffer.position() - acInputOffset, null); if ((ac == null) || (ac.length != PayPConstants.LENGTH_AC)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } // Build Issuer Application Data: // Key Derivation Index [1] // Cryptogram Version Number [1] // Card Verification Results [6] // DAC/ICC Dyn Nr [2] // Plaintext Counters [8] byte[] issuerAppData = new byte[PayPConstants.LENGTH_ISSUER_APPLICATION_DATA]; ByteBuffer issuerAppDataByteBuffer = ByteBuffer.wrap(issuerAppData); issuerAppDataByteBuffer.put(this.cardProfile.getKeyDerivationIndex()); // Cryptogram Version Number for MPP Remote-SE Lite is '14'. issuerAppDataByteBuffer.put((byte) 0x14); issuerAppDataByteBuffer.put(cvr); // IF ICC Dynamic Number (Terminal) = '0000000000000000' if (Arrays.equals(iccDynamicNumberTerminal, ZEROS)) { // DAC/ICC Dyn Nr := Data Authentication Code issuerAppDataByteBuffer.putShort(dataAuthenticationCode); } else { // DAC/ICC Dyn Nr := ICC Dynamic Number (Terminal)[1 : 2] issuerAppDataByteBuffer.put(iccDynamicNumberTerminal, 0, 2); } // Plaintext Counters for MPP Remote-SE Lite is '00 00 00 00 00 00 00 FF'. issuerAppDataByteBuffer.put(ZEROS, 0, 7); issuerAppDataByteBuffer.put((byte) 0xFF); final int sdadOffset = 256; int sdadLength = -1; // IF 'Combined DDA/AC Generation Requested' in Reference Control Parameter is set if ((cid == PayPConstants.CID_ARQC) && cdaRequested) { try { BigInteger primeP = new BigInteger(1, this.cardProfile.getIccPrivKeyPrimeP()); BigInteger primeQ = new BigInteger(1, this.cardProfile.getIccPrivKeyPrimeQ()); BigInteger primeExponentP = new BigInteger(1, this.cardProfile.getIccPrivKeyPrimeExponentP()); BigInteger primeExponentQ = new BigInteger(1, this.cardProfile.getIccPrivKeyPrimeExponentQ()); BigInteger crtCoefficient = new BigInteger(1, this.cardProfile.getIccPrivKeyCrtCoefficient()); BigInteger modulus = primeP.multiply(primeQ); RSAPrivateCrtKeySpec iccPrivKeySpec = new RSAPrivateCrtKeySpec(modulus, null, null, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient); // Note: Need to use "BC" provider. RSAPrivateCrtKey iccPrivKey = (RSAPrivateCrtKey) KeyFactory.getInstance("RSA", "BC").generatePrivate(iccPrivKeySpec); sdadLength = OfflineDataAuthentication.generateSdad(ptpSuk, apduBuffer, sdadOffset, this.pdolData, cdol1RelatedData, cid, issuerAppData, ac, unpredictableNumber, this.cardProfile.getIccPubKeyModulusLength(), iccPrivKey); } catch (Exception e) { Log.e(LOG_TAG, "ICC Private Key not available."); } if (sdadLength != this.cardProfile.getIccPubKeyModulusLength()) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } apduByteBuffer.rewind(); // Build response. apduByteBuffer.put(PayPConstants.TAG_RESPONSE_MESSAGE_TEMPLATE); // Skip response message template length. apduByteBuffer.put((byte) 0); // Append common data elements in response: // '9F27' [1] Cryptogram Information Data // '9F36' [2] Application Transaction Counter apduByteBuffer.putShort(PayPConstants.TAG_CRYPTOGRAM_INFO_DATA); apduByteBuffer.put((byte) 1); apduByteBuffer.put(cid); apduByteBuffer.putShort(PayPConstants.TAG_APPLICATION_TRANSACTION_COUNTER); apduByteBuffer.put((byte) 2); apduByteBuffer.putShort(ptpSuk.getAtc()); if ((cid == PayPConstants.CID_ARQC) && cdaRequested && (sdadLength == this.cardProfile.getIccPubKeyModulusLength())) { // Data elements in CDA response: // '9F27' [1] Cryptogram Information Data // '9F36' [2] Application Transaction Counter // '9F4B' [Length Of ICC Public Key Modulus] Signed Dynamic Application Data // '9F10' [18] Issuer Application Data // Append CDA data element in response: // '9F4B' [Length Of ICC Public Key Modulus] Signed Dynamic Application Data apduByteBuffer.putShort(PayPConstants.TAG_SIGNED_DYNAMIC_APPLICATION_DATA); if (sdadLength >= 128) { // 2-byte Signed Dynamic Application Data length. apduByteBuffer.put((byte) 0x81); } apduByteBuffer.put((byte) sdadLength); apduByteBuffer.put(apduBuffer, sdadOffset, sdadLength); } else { // Data elements in non-CDA response: // '9F27' [1] Cryptogram Information Data // '9F36' [2] Application Transaction Counter // '9F26' [8] Application Cryptogram // '9F10' [7 or 18 or 26] Issuer Application Data // 'DF4B' [3] POS Cardholder Interaction Information (for AAC only) // Append non-CDA data element in response: // '9F26' [8] Application Cryptogram apduByteBuffer.putShort(PayPConstants.TAG_APPLICATION_CRYPTOGRAM); apduByteBuffer.put((byte) ac.length); apduByteBuffer.put(ac); } // Append more common data element in response: // '9F10' [18] Issuer Application Data apduByteBuffer.putShort(PayPConstants.TAG_ISSUER_APPLICATION_DATA); apduByteBuffer.put((byte) issuerAppData.length); apduByteBuffer.put(issuerAppData); // IF the Application Cryptogram is an AAC if (cid == PayPConstants.CID_AAC) { // Append data element in AAC only response: // 'DF4B' [3] POS Cardholder Interaction Information apduByteBuffer.putShort(PayPConstants.TAG_POS_CARDHOLDER_INTERACTION_INFO); apduByteBuffer.put((byte) this.posCardholderInteractionInfo.length); apduByteBuffer.put(this.posCardholderInteractionInfo); } // Set response template message length. int rdataLength = apduByteBuffer.position(); if (rdataLength < 130) { // 1-byte response template message length. apduByteBuffer.put(1, (byte) (rdataLength - 2)); } else { // 2-byte response template message length. apduByteBuffer.put(1, (byte) 0x81); // Shift response template message data. System.arraycopy(apduBuffer, 2, apduBuffer, 3, rdataLength - 2); apduByteBuffer.put(2, (byte) (rdataLength - 2)); rdataLength++; } this.apduState = APDU_SENDING_LAST; // DEBUG Log.v(LOG_TAG, "R-APDU: " + DataUtil.byteArrayToHexString(apduBuffer, 0, rdataLength) + "9000"); apdu.setOutgoingLength((short) rdataLength); apdu.sendBytes((short) 0, (short) rdataLength); // 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 blockCondition(boolean waitGetCardProfile, boolean waitGetPtpSuk, 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.tGetCardProfile != null) && waitGetCardProfile) || ((this.tGetPtpSuk != null) && waitGetPtpSuk)) { if ((this.tGetCardProfile != null) && waitGetCardProfile) { Log.i(LOG_TAG, caller + ", tGetCardProfile is still accessing remote card applet, waiting..."); } if ((this.tGetPtpSuk != null) && waitGetPtpSuk) { Log.i(LOG_TAG, caller + ", tGetPtpSuk is still accessing remote card applet, waiting..."); } try { Thread.sleep(sleepInterval); } catch (InterruptedException e) { } } } private void getCardProfile() { if (this.tGetCardProfile != null) { Log.i(LOG_TAG, "getCardProfile, tGetCardProfile is still accessing remote card applet."); return; } // Block until 'tGetPtpSuk' thread has stopped before continuing. blockCondition(false, true, 200, "getCardProfile"); // 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.tGetCardProfile = new Thread(new Runnable() { public void run() { try { setBusy(); } catch (IOException e) { Log.e(LOG_TAG, "tGetCardProfile setBusy IOException Log", e); try { postMessage("Card Agent Not Available to\n" + "Get Card Profile\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } tGetCardProfile = null; return; } try { connect(); } catch (IOException e) { Log.e(LOG_TAG, "tGetCardProfile connect IOException Log", e); try { disconnect(); } catch (IOException e1) { } if (getNonNullMessage(e).equalsIgnoreCase("SOCKET_ERR")) { if (connectRetryCounter < MAX_CONNECT_RETRY) { connectRetryCounter++; try { clearBusy(); } catch (IOException e1) { } tGetCardProfile = null; // Retry getCardProfile. getCardProfile(); return; } } try { postMessage("No Connection Available to\n" + "Get Card Profile\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } try { clearBusy(); } catch (IOException e1) { } tGetCardProfile = null; return; } TransceiveData getCardData = new TransceiveData(TransceiveData.SOFT_CHANNEL); getCardData.packCardReset(false); getCardData.packApdu(APDU_SELECT_CARDAPPLET, true); getCardData.packApdu(APDU_GET_MOBILE_KEY, true); getCardData.packApdu(APDU_GET_CARDPROFILE, true); try { transceive(getCardData); } catch (IOException e) { Log.e(LOG_TAG, "tGetCardProfile transceive(getCardData) IOException Log", e); try { disconnect(); } catch (IOException e1) { } try { postMessage("Get Card Profile Error\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } try { clearBusy(); } catch (IOException e1) { } tGetCardProfile = null; return; } byte[] selectResponse = getCardData.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 { disconnect(); } catch (IOException e) { } try { if (((selectSw != null) && (selectSw == ISO7816.SW_FUNC_NOT_SUPPORTED)) || invalidVersion) { terminated = true; disabled = true; cardProfile = null; arrayPtpSuk = null; if (invalidVersion) { postMessage("Incompatible Card Applet", false, null); } else { postMessage("Account is Terminated", false, null); } } else { postMessage("Account Not Available", false, null); } } catch (IOException e) { } try { clearBusy(); } catch (IOException e) { } tGetCardProfile = null; return; } byte[] mobileKey = getCardData.getNextResponse(); if ((mobileKey != null) && (mobileKey.length == 34) && (ByteBuffer.wrap(mobileKey).getShort(mobileKey.length - 2) == ISO7816.SW_NO_ERROR)) { // Extract Mobile Key without SW. mobileKey = Arrays.copyOf(mobileKey, mobileKey.length - 2); } else { Log.e(LOG_TAG, "Invalid mobileKey: " + DataUtil.byteArrayToHexString(mobileKey)); try { disconnect(); } catch (IOException e) { } try { postMessage("Invalid Mobile Key", false, null); } catch (IOException e) { } try { clearBusy(); } catch (IOException e) { } tGetCardProfile = null; return; } // DEBUG //Log.i(LOG_TAG, "mobileKey=" + DataUtil.byteArrayToHexString(mobileKey)); if (!DataCipher.setMobileKey(mobileKey)) { Log.e(LOG_TAG, "Failed to set M_Key."); } byte[] cardProfileData = getCardData.getNextResponse(); if ((cardProfileData != null) && (cardProfileData.length > 2) && (ByteBuffer.wrap(cardProfileData).getShort(cardProfileData.length - 2) == ISO7816.SW_NO_ERROR)) { // Reset 'disabled' in case agent was in disabled state. disabled = false; // Extract Card Profile data without SW. cardProfileData = Arrays.copyOf(cardProfileData, cardProfileData.length - 2); // NOTE: Kludge to check 'FF FF FF FF' in card profile data that indicates Mobile PIN not initialized. if (ByteBuffer.wrap(cardProfileData).getInt() == (int) 0xFFFFFFFF) { // Remove 'FF FF FF FF'. cardProfileData = Arrays.copyOfRange(cardProfileData, 4, cardProfileData.length); } ByteArrayInputStream bis = new ByteArrayInputStream(cardProfileData); ObjectInput in = null; try { in = new ObjectInputStream(bis); cardProfile = (CardProfile) in.readObject(); } catch (Exception e) { Log.e(LOG_TAG, "Cannot serialize cardProfileData: " + DataUtil.byteArrayToHexString(cardProfileData)); try { postMessage("Card Profile 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(cardProfileData); Log.e(LOG_TAG, "Invalid cardProfileData: " + invalidResponse); try { if ((invalidResponse.length() == 4) && invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_COMMAND_NOT_ALLOWED))) { disabled = true; cardProfile = null; arrayPtpSuk = null; postMessage("Account is Disabled", false, null); } else { postMessage("Invalid Card Profile Data", false, null); } } catch (IOException e) { } } if (cardProfile == null) { try { disconnect(); } catch (IOException e) { } try { clearBusy(); } catch (IOException e) { } tGetCardProfile = null; return; } else { // DEBUG try { Log.v(LOG_TAG, "cardProfile Aid: " + DataUtil.byteArrayToHexString(cardProfile.getAid())); Log.v(LOG_TAG, "cardProfile AidPpse: " + DataUtil.byteArrayToHexString(cardProfile.getAidPpse())); Log.v(LOG_TAG, "cardProfile PpseResponse: " + DataUtil.byteArrayToHexString(cardProfile.getPpseResponse())); Log.v(LOG_TAG, "cardProfile TagA5Data: " + DataUtil.byteArrayToHexString(cardProfile.getTagA5Data())); Log.v(LOG_TAG, "cardProfile Aip: " + DataUtil.byteArrayToHexString(cardProfile.getAip())); Log.v(LOG_TAG, "cardProfile Afl: " + DataUtil.byteArrayToHexString(cardProfile.getAfl())); Log.v(LOG_TAG, "cardProfile Sfi1Record1: " + DataUtil.byteArrayToHexString(cardProfile.getSfi1Record1())); Log.v(LOG_TAG, "cardProfile Sfi2Record1: " + DataUtil.byteArrayToHexString(cardProfile.getSfi2Record1())); Log.v(LOG_TAG, "cardProfile Sfi2Record2: " + DataUtil.byteArrayToHexString(cardProfile.getSfi2Record2())); Log.v(LOG_TAG, "cardProfile Sfi2Record3: " + DataUtil.byteArrayToHexString(cardProfile.getSfi2Record3())); Log.v(LOG_TAG, "cardProfile Cdol1RelatedDataLength: " + String.format("%02X", cardProfile.getCdol1RelatedDataLength())); Log.v(LOG_TAG, "cardProfile MchipCvmIssuerOptions: " + String.format("%02X", cardProfile.getMchipCvmIssuerOptions())); Log.v(LOG_TAG, "cardProfile CrmCountryCode: " + String.format("%04X", cardProfile.getCrmCountryCode())); Log.v(LOG_TAG, "cardProfile CiacDeclineOnlineCapable: " + DataUtil.byteArrayToHexString(cardProfile.getCiacDeclineOnlineCapable())); Log.v(LOG_TAG, "cardProfile KeyDerivationIndex: " + String.format("%02X", cardProfile.getKeyDerivationIndex())); Log.v(LOG_TAG, "cardProfile ApplicationControl: " + DataUtil.byteArrayToHexString(cardProfile.getApplicationControl())); Log.v(LOG_TAG, "cardProfile AdditionalCheckTable: " + DataUtil.byteArrayToHexString(cardProfile.getAdditionalCheckTable())); Log.v(LOG_TAG, "cardProfile DualTapResetTimeout: " + String.format("%04X", cardProfile.getDualTapResetTimeout())); //Log.v(LOG_TAG, "cardProfile SecurityWord: " + DataUtil.byteArrayToHexString(cardProfile.getSecurityWord())); Log.v(LOG_TAG, "cardProfile CvmResetTimeout: " + String.format("%04X", cardProfile.getCvmResetTimeout())); Log.v(LOG_TAG, "cardProfile MagstripeCvmIssuerOptions: " + String.format("%02X", cardProfile.getMagstripeCvmIssuerOptions())); Log.v(LOG_TAG, "cardProfile CiacDeclinePpms: " + DataUtil.byteArrayToHexString(cardProfile.getCiacDeclinePpms())); //Log.v(LOG_TAG, "cardProfile PinIvCvc3Track1: " + DataUtil.byteArrayToHexString(cardProfile.getPinIvCvc3Track1())); //Log.v(LOG_TAG, "cardProfile PinIvCvc3Track2: " + DataUtil.byteArrayToHexString(cardProfile.getPinIvCvc3Track2())); Log.v(LOG_TAG, "cardProfile IccPubKeyModulusLength: " + String.format("%02X", cardProfile.getIccPubKeyModulusLength())); //Log.v(LOG_TAG, "cardProfile IccPrivKeyPrimeP: " + DataUtil.byteArrayToHexString(cardProfile.getIccPrivKeyPrimeP())); //Log.v(LOG_TAG, "cardProfile IccPrivKeyPrimeQ: " + DataUtil.byteArrayToHexString(cardProfile.getIccPrivKeyPrimeQ())); //Log.v(LOG_TAG, "cardProfile IccPrivKeyPrimeExponentP: " + DataUtil.byteArrayToHexString(cardProfile.getIccPrivKeyPrimeExponentP())); //Log.v(LOG_TAG, "cardProfile IccPrivKeyPrimeExponentQ: " + DataUtil.byteArrayToHexString(cardProfile.getIccPrivKeyPrimeExponentQ())); //Log.v(LOG_TAG, "cardProfile IccPrivKeyCrtCoefficient: " + DataUtil.byteArrayToHexString(cardProfile.getIccPrivKeyCrtCoefficient())); Log.v(LOG_TAG, "cardProfile MaxNumberPtpSuk: " + cardProfile.getMaxNumberPtpSuk()); Log.v(LOG_TAG, "cardProfile MinThresholdNumberPtpSuk: " + cardProfile.getMinThresholdNumberPtpSuk()); } catch (Exception e) { Log.e(LOG_TAG, "cardProfile Debug Exception Log", e); } } final int maxNumberPtpSuk = cardProfile.getMaxNumberPtpSuk(); arrayPtpSuk = new ArrayDeque<PaymentTokenPayloadSingleUseKey>(maxNumberPtpSuk); final int addNumberPtpSuk = maxNumberPtpSuk - arrayPtpSuk.size(); // DEBUG //Log.i(LOG_TAG, "addNumberPtpSuk=" + addNumberPtpSuk); if (addNumberPtpSuk <= 0) { try { disconnect(); } catch (IOException e) { } try { clearBusy(); } catch (IOException e) { } tGetCardProfile = null; return; } byte[] cardProfileHash = null; try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); cardProfileHash = sha256.digest(cardProfileData); } catch (Exception e) { } TransceiveData tranceiveDataGetPtpSuk = new TransceiveData(TransceiveData.SOFT_CHANNEL); int numberPtpSuk = 0; while (numberPtpSuk < addNumberPtpSuk) { tranceiveDataGetPtpSuk.packApdu(APDU_GET_PTPSUK, true); numberPtpSuk++; } try { transceive(tranceiveDataGetPtpSuk); } catch (IOException e) { Log.e(LOG_TAG, "tGetCardProfile transceive(tranceiveDataGetPtpSuk) IOException Log", e); try { postMessage("Get PTP_SUK Error\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } } try { disconnect(); } catch (IOException e) { } numberPtpSuk = 0; while (numberPtpSuk < addNumberPtpSuk) { syncGetPtpSuk(tranceiveDataGetPtpSuk.getNextResponse(), cardProfileHash); numberPtpSuk++; } try { clearBusy(); } catch (IOException e) { } tGetCardProfile = null; } }); this.tGetCardProfile.start(); } private void getPtpSuk(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, "getPtpSuk not allowed in current agent state."); return; } // Block until 'tGetPtpSuk' thread has stopped before continuing. blockCondition(false, true, 200, "getPtpSuk"); if ((this.cardProfile == null) && !this.disabled) { try { postMessage("Missing Card Data\nPlease Check Connection is Available and Refresh Card", false, null); } catch (IOException e) { } return; } if (checkMinThreshold && (this.arrayPtpSuk.size() > this.cardProfile.getMinThresholdNumberPtpSuk())) { //Log.i(LOG_TAG, "Not yet minimum threshold number of PTP_SUK."); return; } final int addNumberPtpSuk = this.cardProfile.getMaxNumberPtpSuk() - this.arrayPtpSuk.size(); if (addNumberPtpSuk <= 0) { Log.i(LOG_TAG, "Already maximum number of PTP_SUK."); return; } // Block until 'tGetCardProfile' thread has stopped before continuing. blockCondition(true, false, 200, "getPtpSuk"); // NOTE: This thread does not call 'setBusy' method so agent is not blocked from processing // contactless transaction while the thread is running. this.tGetPtpSuk = new Thread(new Runnable() { public void run() { try { connect(); } catch (IOException e) { Log.e(LOG_TAG, "tGetPtpSuk connect IOException Log", e); try { disconnect(); } catch (IOException e1) { } if (getNonNullMessage(e).equalsIgnoreCase("SOCKET_ERR")) { if (connectRetryCounter < MAX_CONNECT_RETRY) { connectRetryCounter++; tGetPtpSuk = null; // Retry getPtpSuk. getPtpSuk(checkMinThreshold); return; } } try { postMessage("No Connection Available to\n" + "Get More PTP_SUK\n" + arrayPtpSuk.size() + " Transactions Remaining\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } tGetPtpSuk = null; return; } TransceiveData tranceiveDataGetPtpSuk = new TransceiveData(TransceiveData.SOFT_CHANNEL); tranceiveDataGetPtpSuk.packCardReset(false); tranceiveDataGetPtpSuk.packApdu(APDU_SELECT_CARDAPPLET, true); int numberPtpSuk = 0; while (numberPtpSuk < addNumberPtpSuk) { tranceiveDataGetPtpSuk.packApdu(APDU_GET_PTPSUK, true); numberPtpSuk++; } try { transceive(tranceiveDataGetPtpSuk); } catch (IOException e) { Log.e(LOG_TAG, "tGetPtpSuk transceive IOException Log", e); // Indicate exception occurred. numberPtpSuk = -1; try { postMessage("Get PTP_SUK Error\n" + "Exception: " + getNonNullMessage(e), false, null); } catch (IOException e1) { } } try { disconnect(); } catch (IOException e) { } // Check if error already occurred. if (numberPtpSuk != -1) { byte[] selectResponse = tranceiveDataGetPtpSuk.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, "Invalid selectResponse: " + invalidResponse); try { if ((invalidResponse.length() == 4) && invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_FUNC_NOT_SUPPORTED))) { terminated = true; disabled = true; cardProfile = null; arrayPtpSuk = null; postMessage("Account is Terminated", false, null); } else { postMessage("Account Not Available", false, null); } } catch (IOException e) { } tGetPtpSuk = null; return; } // Calculate Card Profile hash. byte[] cardProfileHash = null; ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = null; try { out = new ObjectOutputStream(bos); out.writeObject(cardProfile); byte[] cardProfileBytes = bos.toByteArray(); MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); cardProfileHash = sha256.digest(cardProfileBytes); } catch (Exception e) { } finally { try { if (out != null) { out.close(); } } catch (IOException ioe) { } try { bos.close(); } catch (IOException ioe) { } } numberPtpSuk = 0; while (numberPtpSuk < addNumberPtpSuk) { syncGetPtpSuk(tranceiveDataGetPtpSuk.getNextResponse(), cardProfileHash); numberPtpSuk++; } } tGetPtpSuk = null; } }); this.tGetPtpSuk.start(); } private synchronized void syncGetPtpSuk(byte[] ptpSukData, byte[] cardProfileHash) { if ((ptpSukData != null) && (ptpSukData.length > 2) && (ByteBuffer.wrap(ptpSukData).getShort(ptpSukData.length - 2) == ISO7816.SW_NO_ERROR)) { // Extract PTP_SUK data without SW. ptpSukData = Arrays.copyOf(ptpSukData, ptpSukData.length - 2); PaymentTokenPayloadSingleUseKey ptpSuk = null; ByteArrayInputStream bis = new ByteArrayInputStream(ptpSukData); ObjectInput in = null; try { in = new ObjectInputStream(bis); ptpSuk = (PaymentTokenPayloadSingleUseKey) in.readObject(); } catch (Exception e) { Log.e(LOG_TAG, "Cannot serialize ptpSukData: " + DataUtil.byteArrayToHexString(ptpSukData)); try { postMessage("PTP_SUK 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 (ptpSuk != null) { // DEBUG try { //Log.v(LOG_TAG, "PTP_SUK PtpCpTruncatedHash: " + DataUtil.byteArrayToHexString(ptpSuk.getPtpCpTruncatedHash())); Log.v(LOG_TAG, "PTP_SUK Atc: " + String.format("%04X", ptpSuk.getAtc())); //Log.v(LOG_TAG, "PTP_SUK Suk: " + DataUtil.byteArrayToHexString(ptpSuk.getSuk())); //Log.v(LOG_TAG, "PTP_SUK Idn: " + DataUtil.byteArrayToHexString(ptpSuk.getIdn())); } catch (Exception e) { Log.e(LOG_TAG, "ptpSuk Debug Exception Log", e); } try { // Validate truncated Card Profile hash received in PTP_SUK. if (Arrays.equals(Arrays.copyOf(cardProfileHash, 24), ptpSuk.getPtpCpTruncatedHash())) { // Hash matched. arrayPtpSuk.add(ptpSuk); } else { postMessage("Corrupted PTP_SUK Error", false, null); } } catch (Exception e) { Log.e(LOG_TAG, "Add PTP_SUK Exception Log", e); } } } else { String invalidResponse = DataUtil.byteArrayToHexString(ptpSukData); Log.e(LOG_TAG, "Invalid ptpSukData: " + invalidResponse); try { if ((invalidResponse.length() == 4) && invalidResponse.equalsIgnoreCase(String.format("%04X", ISO7816.SW_COMMAND_NOT_ALLOWED))) { disabled = true; cardProfile = null; arrayPtpSuk = null; postMessage("Account is Disabled", false, null); } else { postMessage("Invalid PTP_SUK Data", false, null); } } catch (IOException e) { } } } }