An overview of the TLV and NDEF formats used to encode NFC data on Mifare Classic cards
Overview
This post's intended audience is those who are familiar with low-level data structures and the basics of NFC and Mifare Classic technologies.
Data are encoded in an NFC-formatted Mifare Classic tag's memory using the TLV (Type-Length-Value) and NDEF (NFC Data Exchange Format) formats. An Adafruit article provides a good overview of NDEF. My overview is below.
Eventually, I will write a Javascript widget that allows one to encode arbitrary data as NDEF and TLV blocks ready to be written to a Mifare Classic card (or the memory of an emulator), but that's out of the scope of this post.
Type-Length-Value
TLV, defined in NXP application note
AN1304
is simple: a TLV stream is composed of blocks,
each of which has a 1-byte type, a 1- or 3-byte length, and arbitrary-length
value.
For an NDEF message, the type is 0x03
.
The length field is the length of the value field.
(For example, a value field of 0x12 34 56
has a length field of 0x03
.)
For lengths of 0x00
to 0xFE
, length is encoded as one byte.
For lengths of 0xFF
to 0xFFFE
, length is encoded as three bytes:
0xFF
followed by the length as two bytes (big-endian).
The value field is data in the NDEF format.
Other TLV values include 0x00
, which is a 1-byte null block with no length or
value field;
0xFD
, which is a proprietary data block;
and 0xFE
, which is a terminator with no length or value field.
All TLV streams must be terminated with an 0xFE
byte.
NDEF (NFC Data Exchange Format)
NDEF is defined in the NFC Forum document titled NFC Data Exchange Format Technical Specification or abbreviated as [NFCForum-TS-NDEF_1.0]. An NDEF message is composed of records.
Record header
A record is composed of a header, as follows:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Byte | ||||||||
0 | Message Begin | Message End | Chunk | Short | ID present | Type Name Format | ||
1 | Type Length | |||||||
2- | Rest of Header |
The common values for TNF are 0x00
for an empty payload, 0x01
for an
NFC Forum well-known type, and 0x02
for a MIME type.
If the ID Present bit above is set, the next byte is the length of the ID field. Following that is the payload length: 4 bytes (big-endian) if the Short bit is set, 1 byte otherwise. The payload length field does not include the length of the header. Following that are the type, ID (if ID Present is set and the ID Length field is nonzero), and payload.
The Message Begin and End fields indicate whether this record is the first or last of the message, respectively. The Chunk field is set if the payload is chunked (split across multiple records) and the payload is continued beyond this record1.
Text records
The text record type is defined in the NFC Forum document titled
Text Record Type Definition Technical Specification
or abbreviated as [NFCForum-TS-RTD_Text_1.0].
It has a TNF of 0x01
and a type of 0x54
(ASCII T
).
Its payload contains a 1-byte header, a variable length
IETF BCP 47 language tag,
and then the stored text.
The header's MSB is set if the text is encoded in UTF-162 and cleared if the text is encoded in UTF-8; bits 5-0 of the MSB are used to encode (as an unsigned integer) the length of the language tag.
URI records
The URI record type is defined in the NFC Forum document titled
URI Record Type Definition Technical Specification
or abbreviated as [NFCForum-TS-RTD_URI_1.0].
It has a TNF of 0x01
and a type of 0x55
(ASCII U
).
Its payload contains a 1-byte prefix3 followed by the rest of the URL.
The prefix is decoded as follows:
Prefix byte (hex) | Prefix |
---|---|
0x00 |
Empty |
0x01 |
http://www. |
0x02 |
https://www. |
0x03 |
http:// |
0x04 |
https:// |
0x05 |
tel: |
0x06 |
mailto: |
0x07 |
ftp://anonymous:anonymous@ |
0x08 |
ftp://ftp. |
0x09 |
ftps:// |
0x0a |
sftp:// |
0x0b |
smb:// |
0x0c |
nfs:// |
0x0d |
ftp:// |
0x0e |
dav:// |
0x0f |
news: |
0x10 |
telnet:// |
0x11 |
imap: |
0x12 |
rtsp:// |
0x13 |
urn: |
0x14 |
pop: |
0x15 |
sip: |
0x16 |
sips: |
0x17 |
tftp: |
0x18 |
btspp:// |
0x19 |
btl2cap:// |
0x1a |
btgoep:// |
0x1b |
tcpobex:// |
0x1c |
irdaobex:// |
0x1d |
file:// |
0x1e |
urn:epc:id: |
0x1f |
urn:epc:tag: |
0x20 |
urn:epc:pat: |
0x21 |
urn:epc:raw: |
0x22 |
urn:epc: |
0x23 |
urn:nfc: |
0x24 to 0xFF |
Reserved |
Contact records
The NFC read/write app on my phone
(NFC Tools, which is
fairly useful for NFC debugging)
supports a "Contact" record type,
encoded using an NDEF record with TLV of 0x02
(MIME type) and a type field of
text/vcard
.
As indicated by the MIME type,
the payload is encoded using the vCard format
(Wikipedia article,
RFC 6350).
Other records
Among others, the Smart Poster and (Digital) Signature record types are defined in NFC Forum standards [NFCForum-SmartPoster_RTD_1.0] and [RTD_SIG] , respectively; the latter also has a Wikipedia article
Example
Consider a card with sectors
1-----+-----+-------------------------------------------------+-----------------
2 sec | blk | data | ascii
3-----+-----+-------------------------------------------------+-----------------
4 0 | 0 | E6 84 87 F3 16 88 04 00 46 8E 45 55 4D 70 41 04 | ........F.EUMpA.
5 | 1 | 73 00 03 E1 03 E1 03 E1 03 E1 03 E1 03 E1 03 E1 | s...............
6 | 2 | 03 E1 03 E1 03 E1 03 E1 03 E1 03 E1 03 E1 03 E1 | ................
7 | 3 | A0 A1 A2 A3 A4 A5 FF 07 80 C1 FF FF FF FF FF FF | ................
8 1 | 4 | 03 2F D1 01 2B 55 04 74 75 63 6B 65 72 2E 74 68 | ./..+U.tucker.th
9 | 5 | 65 2D 74 77 6F 6D 65 79 73 2E 63 6F 6D 2F 62 6C | e-twomeys.com/bl
10 | 6 | 6F 67 2F 70 6F 73 74 73 2F 6E 64 65 66 2D 74 6C | og/posts/ndef-tl
11 | 7 | D3 F7 D3 F7 D3 F7 FF 07 80 40 FF FF FF FF FF FF | .........@......
12 2 | 8 | 76 FE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | v...............
13 | 9 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
14 | 10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
15 | 11 | D3 F7 D3 F7 D3 F7 FF 07 80 40 FF FF FF FF FF FF | .........@......
Sector 0 contains a MAD indicating the presence of NDEF data in all blocks.
Blocks 3, 7, and 11 contain key material and can be ignored.
Block 4 contains the start of TLV data.
The first block is of type 0x03
(NDEF) with length 0x2F
.
What follows is a NDEF message 0x2F
= 47 bytes long.
The NDEF message contains one record,
with record header 0xD1 01 2B 55
The first byte is 0xD1 = 0b1101 0001
, indicating that this is the first and
last record (by bits 7 and 6 being set),
no chunking is used (by bit 5 being cleared),
the length field being one byte (by bit 4 being set),
no ID field is present (by bit 3 being cleared),
and the record type type field containing an NFC Forum Well-Known type (by bits
2 to 0 being 0x1
).
The next byte is the type length (0x01
),
then the payload length (0x2B
).
There is then a 0x01
-byte type of 0x55
(ASCII U
),
which indicates that what follows is a URL,
then 0x2B
bytes of data.
The first data byte is 0x04
,
which indicates that https://
is to be prepended to the URL field.
Next is the URL (the-twomeys.com/blog/posts/ndef-tlv
, which is the URL for
this post).
Finally, a TLV terminator block (type 0xFE
) is present.
Starting with a blank card in emulator memory,
the data above could be written to emulator memory (or card memory if
hf mf wrbl -a --key D3F7D3F7D3F7
were substituted for hf mf esetblk
) by the
below commands.
1hf mf esetblk -b 4 -d 032FD1012B55047475636b65722e7468
2hf mf esetblk -b 5 -d 652d74776f6d6579732e636f6d2f626c
3hf mf esetblk -b 6 -d 6f672f706f7374732f6e6465662d746c
4hf mf esetblk -b 8 -d 76FE0000000000000000000000000000
When chunking, the payload length of each chunk is the length of that
chunk only.
The TNF, type length, and type of the first chunk is that of the full record,
while the TNF, type length, ID Present, ID, and type fields of subsequent
records are 0x06
, 0x00
, empty, and empty, respectively.
Because it seems that the NFC Forum enjoys using its juicy membership fees and technical wizardry to prop up obsolete text encodings.
Because apparently the NFC Forum has never considered the phrase
"penny wise, pound foolish".
That, or they seem to think that NFC tags are too small to fit the text
https://
but are big enough to fit the rest of a URL.