package com.github.jthuraisamy.mastertap;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import com.github.jthuraisamy.mastertap.models.Card;
import java.util.Arrays;
public class PaymentService extends HostApduService implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String TAG = "MasterTapLog-" + PaymentService.class.getSimpleName();
private Card card;
private String inboundApduDescription;
private boolean transactionInProgress;
private Vibrator vibrator;
@Override
public void onCreate() {
super.onCreate();
// Obtain instance of system vibrator.
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(250);
// Register the OnSharedPreferenceChangeListener.
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
settings.registerOnSharedPreferenceChangeListener(this);
Log.i(TAG, "PaymentService initialized.");
}
@Override
public void onSharedPreferenceChanged(SharedPreferences settings, String key) {
if (key.equals("paymentCardIndex")) {
int paymentCardIndex = settings.getInt(key, 0);
card = MainActivity.cards.get(paymentCardIndex);
Log.i(TAG, "Payment card changed to: " + card.getPan());
}
}
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
// Return error response if no card is selected.
if (card.getPan() == null) return ApduConstants.UNKNOWN_ERROR_RESPONSE;
byte[] responseApdu;
if (Arrays.equals(commandApdu, ApduConstants.SELECT_PPSE)) {
transactionInProgress = true;
inboundApduDescription = "Selecting PPSE…";
responseApdu = Helper.hexToByte(card.getPaymentDirectory());
} else if (Arrays.equals(commandApdu, ApduConstants.SELECT_AID)) {
inboundApduDescription = "Selecting AID…";
responseApdu = Helper.hexToByte(card.getAidFci());
} else if (Arrays.equals(commandApdu, ApduConstants.READ_MAGSTRIPE_RECORDS)) {
inboundApduDescription = "Reading MagStripe records…";
responseApdu = Helper.hexToByte(card.getMagStripeData());
} else if (isGpoCommand(commandApdu)) {
inboundApduDescription = "Getting processing options…";
responseApdu = ApduConstants.GET_PROCESSING_OPTIONS_RESPONSE;
} else if (isCccCommand(commandApdu)) {
inboundApduDescription = "Computing cryptographic checksum…";
int unpredictableNumber = getUN(commandApdu);
boolean hasUN = card.hasUN(unpredictableNumber);
boolean isAttemptedUN = card.getAttemptedUNs().contains(unpredictableNumber);
Log.i(TAG, "UN: " + String.valueOf(unpredictableNumber));
// Return CVC3 values if there is an unattempted response for the given UN.
if (hasUN && !isAttemptedUN) {
responseApdu = Helper.hexToByte(card.getCvc3Map().get(unpredictableNumber));
MainActivity.cardDao.attemptUN(card, unpredictableNumber);
vibrator.vibrate(500);
} else {
responseApdu = ApduConstants.UNKNOWN_ERROR_RESPONSE;
}
} else {
transactionInProgress = false;
inboundApduDescription = "Received Unknown APDU";
responseApdu = ApduConstants.UNKNOWN_ERROR_RESPONSE;
}
Log.i(TAG, "ID: " + inboundApduDescription);
Log.i(TAG, "Rx: " + Helper.byteToHex(commandApdu));
Log.i(TAG, "Tx: " + Helper.byteToHex(responseApdu));
sendApduBroadcast();
return responseApdu;
}
/**
* Return true if the given command APDU is a "Get Processing Options" command.
*
* @param commandApdu byte[]
* @return boolean
*/
private boolean isGpoCommand(byte[] commandApdu) {
return (
commandApdu.length > 4 &&
commandApdu[0] == ApduConstants.GET_PROCESSING_OPTIONS[0] &&
commandApdu[1] == ApduConstants.GET_PROCESSING_OPTIONS[1] &&
commandApdu[2] == ApduConstants.GET_PROCESSING_OPTIONS[2] &&
commandApdu[3] == ApduConstants.GET_PROCESSING_OPTIONS[3]
);
}
/**
* Return true if the given command APDU is a "Compute Cryptographic Checksum" command.
*
* @param commandApdu byte[]
* @return boolean
*/
private boolean isCccCommand(byte[] commandApdu) {
return (
commandApdu.length > 4 &&
commandApdu[0] == ApduConstants.COMPUTE_CRYPTOGRAPHIC_CHECKSUM[0] &&
commandApdu[1] == ApduConstants.COMPUTE_CRYPTOGRAPHIC_CHECKSUM[1] &&
commandApdu[2] == ApduConstants.COMPUTE_CRYPTOGRAPHIC_CHECKSUM[2] &&
commandApdu[3] == ApduConstants.COMPUTE_CRYPTOGRAPHIC_CHECKSUM[3]
);
}
/**
* Send an Intent to the MainActivity with transaction details.
*/
private void sendApduBroadcast() {
Intent intent = new Intent("apduProcessing");
intent.putExtra("transactionInProgress", transactionInProgress);
intent.putExtra("inboundApduDescription", inboundApduDescription);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
/**
* Return the unpredictable number from the given "Compute Cryptographic Checksum" command.
*
* @param commandApdu byte[]
* @return int
*/
private int getUN(byte[] commandApdu) {
byte[] commandTag = new byte[] {(byte) 0x80, 0x2A, (byte) 0x8E, (byte) 0x80};
byte[] bcdArray = TLVParser.readTlv(commandApdu, commandTag);
return Helper.bcdArrayToInt(bcdArray);
}
@Override
public void onDeactivated(int reason) {}
}