DEV Community

Léo Leplat
Léo Leplat

Posted on • Updated on

Create a C/C++ app with the ACR122U (Linux)

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, you have to run the following command:

echo "blacklist pn533_usb" | sudo tee -a /etc/modprobe.d/blacklist-libnfc.conf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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... 
Enter fullscreen mode Exit fullscreen mode

A solution is to restart the pcscd service:

sudo systemctl restart pcscd
Enter fullscreen mode Exit fullscreen mode

If it still doesn't work, you can run the following command:

systemctl status pcscd
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 gave SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1 as the fourth argument of the SCardConnect function. You can specify which protocol to use by giving only SCARD_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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

Discussion (0)