Léo Leplat
Posted on August 27, 2021
The ACR122U NFC Reader is a PC-linked contactless smart card reader/writer. In this tutorial, we are going to see how to create a simple C/C++ application running on Linux that communicates with the ACR122U. This application will be used to send and retrieve data from the MIFARE Ultralight and MIFARE Classic 1K tags.
To follow this tutorial, you will need:
- An ACR122U.
- A contactless smart card following the ISO/IEC 14443 Type A specification (like the MIFARE Ultralight and MIFARE Classic 1K tags).
- A Linux distribution (this tutorial was tested on Fedora and Ubuntu but it should be working on any other distro).
- Some basic knowledge in C.
1. Installing the driver
The first step is to install the driver of the ACR122U. This step is needed for both developing and running the application we are going to create.
Let's get to the official website, on the Downloads section.
Two downloads are available for Linux:
- PC/SC Driver Package, which contains packages for different distributions (Debian, Fedora, Ubuntu, Raspbian...). This is the easiest way to install the driver if you are using one of these distributions.
- PC/SC Drivers, which contains the sources of the driver. Here, it's a bit more complex because we have to build the driver before installing it, but it can work on any Linux distribution.
I will show how to build and install the PC/SC Drivers. First, download and uncompress the sources:
wget https://www.acs.com.hk/download-driver-unified/12030/ACS-Unified-Driver-Lnx-Mac-118-P.zip
unzip ACS-Unified-Driver-Lnx-Mac-118-P.zip
cd ACS-Unified-Driver-Lnx-Mac-118-P/
A README
file indicates what is required to build the sources:
- pcsclite 1.8.3 or above
- libusb 1.0.9 or above
- flex
- perl
- pkg-config
In addition, you will need to install these two other packages:
- pcsclite-devel
- libusb-devel
So you should install all of these packages first. On Fedora and Ubuntu, the command would be:
# Fedora
sudo dnf install pcsc-lite libusb flex perl pkg-config pcsc-lite-devel libusb-devel
# Ubuntu
sudo apt-get install pcscd libpcsclite1 libusb-1.0-0 flex perl pkg-config libpcsclite-dev libusb-1.0-0-dev
Then we can uncompress, configure, build, and install the sources:
tar -xf acsccid-1.1.8.tar.bz2
cd acsccid-1.1.8/
./configure
./configure --disable-dependency-tracking # for Ubuntu
make
sudo make install
Two more steps are needed. First, you need to make sure that the pcscd
service is running:
sudo systemctl start pcscd
sudo systemctl enable pcscd
Then, you have to run the following command:
echo "blacklist pn533_usb" | sudo tee -a /etc/modprobe.d/blacklist-libnfc.conf
This is used to avoid conflicts between the driver and one of the default kernel modules.
You can now reboot your computer.
If you are using Yocto, I created a recipe available here.
2. Installing the SDK
The next step is to install the SDK, which is required for developing the application but not for running it.
We will not use the SDK proposed on the official website. It is not open-source and only available for the i386 and x86_64 CPU architecture, which means that it is not possible to use it on Raspberry Pi for example (with their ARM CPU architecture).
Instead, we are going to use PCSC lite. You will need the systemd-devel
package, as well as the libudev-dev
package for Ubuntu:
# Fedora
sudo dnf install systemd-devel
# Ubuntu
sudo apt-get install libsystemd-dev libudev-dev
Then you can download, uncompress, configure, build, and install the sources:
wget https://pcsclite.apdu.fr/files/pcsc-lite-1.9.3.tar.bz2
tar -xf pcsc-lite-1.9.3.tar.bz2
cd pcsc-lite-1.9.3/
./configure
make
sudo make install
3. Check that the ACR122U is detected by your computer (optional)
Before starting to create the application, it's better to make sure your computer recognizes the reader: plug in the reader, install the pcsc-tools
package, and run the pscs_scan
command.
If your reader is not detected, the following message will appear:
Waiting for the first reader...
A solution is to restart the pcscd
service:
sudo systemctl restart pcscd
If it still doesn't work, you can run the following command:
systemctl status pcscd
which may give you the reason of why the reader is not detected.
4. Creating the application
Let's now create the application. I will use the C language with the gcc compiler, but it's also compatible in C++ with g++. To communicate with the reader, we are going to use the PC/SC Lite API (WinSCard). All the code is written is a main.c
file which is available on GitHub.
In this tutorial, we will be using the MIFARE Ultralight and MIFARE Classic 1K tags.
First, let's check that we can include the WinSCard library:
#include <winscard.h>
int main() {
return 0;
}
The header file of the WinSCard library is in /usr/local/include/PCSC
. Therefore, the program can be compiled and run with:
gcc main.c -lpcsclite -I/usr/local/include/PCSC
./a.out
Now, we will use the documentation of the WinSCard API to communicate with the reader. The first step is to establish a context (which has to be released at the end of the application):
#include <stdio.h>
#include <stdlib.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
void establishContext() {
SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &applicationContext);
}
void releaseContext() {
SCardReleaseContext(applicationContext);
}
int main() {
establishContext();
releaseContext();
return 0;
}
We use a global variable applicationContext
because we will need it for future functions. This code does not check whether the operations succeeded or not. Therefore, we add:
#include <stdio.h>
#include <stdlib.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
void establishContext() {
LONG status = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &applicationContext);
if (status == SCARD_S_SUCCESS) {
printf("Context established\n");
} else {
printf("Establish context error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
void releaseContext() {
LONG status = SCardReleaseContext(applicationContext);
if (status == SCARD_S_SUCCESS) {
printf("Context released\n");
} else {
printf("Release context error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
int main() {
establishContext();
releaseContext();
return 0;
}
This will give us some information on the success of each operation, and exit the program as soon as a failure occures.
Then, we have to list the available readers:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
void establishContext() {}
void releaseContext() {}
void listReaders() {
DWORD readers = SCARD_AUTOALLOCATE;
LONG status = SCardListReaders(applicationContext, NULL, (LPSTR)&reader, &readers);
if (status == SCARD_S_SUCCESS) {
char *p = reader;
while (*p) {
printf("Reader found: %s\n", p);
p += strlen(p) +1;
}
} else {
printf("List reader error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
void freeListReader() {
LONG status = SCardFreeMemory(applicationContext, reader);
if (status == SCARD_S_SUCCESS) {
printf("Reader list free\n");
} else {
printf("Free reader list error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
int main() {
establishContext();
listReaders();
freeListReader();
releaseContext();
return 0;
}
We add a global variable reader
which will be used to identify the reader for further use. The function SCardListReaders
allocates some memory for reader
, which has to be freed with the SCardFreeMemory
function before the end of the program. If you get an error here even so the reader is plugged in, be sure to check that the ACR122U is detected by your computer.
Note that if the SCardListReaders
function fails, the program exits directly without releasing the context. I kept this behavior to make this tutorial as simple as possible, but this is not something you want to do with real applications.
The next step is to establish a connection with a tag. Put a tag on the reader (for example a MIFARE Ultralight), and run the following code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
SCARDHANDLE connectionHandler;
DWORD activeProtocol;
void establishContext() {}
void releaseContext() {}
void listReaders() {}
void freeListReader() {}
void connectToCard() {
activeProtocol = -1;
LONG status = SCardConnect(applicationContext, reader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &connectionHandler, &activeProtocol);
if (status == SCARD_S_SUCCESS) {
printf("Connected to card\n");
} else {
printf("Card connection error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
void disconnectFromCard() {
LONG status = SCardDisconnect(connectionHandler, SCARD_LEAVE_CARD);
if (status == SCARD_S_SUCCESS) {
printf("Disconnected from card\n");
} else {
printf("Card deconnection error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
int main() {
establishContext();
listReaders();
connectToCard();
disconnectFromCard();
freeListReader();
releaseContext();
return 0;
}
We have here two more global variables:
-
connectionHandler
: used to communicate with the tag. -
activeProtocol
: it is the established protocol to this connection. We need to keep track of the protocol because we let WinSCard choose which protocol to use, as we gaveSCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1
as the fourth argument of theSCardConnect
function. You can specify which protocol to use by giving onlySCARD_PROTOCOL_T0
for example. The protocol will be required when we want to send a custom command to the the tag.
Like before, the connection must be closed before the end of the program.
Now that we have a connection to the tag, we can get some information from it, like its ATR. The ATR can be used to identify the type of card. A complete list is available here.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
SCARDHANDLE connectionHandler;
DWORD activeProtocol;
void establishContext() {}
void releaseContext() {}
void listReaders() {}
void freeListReader() {}
void connectToCard() {}
void disconnectFromCard() {}
void getCardInformation() {
BYTE ATR[MAX_ATR_SIZE] = "";
DWORD ATRLength = sizeof(ATR);
char readerName[MAX_READERNAME] = "";
DWORD readerLength = sizeof(readerName);
DWORD readerState;
DWORD readerProtocol;
LONG status = SCardStatus(connectionHandler, readerName, &readerLength, &readerState, &readerProtocol, ATR, &ATRLength);
if (status == SCARD_S_SUCCESS) {
printf("\n");
printf("Name of the reader: %s\n", readerName);
printf("ATR: ");
for (int i=0; i<ATRLength; i++) {
printf("%02X ", ATR[i]);
}
printf("\n\n");
} else {
printf("Get card information error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
int main() {
establishContext();
listReaders();
connectToCard();
getCardInformation();
disconnectFromCard();
freeListReader();
releaseContext();
return 0;
}
Finally, we can send a custom command to the reader. Going back to the ACR122U webpage, you can download the API Driver Manual of ACR122U NFC Contactless Smart Card Reader. This document presents a list of commands we can send to the reader. For example, let's retrieve the firmware version of the reader (on page 24). The associated command is FFh 00h 48h 00h 00h
.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
SCARDHANDLE connectionHandler;
DWORD activeProtocol;
void establishContext() {}
void releaseContext() {}
void listReaders() {}
void freeListReader() {}
void connectToCard() {}
void getCardInformation() {}
void sendCommand(uint8_t command[], unsigned short commandLength) {
const SCARD_IO_REQUEST *pioSendPci;
SCARD_IO_REQUEST pioRecvPci;
uint8_t response[300];
unsigned long responseLength = sizeof(response);
switch(activeProtocol) {
case SCARD_PROTOCOL_T0:
pioSendPci = SCARD_PCI_T0;
break;
case SCARD_PROTOCOL_T1:
pioSendPci = SCARD_PCI_T1;
break;
default:
printf("Protocol not found\n");
exit(1);
}
LONG status = SCardTransmit(connectionHandler, pioSendPci, command, commandLength, &pioRecvPci, response, &responseLength);
if (status == SCARD_S_SUCCESS) {
printf("Command sent: \n");
for (int i=0; i<commandLength; i++) {
printf("%02X ", command[i]);
}
printf("\nResponse: \n");
for (int i=0; i<responseLength; i++) {
printf("%02X ", response[i]);
}
printf("\n\n");
} else {
printf("Send command error: %s\n", pcsc_stringify_error(status));
exit(1);
}
}
int main() {
establishContext();
listReaders();
connectToCard();
getCardInformation();
printf("Firmware command:\n");
uint8_t firmwareCommand[] = { 0xFF, 0x00, 0x48, 0x00, 0x00 };
unsigned short firmwareCommandLength = sizeof(firmwareCommand);
sendCommand(firmwareCommand, firmwareCommandLength);
disconnectFromCard();
freeListReader();
releaseContext();
return 0;
}
The switch
statement is needed here because the established protocol to the connection will be determined on runtime. If we had only used SCARD_PROTOCOL_T0
in the SCardConnect
function, we could have directly written pioSendPci = SCARD_PCI_T0;
instead of the switch statement.
We have now a running application that can be used to send custom commands to the reader and the tag. We will apply this to send and retrieve data from the MIFARE Ultralight and the MIFARE Classic 1k. Let's start with the MIFARE Ultralight.
The MIFARE Ultralight is composed of 16 pages with 4 bytes each. The user memory starts from the fifth page and the data is unprotected (we can access without authentication). You can find more information on the data sheet of the tag.
On page 16 and 17 of the API Driver Manual of ACR122U, we can see the read and write commands. Let's write a function that read 16 bytes from the fifth page (page 04h
) and write 4 bytes to the fifth page:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
SCARDHANDLE connectionHandler;
DWORD activeProtocol;
void establishContext() {}
void releaseContext() {}
void listReaders() {}
void freeListReader() {}
void connectToCard() {}
void disconnectFromCard() {}
void getCardInformation() {}
void sendCommand(uint8_t command[], unsigned short commandLength) {}
void mifareUltralight() {
printf("### MIFARE Ultralight ###\n");
uint8_t pageNumber = 0x04;
// Read 4 blocks (16 bytes) starting from pageNumber
uint8_t readCommand[] = { 0xFF, 0xB0, 0x00, pageNumber, 0x10 };
unsigned short readCommandLength = sizeof(readCommand);
sendCommand(readCommand, readCommandLength);
// Write 1 block (4 bytes) to pageNumber
uint8_t data[] = { 0x00, 0x01, 0x02, 0x03 };
uint8_t writeCommand[9] = { 0xFF, 0xD6, 0x00, pageNumber, 0x04 };
unsigned short writeCommandLength = sizeof(writeCommand);
for (int i=0; i<4; i++) {
writeCommand[i+5] = data[i];
}
sendCommand(writeCommand, writeCommandLength);
}
int main() {
establishContext();
listReaders();
connectToCard();
getCardInformation();
printf("Firmware command:\n");
uint8_t firmwareCommand[] = { 0xFF, 0x00, 0x48, 0x00, 0x00 };
unsigned short firmwareCommandLength = sizeof(firmwareCommand);
sendCommand(firmwareCommand, firmwareCommandLength);
mifareUltralight();
disconnectFromCard();
freeListReader();
releaseContext();
return 0;
}
Finally, let's do the same with the MIFARE Classic 1k. This tag consists of 16 sectors of 4 blocks, each block containing 16 bytes. The data is protected by a 6-bytes key, the default key being FFh FFh FFh FFh FFh FFh
. You can find more information on the data sheet of the tag.
With the MIFARE Classic 1k, we need do authenticate before accessing a sector, which is indicated on page 12 and 13 of the API Driver Manual of ACR122U.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <winscard.h>
SCARDCONTEXT applicationContext;
LPSTR reader = NULL;
SCARDHANDLE connectionHandler;
DWORD activeProtocol;
void establishContext() {}
void releaseContext() {}
void listReaders() {}
void freeListReader() {}
void connectToCard() {}
void disconnectFromCard() {}
void getCardInformation() {}
void sendCommand(uint8_t command[], unsigned short commandLength) {}
void mifareUltralight() {}
void mifareClassic() {
printf("### MIFARE Classic ###\n");
uint8_t blockNumber = 0x04;
// Load Authentication Keys
uint8_t key[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
uint8_t authenticationKeysCommand[11] = { 0xFF, 0x82, 0x00, 0x00, 0x06 };
unsigned short authenticationKeysCommandLength = sizeof(authenticationKeysCommand);
for (int i=0; i<6; i++) {
authenticationKeysCommand[i+5] = key[i];
}
sendCommand(authenticationKeysCommand, authenticationKeysCommandLength);
// Authenticate
uint8_t authenticateCommand[] = { 0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, blockNumber, 0x60, 0x00 };
unsigned short authenticateCommandLength = sizeof(authenticateCommand);
sendCommand(authenticateCommand, authenticateCommandLength);
// Read 1 block (16 bytes) at blockNumber
uint8_t readCommand[] = { 0xFF, 0xB0, 0x00, blockNumber, 0x10 };
unsigned short readCommandLength = sizeof(readCommand);
sendCommand(readCommand, readCommandLength);
// Write 1 block (16 bytes) at blockNumber
uint8_t data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
uint8_t writeCommand[21] = { 0xFF, 0xD6, 0x00, blockNumber, 0x10 };
unsigned short writeCommandLength = sizeof(writeCommand);
for (int i=0; i<16; i++) {
writeCommand[i+5] = data[i];
}
sendCommand(writeCommand, writeCommandLength);
}
int main() {
establishContext();
listReaders();
connectToCard();
getCardInformation();
printf("Firmware command:\n");
uint8_t firmwareCommand[] = { 0xFF, 0x00, 0x48, 0x00, 0x00 };
unsigned short firmwareCommandLength = sizeof(firmwareCommand);
sendCommand(firmwareCommand, firmwareCommandLength);
//mifareUltralight();
mifareClassic();
disconnectFromCard();
freeListReader();
releaseContext();
return 0;
}
5. Conclusion
You know now how to send and retrieve data from the MIFARE Ultralight and Classic 1k with the ACR122U. This should be enough for most of the applications. Otherwise, you can take a look at:
- The PC/SC Lite API (WinSCard) documentation.
- The API Driver Manual of ACR122U NFC Contactless Smart Card Reader.
- The data sheets of your own tags.
Posted on August 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.