Magnetic cards have been around for a while. It feels only recently in the past few years that point of sale systems in the USA primarily switched to NFC and chipped cards for payments. But magnetic cards still have lots of usages, such as gift cards or door entry cards. While cleaning up, I found a magnetic card reader that was used for a project a long time ago. I happen to find this at a time where I’m already writing about NFC cards, just wrote about the Luhn’s Check algorithm used in credit card entry, and looking at how to read smart cards. I think it’s fitting to take a glance at reading information from magnetic cards. There are multiple standards for encoding information onto magnetic cards.
The physical unit I found was purchased for part of the functionality that was used in a demo for a concept in a past NRF Conference. I still have some video from the event.
This particular reader appears as an HID device to the computer. When a card is scanned, it generates keystrokes to the computer. For demonstration, I am using a gift card from “Raising Cane’s.” I’ve selected this card because I believe the card is cancelled and thus has no funds on it. But even if it does have funds on it and someone uses it, I suffer no lost since I am not positioned to suffer a lost. I also found an old American Express gift card whose funds have long been exhausted.
Encoding
The data on track 1 of the cards is encoded in 7 bits; 6 bits are data, 1 bit is for parity. This means that only 64 possible characters could be in the encoding. This isn’t UTF-8 or ASCII like you may be acustomed to. The reader does translate from these encoding to the equivalent keystrokes. Data on tracks 2 and 3 (if they exists) have 5 bits per character. Track 1 can have up to 79 characters, track 2 up to 40, and track 3 up to 107 characters.
I’ve got a tables at the end of this post that shows the actual encodings and their classifications. The reader will translate these to ASCII encodings for you. These are primarily included as a curiosity. There is more than one standard for encoding data on magnetic cards. With some of the hotel key cards that I have, I found some of them don’t work with this reader. This is somewhat expected, as a hotel might want to use a format that isn’t as easy to replicate. The varied gift cards, membership cards, and payment cards I tried generally worked fine.
Error Response
I want to start off talking about the error response since people often overlook those. If you were to use a card that the reader cannot process, it will return the string +E?
. If you receive this response, the reading failed, possibly because the card is using an encoding that isn’t understood.
Sample Card Scan
Here’s the data that comes back when I read the “Raising Cane’s” gift card. I will refer to it in the following sections.
%B6000205500145033524^GIFT/RAISINGCANES^4211?;6000205500145033524=4211101685485?
Start and End Sentinel
When a card is read, the first character in the data will be a %
. The last character will be a ?
. If you were making something that were reading card data, you could use these characters to let your program know that it is receiving card data or that the data. Fields on the card are separated with the ^
. Though not part of the card data, when my specific card reader has finished reading data, it will also send an enter keystroke. All of the data between the %
and ?
comes from the card. But there are some more delimiters in that block of data.
Track Delimiters and Field Delimiters
A magnetic card could have multiple tracks in parallel. The card reader returns all three tracks in a single stream of data. But the tracks are delimited with a semicolon(;
). A single track could have multiple fields of data. Fields are separated with the caret (^
) character on track 1 or an equal (=
) character on tracks 2 and 3. Parsing out the sample card read I provided above, we end up with the following. The exact purpose of each of these fields could vary by card.
Track 1 Field 1:B6000205500145033524
Track 1 Field 2:GIFT/RAISINGCANES
Track 1 Field 3:4211?
Track 2 Field 1:6000205500145033524
Track 2 Field 2:4211101685485
Card Class
The very first character read on the card indicates the class/type of card being read. For all of the payment cards (both gift cards and credit cards) that I’ve encountered, this character is a ‘B’. From my readings and reading other cards that I have, I have found the following.
Prefix | Association |
B | Payment or Gift Card |
G | Gift Card |
M | Membership Card |
GC | Gift Card |
Credit Card Format
While I found the way data is structured on a card to be variable, when I tried several credit cards, the construction of the data is consistent. Here is a modified data stream from a gift card.
%B377936453080000^THANK YOU ^2806521190729520 ?;377936453080000=280652119072952000000?
Breaking it out into fields, we have the following.
Field | Purpose | Data |
Track 1 Field 1 | Primary Account Number | 377936453080000 |
Track 1 Field 2 | Name | THANK YOU |
Track 2 Field 3 | Expiration Date (2028-06) Service code (521) Discretionary Data(190729520) | 2806521190729520 |
Track 2 Field 1 | Credit Card Number | 377936453080000 |
Track 2 Field 2 | Expiration Date (2028-06) Service code (521) Discretionary Data (190729520) | 280652119072952000000 |
You’ll notice that some of the data is on the card twice. I’m not quite sure of the reason for this. Is that for verifying the integrity of the data? To provide an alternative method of reading data for cheaper devices, allowing them to only read one track? This, I don’t know.
Reading the Data in Code
As I said the post on Luhn’s Check, don’t enter a real credit card number on the site where I have sample code posted. Though the site doesn’t actually communicate any data back to any site, I think it is still better to advise you not to enter data. But the site is there for you to examine the code. Feel free to download it to run in a local sandbox (where you can prohibit Internet communication) or view the source code to see how it works. You can find the code at https://j2inet.github.io/apps/magreader. If you would like to use a magnetic card reader that is similar to what I have, you can find it here (affiliate link):
Encoding Tables
BCD Data Format
Character | Hex | Function |
0 | 0x00 | DATA |
1 | 0x01 | DATA |
2 | 0x02 | DATA |
3 | 0x03 | DATA |
4 | 0x04 | DATA |
5 | 0x05 | DATA |
6 | 0x06 | DATA |
7 | 0x07 | DATA |
8 | 0x08 | DATA |
9 | 0x09 | DATA |
: | 0x0A | Control |
; | 0x0B | Start Sentinel |
< | 0x0C | Control |
= | 0x0D | Field Separator |
> | 0x0E | Control |
? | 0x0F | End Sentinel |
Alpha Encoded Data
Character | Hex | Function |
[space] | 0x00 | Special |
! | 0x01 | Special |
“ | 0x02 | Special |
# | 0x03 | Special |
$ | 0x04 | Special |
% | 0x05 | Start Sentinel |
& | 0x06 | Special |
‘ | 0x07 | Special |
( | 0x08 | Special |
) | 0x09 | Special |
* | 0x0A | Special |
+ | 0x0B | Special |
‘ | 0x0C | Special |
– | 0x0D | Special |
. | 0x0E | Special |
/ | 0x0F | Special |
0 | 0x10 | DATA |
1 | 0x11 | DATA |
2 | 0x12 | DATA |
3 | 0x13 | DATA |
4 | 0x14 | DATA |
5 | 0x15 | DATA |
6 | 0x16 | DATA |
7 | 0x17 | DATA |
8 | 0x18 | DATA |
9 | 0x19 | DATA |
: | 0x1A | Special |
; | 0x1B | Special |
< | 0x1C | Special |
= | 0x1D | Special |
> | 0x1E | Special |
? | 0x1F | End Sentinel |
@ | 0x20 | Special |
A | 0x21 | DATA |
B | 0x22 | DATA |
C | 0x23 | DATA |
D | 0x24 | DATA |
E | 0x25 | DATA |
F | 0x26 | DATA |
G | 0x27 | DATA |
H | 0x28 | DATA |
I | 0x29 | DATA |
J | 0x2A | DATA |
K | 0x2B | DATA |
L | 0x2C | DATA |
M | 0x2D | DATA |
N | 0x2E | DATA |
O | 0x2F | DATA |
P | 0x30 | DATA |
Q | 0x31 | DATA |
R | 0x32 | DATA |
S | 0x33 | DATA |
T | 0x3f | DATA |
U | 0x35 | DATA |
V | 0x36 | DATA |
W | 0x37 | DATA |
X | 0x38 | DATA |
Y | 0x39 | DATA |
Z | 0x3A | DATA |
[ | 0x3B | Special |
\ | 0x3C | Special |
] | 0x3D | Special |
^ | 0x3E | Field Separator |
_ | 0x3F | Special |