In the previous (and first) post on Lassie Codes I mentioned that I’d be checking out NFC to add an RFID option for stickin’ emergency contact info on your helmet. And here we are. It’s not exactly hard, but I definitely found a few ways that don’t work before I got a tag to actually prompt a call to my wife.
I was surprised
that I could not find a simple working description of tag memory for a telephone number.
(Really, top n
hits on the search machine are filled with “TODO” placeholders when it gets to
concrete examples, like everyone started to document this but got distracted part way through.) The official
NDEF spec also costs $100, and given NFC is part of the “smart” future we’re all trying to build I have a very
strong negative reaction to putting the spec itself behind a paywall.1
So after piecing together blogs, other SDK docs, and some reverse engineering, here’s going from knowing and having nothing through writing bytes that actually trigger a call.
Hardware
I bought a roll of 50 of the cheapest ntag213 stickers, which cost about $23. I also got the Waveshare PN532 hat for a Raspberry Pi 4 so I could write a service that pulls orders and writes the tags (so far it only writes tags, no greater service around it yet). It uses the SPI interface in this setup, but there’s equivalent support for I2C and UART.
Software
The software I wrote for this is so far an unpolished Python module. It makes use of the library provided by Waveshare2 to handle the lowest level protocol details and ndeflib3 for formatting NDEF records.
The confusing bit to me is that – even though ndeflib and similar libraries have the functionality to write NDEF records – the records in that format aren’t useful by themselves. They have to be nested in a NDEF message, just one of a handful of TLV structures recognized in a tag.4 Why don’t these libs include the full TLV structure? I might be missing something. So in my module as it exists now, a couple custom classes wrap the records in the necessary TLV structure when dumping the bytes to be written.
Actual Bytes - Memory Dump
NTAG memory is organized in blocks of 4 bytes (or pages, AFAICT the terms are equivalent for NTAG2xx).
All reads and writes must be done in exactly 1 full block. So in these snippets, the chunks of memory
detailed are block by block. The format documented will be n : 0xaa 0xbb 0xcc 0xdd
, where n
is the block number
and 0xaa-0xdd
are the actual hex values. Specific
bytes will be referenced like 1p3b
, indicating byte 3 on page 1 (both 0-indexed). This example is of
NTAG213, which mostly matches NTAG215 and NTAG216, but there are some differences, mostly around aspects like
the number of pages and available memory.
Tag Serial Number
So! Here goes. The first two blocks are read-only and contain preset bytes for this tag’s serial number, along with
check bytes. 0p3b
is the first check byte. So this tag’s serial number is 0x0436668A816B80
.
0 : 04 36 66 DC
1 : 8A 81 6B 80
Lock Bytes
In block 2, the first byte is the second check byte of the serial number. 2p2b
and 2p3b
are static lock
bytes, where individual bits correspond to blocks (or ranges of blocks), and when set to 1 those blocks are
permanently read-only (cannot undo).
Side note, I naively started just writing blocks at first, to see if my iPhone would read them, and was really confused why my writes would start failing. I was writing to these last two bytes and inadvertently locking pages.
2p1b
is “internal”, also read-only and set at the factory. I actually cannot figure out what it’s
for, though, anyone help me out?
2 : E0 48 00 00
Capability Container
Third block is the capability container, which stores data like the memory capacity and info about.
the tag. These bytes are kind of writable, but it’s the OR
of the current value is what gets
written, so the writing options are limited; no bit can go back from a logical 1.
3 : E1 10 12 00
Data Pages - Lock Control TLV
Pages 0x04-0x27
are actual data, so here starts the first TLV. 4p0b
= 0x01
, which is the lock
control type TLV. Next byte is the length of 3.
4p2b
starts the value and gets a little complex for the lock control type, but it’s composed of 2
nibbles: 0xa
num of pages with a 0x0
byte offset. 4p3b
is the size of lock area, in bits.
4 : 01 03 A0 0C
Data Pages - Message TLV: the Phone Record
Byte 5p0b
is also interpreted as two nibbles of lock config: 0x3
bytes locked per bit and 0x4
bytes/page.
At the next byte, 5p1b
, we finally get into the phone record. The 0x03
in 5p1b
is the message type TLV,
0x0F
for a payload length of 15. In 5p3b
we have the record type within the message (0x01
for well-known)
and two flags: 0x80
for first record of the message, 0x40
for also being the final record in the message.
And 0x01 | 0x80 | 0x40 = 0xD1
.
At the start of page 6 we have 0x01
for a well-known URI (again, this much match the type defined in the
message header at 5b3p
). The 6p1b
spot is a length of 0x0B
, or 11. The UFI type is 0x55
, which
is a constant denoting a URI record. The final byte of page 6 is 0x05
, which means we’re using URI prefix
tel:
, a telephone number (this byte doesn’t have to be used, if set to 0x00
it just means the entire
record is written, no additional prefixing. It’s a helper to set prefixes without needing to write every byte,
saving a little space).
From here, most of the bytes are very straight forward! If you pull up an ASCII chart, you’ll see the next 10 bytes
are 1234567890
, which is the (not actually valid) phone number programmed into this tag.
Finally, byte 9p2b
is a special record type that serves as the terminator. 0xFE
means there’s no more, all done.
5 : 34 03 0F D1
6 : 01 0B 55 05
7 : 31 32 33 34
8 : 35 36 37 38
9 : 39 30 FE 00
Configuration Pages - Dynamic Password Protection
The value in 41p3b
is the page at which dynamic locking starts, and since we don’t even have 255 pages of
data, 0xFF
is effectively no lock. But if we were locking, page 43 is the password.
It is always echo’d as 0x00
s, even when set. The same is true for 44p0b
and 44p1b
, which are the password
ack bytes.
So for example, if 44p3b
were set to 0x04
instead of 0xFF
, the tag wouldn’t allow any of the data pages
to be rewritten without first providing the password defined in page 43.5 The first bit of 42p0b
can be set to
logical 1 to also guard read access.
[... bunch of zeros removed in data ... ]
40 : 00 00 00 BD
41 : 04 00 00 FF
42 : 00 05 00 00
43 : 00 00 00 00
44 : 00 00 00 00
And voila, there you have it. All in one shot for reference:
0 : 04 36 66 DC
1 : 8A 81 6B 80
2 : E0 48 00 00
3 : E1 10 12 00
4 : 01 03 A0 0C
5 : 34 03 0F D1
6 : 01 0B 55 05
7 : 31 32 33 34
8 : 35 36 37 38
9 : 39 30 FE 00
[... removed data pages of 0x00s ... ]
40 : 00 00 00 BD
41 : 04 00 00 FF
42 : 00 05 00 00
43 : 00 00 00 00
44 : 00 00 00 00
Footnotes
-
NXP’s reference is hugely helpful and does a wonderful job explaining the function and use of each page of the tag. But that does not include the format or interoperability of the data section, for which they defer to the official $100 NDEF spec. So as good as the manufacturer’s spec is, it does not alleviate my gripe over the mystery and “proprietariness” of the knowledge necessary to actually use a tag in a way that’s universally supported. https://www.nxp.com/docs/en/data-sheet/NTAG213_215_216.pdf ↩
-
Available on the Waveshare wiki. https://www.waveshare.com/wiki/PN532_NFC_HAT#Demo_codes ↩
-
From PyPi. https://pypi.org/project/ndeflib/ ↩
-
The docs from NordicSemi got me past a couple hurdles figuring out how the records really needed to be bundled. This and reverse engineering working tags was what ultimately got me a working tag. https://developer.nordicsemi.com/nRF_Connect_SDK_dev/doc/PR-4304/nrfxlib/nfc/doc/type_2_tag.html#data ↩
-
A four byte password doesn’t live up to any common security recommendations, and isn’t really meant to. Brute-forcing can prevented by setting an limit on the number of attempts in the last 3 bits of
42p3b
, but if the data is truly sensitive you’ll need to manage encryption of the data itself. ↩
Leave a Comment