Search code examples
javaandroidnfcmifarendef

Writing NDEF data to NTAG216 tag using low-level NFC communication methods


I have implemented the code to interact with an NTAG216 by means of low-level communication methods (following NTAG212 Mifare Ultralight with Authentication and the datasheet of NTAG216).

What I have achieved so far:

  • Set password write protection on NTAG216 if not set or if new/blank tag.
  • If password is already set, authenticate using PWD_AUTH and comparing PACK.
  • Read data.
  • Write/overwrite data.

What I haven't been able to do so far:

  • Detect NDEF messages that I write to the tag in other apps. In other words I'm able to write to the tag using writePage() method and also read tags using readPage(). However, on time of writing pages I convert an NdefMessage into a byte array which can be read as well as written. However, this NDEF message is not detected in other applications.

What do I need to do in order to be able to detect NDEF messages I write from other applications?


Solution

  • NTAG216 is an NFC Forum Type 2 tag. Consequently, you have to follow the NFC Forum Type 2 Tag Operation specification when writing data to this type of NFC tag.

    Therefore, you will need to follow a few rules for a tag to be discoverable as NDEF tag (Type 2 tag):

    First, the capability container (located in block 3) needs to be configured.

    • Byte 0 must be set to the "magic" value 0xE1.
    • Byte 1 must be set to 0x10 to indicate mapping version 1.0.
    • Byte 2 must be set to 0x6D to indicate the memory size of NTAG216.
    • Byte 3 can be set to 0x00 to indicate read/write access to the NDEF data or to 0x0F to indicate read-only access (note that these are permissions on the application layer).

    So you can write the capability container as:

    byte[] response = nfc.transceive(new byte[] {
        (byte)0xA2, // WRITE
        (byte)3,    // block address
        (byte)0xE1, (byte)0x10, (byte)0x6D, (byte)0x00
    });
    

    NTAG216 already ships with a properly configured capability container so there is no need to do this by hand. Also note that block 3 is one-time-programmable, which means that bits can only be set to one but cannot be cleared to zero again. So if you did already overwrite the capability container with a different value, the tag can most likely no longer be used as an NDEF tag.

    The data must be written to the data blocks starting at block 4. NDEF messages must be wrapped into an NDEF message TLV (tag-length-value) structure. The tag for this TLV is 0x03. The length can be either in one-byte format (for NDEF messages with a length between 0 and 254 bytes) or in three-byte format (for NDEF messages with a length of 255 or more bytes). The data of this TLV block is the actual NDEF message (that you can obtain from ndefMessage.toByteArray()).

    For example, for the NDEF message D1 01 0D 55 01 65 78 61 6D 70 6C 65 2E 63 6F 6D 2F (which is a URI record with the URL "http://www.example.com/"), you would get the following TLV structure:

    03 11 D1010D55016578616D706C652E636F6D2F
    

    If you have alonger NDEF message (e.g. one with 259 bytes), you would use the three-byte length format:

    03 FF0103 D101FF5501...
    

    Further, you should mark the end of the data on the tag with a Terminator TLV (tag 0xFE, no length and data fields):

    FE
    

    You could then write this data to the tag as:

    byte[] ndefMessage = new byte[] {
        (byte)0xD1, (byte)0x01, (byte)0x0D, (byte)0x55, (byte)0x01, (byte)0x65, (byte)0x78, (byte)0x61, (byte)0x6D, (byte)0x70, (byte)0x6C, (byte)0x65, (byte)0x2E, (byte)0x63, (byte)0x6F, (byte)0x6D, (byte)0x2F
    };
    
    // wrap into TLV structure
    byte[] tlvEncodedData = null;
    if (ndefMessage.length < 255) {
        tlvEncodedData = new byte[ndefMessage.length + 3];
        tlvEncodedData[0] = (byte)0x03;  // NDEF TLV tag
        tlvEncodedData[1] = (byte)(ndefMessage.length & 0x0FF);  // NDEF TLV length (1 byte)
        System.arraycopy(ndefMessage, 0, tlvEncodedData, 2, ndefMessage.length);
        tlvEncodedData[2 + ndefMessage.length] = (byte)0xFE;  // Terminator TLV tag
    } else {
        tlvEncodedData = new byte[ndefMessage.length + 5];
        tlvEncodedData[0] = (byte)0x03;  // NDEF TLV tag
        tlvEncodedData[1] = (byte)0xFF;  // NDEF TLV length (3 byte, marker)
        tlvEncodedData[2] = (byte)((ndefMessage.length >>> 8) & 0x0FF);  // NDEF TLV length (3 byte, hi)
        tlvEncodedData[3] = (byte)(ndefMessage.length & 0x0FF);          // NDEF TLV length (3 byte, lo)
        System.arraycopy(ndefMessage, 0, tlvEncodedData, 4, ndefMessage.length);
        tlvEncodedData[4 + ndefMessage.length] = (byte)0xFE;  // Terminator TLV tag
    }
    
    // fill up with zeros to block boundary:
    tlvEncodedData = Arrays.copyOf(tlvEncodedData, (tlvEncodedData.length / 4 + 1) * 4);
    for (int i = 0; i < tlvEncodedData.length; i += 4) {
        byte[] command = new byte[] {
            (byte)0xA2, // WRITE
            (byte)((4 + i / 4) & 0x0FF), // block address
            0, 0, 0, 0
        };
        System.arraycopy(tlvEncodedData, i, command, 2, 4);
        byte[] response = nfc.transceive(command);
    }
    

    Finally, be aware that you can't use the tag as NDEF tag if you have read-password set on the NDEF data area since the NFC Forum Type 2 Tag Operation specification requires the tag to be freely readable.