package com.github.jthuraisamy.mastertap; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.nfc.Tag; import android.nfc.tech.IsoDep; import android.os.AsyncTask; import android.util.Log; import com.github.jthuraisamy.mastertap.fragments.RenameCardDialog; import com.github.jthuraisamy.mastertap.helpers.MastercardHelper; import com.github.jthuraisamy.mastertap.helpers.VisaHelper; import com.github.jthuraisamy.mastertap.models.Card; import com.github.jthuraisamy.mastertap.models.CardRecord; import com.google.common.primitives.Bytes; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class CardReaderTask extends AsyncTask<Tag, Integer, String> { private static final String TAG = "MasterTapLog-" + CardReaderTask.class.getSimpleName(); private final Context ctx; private final MainActivity mainActivity; private ProgressDialog progressDialog; private IsoDep tagCommunicator; private Card card; Map<Integer, String> cvc3Map = new HashMap<Integer, String>(); private int taskStatus = 0; private final int STATUS_INVALID_AID = 1; private final int STATUS_TAG_LOST = 2; public CardReaderTask(Context ctx) { this.ctx = ctx; this.mainActivity = (MainActivity) ctx; MainActivity.cardDao.open(); } @Override protected void onPreExecute() { super.onPreExecute(); // Show progress dialog. progressDialog = new ProgressDialog(ctx); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMessage(ctx.getText(R.string.card_reading_progress_message)); progressDialog.setProgressNumberFormat(ctx.getText(R.string.card_read_progress_format).toString()); progressDialog.show(); } @Override protected String doInBackground(Tag... params) { Tag tag = params[0]; tagCommunicator = IsoDep.get(tag); try { tagCommunicator.connect(); tagCommunicator.setTimeout(5000); if (tagCommunicator.isConnected()) { readCard(); } } catch (IOException e) { e.printStackTrace(); } return null; } private void readCard() { try { // SELECT PPSE "2PAY.SYS.DDF01". byte[] paymentDirectory = tagCommunicator.transceive(ApduConstants.SELECT_PPSE); // Get Application Identifier (AID) from response. byte[] aid = TLVParser.readTlv(paymentDirectory, new byte[]{0x4F}); // Select AID. ByteArrayOutputStream selectAidCommand = new ByteArrayOutputStream(); selectAidCommand.write(new byte[] {0x00, (byte) 0xA4, 0x04, 0x00, 0x07}); selectAidCommand.write(aid); selectAidCommand.write(new byte[] {0x00}); byte[] aidFci = tagCommunicator.transceive(selectAidCommand.toByteArray()); Log.i(TAG, "AID = " + Helper.byteToHex(aid)); Log.i(TAG, "PPSE = " + Helper.byteToHex(paymentDirectory)); Log.i(TAG, "AIDFCI = " + Helper.byteToHex(aidFci)); // Reject non-MasterCards. if (Arrays.equals(aid, ApduConstants.SUPPORTED_AID)) { readMasterCard(paymentDirectory, aidFci); } else { taskStatus = STATUS_INVALID_AID; } } catch (IOException e) { taskStatus = STATUS_TAG_LOST; } } private void readVisa(byte[] paymentDirectory, byte[] aidFci) { try { // Construct Get Processing Options (GPO) command. byte[] gpoCommand = VisaHelper.getGpoCommand(aidFci); Log.i(TAG, "GPO Command = " + Helper.byteToHex(gpoCommand)); // Execute GPO command. byte[] gpoResponse = tagCommunicator.transceive(gpoCommand); Log.i(TAG, "GPO Response = " + Helper.byteToHex(gpoResponse)); // Modify Application Interchange Profile (AIP) in GPO response to support MSD. if (gpoResponse[0] == (byte) 0x77) { int aipHeaderIndex = Bytes.indexOf(gpoResponse, new byte[]{(byte) 0x82, 0x02}); gpoResponse[aipHeaderIndex + 3] |= 0x80; } else if (gpoResponse[0] == (byte) 0x80) { gpoResponse[3] |= 0x80; } Log.i(TAG, "Modfied GPO Response = " + Helper.byteToHex(gpoResponse)); // Retrieve records from Application File Locator (AFL) in GPO response. byte[] afl = new byte[] {}; if (gpoResponse[0] == (byte) 0x77) { afl = TLVParser.readTlv(gpoResponse, new byte[]{(byte) 0x94}); } else if (gpoResponse[0] == (byte) 0x80) { afl = Arrays.copyOfRange(gpoResponse, 4, gpoResponse.length - 2); } Log.i(TAG, "AFL = " + Helper.byteToHex(afl)); List<CardRecord> records = VisaHelper.getRecordsFromAfl(tagCommunicator, afl); } catch (IOException e) { taskStatus = STATUS_TAG_LOST; } } private void readMasterCard(byte[] paymentDirectory, byte[] aidFci) { try { // Read Mag Stripe Application Data. byte[] magStripeData = tagCommunicator.transceive(ApduConstants.READ_MAGSTRIPE_RECORDS); byte[] track1Data = TLVParser.readTlv(magStripeData, new byte[]{0x56}); String pan = new String(Arrays.copyOfRange(track1Data, 1, 17)); String expiryDate = new String(Arrays.copyOfRange(track1Data, 45, 49)); Log.i(TAG, Helper.byteToHex(magStripeData)); Log.i(TAG, pan); // Check if card exists in the database. card = MainActivity.cardDao.getCard(pan); // If card does not exist, create it, then retrieve it so it has the correct ID. if (card == null) { card = new Card(ctx); card.setPan(pan); card.setExpiryDate(expiryDate); card.setPaymentDirectory(Helper.byteToHex(paymentDirectory)); card.setAidFci(Helper.byteToHex(aidFci)); card.setMagStripeData(Helper.byteToHex(magStripeData)); MainActivity.cardDao.addCard(card); card = MainActivity.cardDao.getCard(pan); } Log.i(TAG, MainActivity.cardDao.getCard(pan).toString()); // Determine the maximum digits for unpredictable numbers (n_un). cvc3Map = card.getCvc3Map(); int totalUnpredictableNumbers = MastercardHelper.getTotalUNs(magStripeData); // Compute Cryptographic Checksums (ccc) into a map of new CVC3 values. int unpredictableNumber = 0; while (unpredictableNumber < totalUnpredictableNumbers) { if (cvc3Map.containsKey(unpredictableNumber)) { // Update progress. unpredictableNumber++; publishProgress(unpredictableNumber, totalUnpredictableNumbers); continue; } // Calculate BCD (binary coded decimal) 4-byte array for unpredictableNumber. byte[] bcdBytes = Helper.intToBcdArray(unpredictableNumber); // Get Processing Options, return AIP/AFL. byte[] processingOptions = tagCommunicator.transceive(ApduConstants.GET_PROCESSING_OPTIONS); Log.i(TAG, Helper.byteToHex(processingOptions)); // Compute Cryptographic Checksum for unpredictableNumber, returning CVC3/ATC. ByteArrayOutputStream cccCommand = new ByteArrayOutputStream(); cccCommand.write(new byte[]{(byte) 0x80, 0x2A, (byte) 0x8E, (byte) 0x80, 0x04}); cccCommand.write(bcdBytes); cccCommand.write(new byte[]{0x00}); byte[] cvc3Response = tagCommunicator.transceive(cccCommand.toByteArray()); // Add CVC3 response to cvc3Map. cvc3Map.put(unpredictableNumber, Helper.byteToHex(cvc3Response)); // Update progress. unpredictableNumber++; publishProgress(unpredictableNumber, totalUnpredictableNumbers); } } catch (IOException e) { taskStatus = STATUS_TAG_LOST; } } protected void onProgressUpdate(Integer... progress) { progressDialog.setMax(progress[1]); progressDialog.setProgress(progress[0]); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); // Write CVC3 responses to database. MainActivity.cardDao.addCvc3Map(card, cvc3Map); // Dismiss progress dialog. if (progressDialog.isShowing()) progressDialog.dismiss(); switch (taskStatus) { // If the device has lost contact with the card: case STATUS_TAG_LOST: mainActivity.toastMessage(ctx.getString(R.string.contact_lost)); break; // If the card is not a valid MasterCard credit card. case STATUS_INVALID_AID: AlertDialog.Builder alertDialog = new AlertDialog.Builder(ctx); alertDialog.setMessage(ctx.getText(R.string.only_mastercard)); alertDialog.setNeutralButton(R.string.ok, null); alertDialog.create(); alertDialog.show(); return; default: break; } // Provided that the card is a valid MasterCard credit card, if (card != null) { // Prompt rename card dialog if the card still retains the default label. if (card.getLabel().equals(ctx.getString(R.string.default_label))) { if (!mainActivity.isFragmentVisible(RenameCardDialog.TAG)) { RenameCardDialog renameCardDialog = RenameCardDialog.create(card); renameCardDialog.show(mainActivity.getSupportFragmentManager(), "RenameCardDialog"); } // Otherwise, refresh cards and scroll to this card. } else { mainActivity.refreshViewPager(card.getPan()); } } // Close database handles. MainActivity.cardDao.close(); } }