Talk to your Credit Card part 6: read all files given in the AFL list

AndroidCrypto
10 min readApr 12, 2023

--

Now we come to the most interesting part of our journey and read files from the card.

An EMV card contains a file system that is organized in tracks and records. Thinking of a traditional hard disk the tracks are like tree rings; track 1 is starting as the innermost ring, followed by track 2… up the outermost ring. The record is part or peace of the “ring” or “track” (think of a pizza cut in 8 segments). To address a single record we need two informations — the number of the track and the record. In an EMV file system the track is named “Short File Indicator” (“SFI”). As the name Application File Locator suggests our AFL does contain this information but it is a little bit tricky to get the information.

Let’s start with the general structure of the AFL list, our data are from the MasterCard response in step 5:

94 0C -- Application File Locator (AFL)
08 01 01 00 10 01 01 01 20 01 02 00 (BINARY)

The AFL is a list of entries and each entry is of length 4 bytes, so I’m simply splitting the complete value in 4 bytes long chunks:

94 0C -- Application File Locator (AFL)
08 01 01 00 10 01 01 01 20 01 02 00
08 01 01 00 = chunk 1
10 01 01 01 = chunk 2
20 01 02 00 = chunk 3

Each chunk is 4 bytes long and for the next parsing step I’ using chunk 3:

20 01 02 00 = chunk 3
20 = SFI = Short File Indicator
01 = first record to read
02 = last record to read
00 = number of records included in offline transactions

That means that we are asked to read 2 records (record 1 and record 2) from SFI 20. The last information “number of records included in offline transactions” can skipped out as we do not need this information for our journey.

Now we loop through the AFL list — it is a double loop with an “outer loop” taking each chunk and an “inner loop” reading the number of records.

In this article I’m not showing the contents of all files read but just the relevant ones.

According to our AFL list we need to read 4 files in total:

08010100 = 1 file
10010101 = 1 file
20010200 = 2 files

I’m only showing the data for 2 files. Serious security warning: This step may reveal confidential data like the credit card number ! The card number shown in my example is from a card cancelled long time ago and is no longer valid.

for SFI 8 we read 1 record
readRecord command length: 5 data: 00b2010c00
readRecord response length: 121 data: 70759f6c0200019f6206000000000f009f63060000000000fe563442353337353035303030303136303131305e202f5e323430333232313237393433323930303030303030303030303030303030309f6401029f65020f009f660200fe9f6b135375050000160110d24032210000000000000f9f6701029000
------------------------------------
70 75 -- Record Template (EMV Proprietary)
9F 6C 02 -- Mag Stripe Application Version Number (Card)
00 01 (BINARY)
9F 62 06 -- Track 1 bit map for CVC3
00 00 00 00 0F 00 (BINARY)
9F 63 06 -- Track 1 bit map for UN and ATC
00 00 00 00 00 FE (BINARY)
56 34 -- Track 1 Data
42 35 33 37 35 30 35 30 30 30 30 31 36 30 31 31
30 5E 20 2F 5E 32 34 30 33 32 32 31 32 37 39 34
33 32 39 30 30 30 30 30 30 30 30 30 30 30 30 30
30 30 30 30 (BINARY)
9F 64 01 -- Track 1 number of ATC digits
02 (BINARY)
9F 65 02 -- Track 2 bit map for CVC3
0F 00 (BINARY)
9F 66 02 -- Terminal Transaction Qualifiers
00 FE (BINARY)
9F 6B 13 -- Track 2 Data
53 75 05 00 00 16 01 10 D2 40 32 21 00 00 00 00
00 00 0F (BINARY)
9F 67 01 -- Track 2 number of ATC digits
02 (BINARY)
90 00 -- Command successfully executed (OK)
------------------------------------

There are 2 tags of interest for our journey:

  • tag 0x56 Track 1 Data: It is as well an old relic from the magnetic stripe area and does contain the credit card number and expiration date in cleartext — do you see it ? A little hint: the data is in ASCII encoding, so the value “35 33 37 35 30 35 30 30 30 30 31 36 30 31 31 30” reads as “5375 0500…”
  • tag 0x9F6B Track 2 Data: the data is equals to tag 0x57 shown in the previous step 5 and reveals the credit card number and expiration date as well
for SFI 10 we read 1 record
readRecord command length: 5 data: 00b2011400
readRecord response length: 171 data: 7081a69f420209785f25032203015f24032403315a0853750500001601105f3401009f0702ffc09f080200028c279f02069f03069f1a0295055f2a029a039c019f37049f35019f45029f4c089f34039f21039f7c148d0c910a8a0295059f37049f4c088e0e000000000000000042031e031f039f0d05b4508400009f0e0500000000009f0f05b4708480005f280202809f4a018257135375050000160110d24032212794329000000f9000
------------------------------------
70 81 A6 -- Record Template (EMV Proprietary)
9F 42 02 -- Application Currency Code
09 78 (NUMERIC)
5F 25 03 -- Application Effective Date
22 03 01 (NUMERIC)
5F 24 03 -- Application Expiration Date
24 03 31 (NUMERIC)
5A 08 -- Application Primary Account Number (PAN)
53 75 05 00 00 16 01 10 (NUMERIC)
5F 34 01 -- Application Primary Account Number (PAN) Sequence Number
00 (NUMERIC)
9F 07 02 -- Application Usage Control
FF C0 (BINARY)
9F 08 02 -- Application Version Number - card
00 02 (BINARY)
8C 27 -- Card Risk Management Data Object List 1 (CDOL1)
9F 02 06 -- Amount, Authorised (Numeric)
9F 03 06 -- Amount, Other (Numeric)
9F 1A 02 -- Terminal Country Code
95 05 -- Terminal Verification Results (TVR)
5F 2A 02 -- Transaction Currency Code
9A 03 -- Transaction Date
9C 01 -- Transaction Type
9F 37 04 -- Unpredictable Number
9F 35 01 -- Terminal Type
9F 45 02 -- Data Authentication Code
9F 4C 08 -- ICC Dynamic Number
9F 34 03 -- Cardholder Verification (CVM) Results
9F 21 03 -- Transaction Time (HHMMSS)
9F 7C 14 -- Merchant Custom Data
8D 0C -- Card Risk Management Data Object List 2 (CDOL2)
91 0a -- Issuer Authentication Data
8A 02 -- Authorisation Response Code
95 05 -- Terminal Verification Results (TVR)
9F 37 04 -- Unpredictable Number
9F 4C 08 -- ICC Dynamic Number
8E 0E -- Cardholder Verification Method (CVM) List
00 00 00 00 00 00 00 00 42 03 1E 03 1F 03 (BINARY)
9F 0D 05 -- Issuer Action Code - Default
B4 50 84 00 00 (BINARY)
9F 0E 05 -- Issuer Action Code - Denial
00 00 00 00 00 (BINARY)
9F 0F 05 -- Issuer Action Code - Online
B4 70 84 80 00 (BINARY)
5F 28 02 -- Issuer Country Code
02 80 (NUMERIC)
9F 4A 01 -- Static Data Authentication Tag List
82 (BINARY)
57 13 -- Track 2 Equivalent Data
53 75 05 00 00 16 01 10 D2 40 32 21 27 94 32 90
00 00 0F (BINARY)
90 00 -- Command successfully executed (OK)
------------------------------------

There are two tags of interest:

  • tag 0x5A Application Primary Account Number (PAN). Please note that the PAN = credit card number could be padded with trailing “F”s
  • tag 0x5F24 Application Expiration Date

This is the source code added for step 6 in MainActivity.java class:

public void onTagDiscovered(Tag tag) {
...
// parse content of gpoResponse to get Track 2 or AFL

/**
* We do have 3 scenarios to work with:
* a) the response contains a Track 2 Equivalent Data tag (tag 0x57)
* b) the response is of type 'Response Message Template Format 1' (tag 0x80)
* c) the response is of type 'Response Message Template Format 2' (tag 0x77)
*/
//System.out.println("*** gpoResponse ***");
//System.out.println(prettyPrintDataToString(gpoRequestResponse));
BerTlvs tlvsGpo = parser.parse(gpoRequestResponse);
byte[] aflBytes = null;

/**
* workflow a)
* The response contains a Track 2 Equivalent Data tag and from this we can directly
* retrieve the Primary Application Number (PAN, here the Credit Card Number)
* found using a VisaCard
*/

BerTlv tag57 = tlvsGpo.find(new BerTag(0x57));
if (tag57 != null) {
//writeToUiAppend("");
writeToUiAppend("workflow a)");
writeToUiAppend("");
printStepHeader(6, "read files & search PAN");
writeToUiAppend("06 read the files from card skipped");
writeToUiAppend(etData, "06 read the files from card skipped");

writeToUiAppend("the response contains a Track 2 Equivalent Data tag [tag 0x57]");
byte[] gpoResponseTag57 = tag57.getBytesValue();
writeToUiAppend("found tag 0x57 in the gpoResponse length: " + gpoResponseTag57.length + " data: " + bytesToHexNpe(gpoResponseTag57));
String pan = getPanFromTrack2EquivalentData(gpoResponseTag57);
String expDate = getExpirationDateFromTrack2EquivalentData(gpoResponseTag57);
writeToUiAppend("found a PAN " + pan + " with Expiration date: " + expDate);
writeToUiAppend("");
printStepHeader(7, "print PAN & expire date");
writeToUiAppend("07 get PAN and Expiration date from tag 0x57 (Track 2 Equivalent Data)");
writeToUiAppend(etData, "07 get PAN and Expiration date from tag 0x57 (Track 2 Equivalent Data) completed");
writeToUiAppend("data for AID " + bytesToHexNpe(aidSelected));
writeToUiAppend("PAN: " + pan);
String expirationDateString = "Expiration date (" + (expDate.length() == 4 ? "YYMM): " : "YYMMDD): ") + expDate;
writeToUiAppend(expirationDateString);
writeToUiAppend(etData, "data for AID " + bytesToHexNpe(aidSelected));
writeToUiAppend(etData,"PAN: " + pan);
writeToUiAppend(etData, expirationDateString);
writeToUiAppend("");
}

/**
* workflow b)
* The response is of type 'Response Message Template Format 1' and we need to know
* the meaning of each byte, so we need to parse the content to get the data for the
* 'Application File Locator' (AFL).
* found using a American Express Card
*/

BerTlv tag80 = tlvsGpo.find(new BerTag(0x80));
if (tag80 != null) {
//writeToUiAppend("");
writeToUiAppend("workflow b)");
writeToUiAppend("the response is of type 'Response Message Template Format 1' [tag 0x80]");
byte[] gpoResponseTag80 = tag80.getBytesValue();
writeToUiAppend("found tag 0x80 in the gpoResponse length: " + gpoResponseTag80.length + " data: " + bytesToHexNpe(gpoResponseTag80));
aflBytes = Arrays.copyOfRange(gpoResponseTag80, 2, gpoResponseTag80.length);
}


/**
* workflow c)
* The response is of type 'Response Message Template Format 2' and we need to find
* tag 0x94; the content is the 'Application File Locator' (AFL)
* found using a MasterCard
*/

BerTlv tag77 = tlvsGpo.find(new BerTag(0x77));
if (tag77 != null) {
//writeToUiAppend("");
writeToUiAppend("workflow c)");
writeToUiAppend("the response is of type 'Response Message Template Format 2' [tag 0x77]");
writeToUiAppend("found tag 0x77 in the gpoResponse");
}
BerTlv tag94 = tlvsGpo.find(new BerTag(0x94));
if (tag94 != null) {
writeToUiAppend("found 'AFL' [tag 0x94] in the response of type 'Response Message Template Format 2' [tag 0x77]");
byte[] gpoResponseTag94 = tag94.getBytesValue();
writeToUiAppend("found tag 0x94 in the gpoResponse length: " + gpoResponseTag94.length + " data: " + bytesToHexNpe(gpoResponseTag94));
aflBytes = gpoResponseTag94;
}

writeToUiAppend("");
printStepHeader(6, "read files & search PAN");
writeToUiAppend("06 read the files from card and search for PAN & Expiration date");
writeToUiAppend(etData, "06 read the files from card and search for PAN & Expiration date");

List<byte[]> tag94BytesList = divideArray(aflBytes, 4);
int tag94BytesListLength = tag94BytesList.size();
//writeToUiAppend(etLog, "tag94Bytes divided into " + tag94BytesListLength + " arrays");
writeToUiAppend("");
writeToUiAppend("The AFL contains " + tag94BytesListLength + (tag94BytesListLength == 1 ? " entry to read" : " entries to read"));

// the AFL is a 4 byte long byte array, so I your aflBytes array is 12 bytes long there are three sets to read.

/**
* now we are going to read the specified files from the card. The system is as follows:
* The first byte is the SFI, the second byte the first record to read,
* the third byte is the last record to read and byte 4 gives the number
* of sectors involved in offline authorization.
* Here an example: 10 01 03 00
* SFI: 10
* first record: 01
* last record: 03
* offline: 00
* means that we are asked to read 3 records (number 1, 2 and 3) from SFI 10
*
* The fourth byte codes the number of records involved in offline data
* authentication starting with the record number coded in the second byte. The
* fourth byte may range from zero to the value of the third byte less the value of
* the second byte plus 1.
*/

for (int i = 0; i < tag94BytesListLength; i++) {
byte[] tag94BytesListEntry = tag94BytesList.get(i);
byte sfiOrg = tag94BytesListEntry[0];
byte rec1 = tag94BytesListEntry[1];
byte recL = tag94BytesListEntry[2];
byte offl = tag94BytesListEntry[3]; // offline authorization
int sfiNew = (byte) sfiOrg | 0x04; // add 4 = set bit 3
int numberOfRecordsToRead = (byteToInt(recL) - byteToInt(rec1) + 1);
writeToUiAppend("for SFI " + byteToHex(sfiOrg) + " we read " + numberOfRecordsToRead + (numberOfRecordsToRead == 1 ? " record" : " records"));
// read records
byte[] readRecordResponse = new byte[0];
for (int iRecord = (int) rec1; iRecord <= (int) recL; iRecord++) {
byte[] cmd = hexToBytes("00B2000400");
cmd[2] = (byte) (iRecord & 0x0FF);
cmd[3] |= (byte) (sfiNew & 0x0FF);
writeToUiAppend("readRecord command length: " + cmd.length + " data: " + bytesToHexNpe(cmd));
readRecordResponse = nfc.transceive(cmd);
byte[] readRecordResponseTag5a = null;
byte[] readRecordResponseTag5f24 = null;
if (readRecordResponse != null) {
writeToUiAppend("readRecord response length: " + readRecordResponse.length + " data: " + bytesToHexNpe(readRecordResponse));
writeToUiAppend(prettyPrintDataToString(readRecordResponse));
System.out.println("readRecord response length: " + readRecordResponse.length + " data: " + bytesToHexNpe(readRecordResponse));
System.out.println(prettyPrintDataToString(readRecordResponse));

// checking for PAN and Expiration Date
try {
BerTlvs tlvsReadRecord = parser.parse(readRecordResponse);
BerTlv tag5a = tlvsReadRecord.find(new BerTag(0x5a));
if (tag5a != null) {
readRecordResponseTag5a = tag5a.getBytesValue();
writeToUiAppend("found tag 0x5a in the readRecordResponse length: " + readRecordResponseTag5a.length + " data: " + bytesToHexNpe(readRecordResponseTag5a));
}
BerTlv tag5f24 = tlvsReadRecord.find(new BerTag(0x5f, 0x24));
if (tag5f24 != null) {
readRecordResponseTag5f24 = tag5f24.getBytesValue();
writeToUiAppend("found tag 0x5f24 in the readRecordResponse length: " + readRecordResponseTag5f24.length + " data: " + bytesToHexNpe(readRecordResponseTag5f24));
}
if (readRecordResponseTag5a != null) {
String readRecordPanString = removeTrailingF(bytesToHexNpe(readRecordResponseTag5a));
String readRecordExpirationDateString = bytesToHexNpe(readRecordResponseTag5f24);
writeToUiAppend("");
printStepHeader(7, "print PAN & expire date");
writeToUiAppend("07 get PAN and Expiration date from tags 0x5a and 0x5f24");
writeToUiAppend(etData, "07 get PAN and Expiration date from tags 0x5a and 0x5f24 completed");
writeToUiAppend("data for AID " + bytesToHexNpe(aidSelected));
writeToUiAppend("PAN: " + readRecordPanString);
String expirationDateString = "Expiration date (" + (readRecordExpirationDateString.length() == 4 ? "YYMM): " : "YYMMDD): ") + readRecordExpirationDateString;
writeToUiAppend(expirationDateString);
writeToUiAppend(etData, "data for AID " + bytesToHexNpe(aidSelected));
writeToUiAppend(etData,"PAN: " + readRecordPanString);
writeToUiAppend(etData, expirationDateString);
writeToUiAppend("");
}
} catch (RuntimeException e) {
System.out.println("Runtime Exception: " + e.getMessage());
//startEndSequence(nfc);
}
} else {
writeToUiAppend("readRecord response was NULL");
}
}
}

We are come to the end of our journey and go to step 07 find and print out the “Application Primary Account Number” (“PAN”) = card number and “Application Expiration Date” = expiration date of the card

Find the full code of the app in my GitHub repository TalkToYourCreditCardPart6: TalkToYourCreditCardPart6

--

--