NFC fun with Python
I’ve recently been looking into using an NFC reader/writer pad at work to make configuring some devices we have much quicker and easier, as the standard process for these devices is an Android phone and a lot of manual data entry. Checking the device using a mobile is great, but configuring them this way is not scalable.
I did some research and found we could get an NFC pad and write some Python code using nfcpy to automate much of the labour, reducing the time taken and decreasing the likelihood of human error. Some quick searches on eBay showed that the ACS ACR122U was a reasonably cost-effective option, so I ordered a couple of units for testing.1
When they arrived, I unpacked one and powered it up — so far, so good. Linux seems to have a basic driver for it that makes it beep when you bring a card near (at least that’s all I could get it to do). It’s also a fairly powerful reader — mine was able to communicate with test tags up to 50mm away, including through 25mm particleboard.
The next step is getting the reader working with Python.
Using the reader with nfcpy
At this point, I immediately encountered problems. The most recent version of nfcpy does not support Python 3. In particular, there a number of places where it treats
bytes types the same, which is no longer the case. My options were to move back to Python 2, port the module to Python 3 or abandon the idea entirely — I decided to try and port the library to Python 3. I’ve spent a fair amount of time on it so it can work for my device; feel free to grab the code from my fork, but be aware it might not work for you out-of-the-box.
Part way through this process, I also discovered that nfcpy has this to say of the ACR122U:
It can not be overstated that the ACR122U is not a good choice for nfcpy.
So, yeah. The lesson for me here is do (better) research before I commit to a plan. I hope you’ll learn from my mistake.
Linux device access issues
While porting the code, I also hit upon the issue of the device being locked out, so Python couldn’t use it. Some research led me to running
python -m nfc, which helpfully reports that it’s a kernel module causing the problems:
** found usb:072f:2200 at usb:001:008 but it's already used -- scan sysfs entry at '/sys/bus/usb/devices/1-1:1.0/' -- the device is used by the 'pn533_usb' kernel driver -- this kernel driver belongs to the linux nfc subsystem -- you can remove it to free the device for this session sudo modprobe -r pn533_usb -- and blacklist the driver to prevent loading next time sudo sh -c 'echo blacklist pn533_usb >> /etc/modprobe.d/blacklist-nfc.conf'
As indicated, a temporary fix is running
sudo modprobe -r pn533_usb; or for a permanent solution blacklist the module:
$ sudo sh -c 'echo blacklist pn533_usb >> /etc/modprobe.d/blacklist-nfc.conf'
Wireshark USB analysis
Part way through migrating the module to Python 3, I started encountering issues where the
nfc-poll command on Linux would work fine, but Python was indicating that the NFC chip wasn’t responding with enough data. This seemed a little bit suspect to me, so I decided to crack out Wireshark to do some USB sniffing and find out where the differences were.
Wireshark can capture data from USB devices using the various
usbmon adapters. Unfortunately the permissions on these are by default pretty locked down on Linux, so some searching led me to this helpful SuperUser post. The temporary hack to fix this involves granting everyone read and write access to the file:
sudo chmod o=rw /dev/usbmon1.
Tip: If you’re missing
/dev/usbmon* files, chances are you haven’t loaded the kernel module — run
sudo modprobe usbmon. Following the instructions below will take care of this for you automatically from this point onwards.
A more robust solution is to create a
usbmon group and tell the
udev daemon to grant appropriate access. These instructions add you to the newly-created group; you’ll need to logout for the group changes to take effect (or you can try your luck with these instructions).
$ sudo addgroup usbmon $ sudo usermod -a -G usbmon $USER $ echo 'SUBSYSTEM=="usbmon", MODE="640", GROUP="usbmon"' | sudo tee /etc/udev/rules.d/99-usbmon.rules $ echo 'SUBSYSTEM=="usb", RUN+="/sbin/modprobe usbmon"' | sudo tee /etc/udev/rules.d/99-usbmon.rules $ sudo udevadm trigger
Opening a USB capture
You might have a number of
usbmon devices that Wireshark can attach to. To work out which one you need, run
lsusb and look for your device. The Bus number (
001) means that we will use
Bus 001 Device 020: ID 072f:2200 Advanced Card Systems, Ltd ACR122U
Depending on what other devices are on the bus you’re capturing on, you could be flooded by traffic pretty quickly. You can reduce what is shown using a display filter of
usbccid (to jump straight to the display filter box in Wireshark, press Ctrl+/).
Dissectors and filtering
Wireshark should now be capturing USB traffic and looking for CCID packets to display. Chances are, there might not be anything there right now. We can get some messages to appear in Wireshark by running the
nfc-poll command in a terminal; it will look for your NFC card reader and interrogate it. To get some informative output, put an NFC card on the top of the reader before you run the command. Here’s output from when I ran it:
nfc-poll uses libnfc 1.7.1 NFC reader: ACS / ACR122U PICC Interface opened NFC device will poll during 30000 ms (20 pollings of 300 ms for 5 modulations) ISO/IEC 14443A (106 kbps) target: ATQA (SENS_RES): 00 04 UID (NFCID1): fc 09 2e 06 SAK (SEL_RES): 08 nfc_initiator_target_is_present: Target Released Waiting for card removing...done.
Once Wireshark has captured some USB traffic, you can take a look through it to try and spot any issues. Unfortunately, it doesn’t tell us much about the content of the messages by default. We can tell Wireshark that we know the messages are from an ACR122U device by changing the decoding.
If you’ve got rows with
USBCCID in the Protocol column, just right click one of them and select ‘Decode As…’ in the context menu. A dialog will open with a table with one row. Change the
(none) in the ‘Current’ column to
ACR 122, then click OK:
On the other hand, if you don’t have any
USBCCID rows we can still get the same result. Just right click one of the USB rows and pick ‘Decode As…’ in the context menu. In the
USB product row, change the
(none) in the ‘Current’ column to
USBCCID. Then click the plus button and pick
USB CCID Payload in the dropdown in the first column. Then you’ll be able to select
ACR 122 like above:
Wireshark will now be able to work out exactly what commands are being sent to the NFC reader and chipset — you can check the ‘Protocol’ column in the packet list. I’d recommend taking a couple of minutes to click through the packet list and get an idea of what’s going on.
Fixing the bug
After capturing known-good traffic from
nfc-poll and comparing it with the output from Python, I could immediately see where the messages diverged:
The Python packet wasn’t being constructed properly. Some clever hunches with the debugger and breakpoints revealed the source of the error: my attempts at migrating the Python 2 code to Python 3. The original source includes this line:
# brty is type int, initiator_data is type bytearray data = chr(1) + chr(brty) + initiator_data
Which is no longer valid Python (you can’t concatenate
bytearray objects). I’d migrated it to look like this:
data = bytes(1) + bytes(brty) + initiator_data
Once I realised there was a problem here, I decided to actually read the documentation, which told me exactly what I should have known all along:
In addition to the literal forms, bytes objects can be created in a number of other ways:
- A zero-filled bytes object of a specified length:
- From an iterable of integers:
So where I was trying to create a
bytes object of length 1 containing the value
brty, I was instead creating a
brty elements long with each element set to zero (the same bug is present with
bytes(1)). The easy fix is to make the arguments into iterables: lists or sets. The correct code now looks like this:
data = bytes([1, brty]) + initiator_data
Success, right? Not really. The Python 3 port is still incomplete at time of writing and there’s a number of fixes to be made, but progress is definitely underway and it looks like this device is capable of doing what I need it to (but not much more). Once I have the code working for my intended application, I might buy a more capable reader for some further experimentation.
Next week I hope to finish the project for work and provide some code for you that demonstrates how to read and write smartcards.
Image sourced from Advanced Card Systems ↩