Provisioning Devices over NFC

Near Field Communication (NFC) is widely known as the way in which one purchases coffee using a smartphone. But of course the technology is useful for a number of different data transmission applications. During a recent Golioth hack day, I jumped down the rabbit hole to use NFC as a way to provision devices.

The product of a misadventures in NFC

The majority of the Nordic dev kits I’ve purchased over the years came with a little spoon-shaped orange antenna for the NFC features present on the nRF52 and nRF53 lines of chips. I’ve never used it for any of the kits and had to scrounge around to locate one for this project.

I also ordered some cheap NFC tags to use on our hack day. Unfortunately, the Nordic parts themselves cannot function as a tag reader, only as an emulated tag. However, all was not lost. While I had been thinking of an access control system as my project for the day, Chris Gammell came up with the idea to write credentials to an unprovisioned device to add it to a Golioth IoT fleet. This an excellent idea, and Nordic has example code to use the nRF52/53 as a writable tag. Let the hacking begin!

There are two apps available for Android phones that I recommend for working with NFC. NFC Taginfo is great for reading tag, and NFC TagWriter is wonderful for writing data to an NCS tag. Both are from NXP, and free on the app store. Here you can see the app raising some skepticism for the cheap tags I purchased (which ended up going unused in this project).

Parsing Data Written to an NFC Tag

I started the hack day by running the Writable NDEF message sample code provided by Nordic. It works exactly as advertised; you write to the tag and the data will be written to non-volatile storage (NVS), making it the new readable value of the tag. Instead of storing the information as an NFC NDEF file, I needed to parse the incoming data and store it as a credential to authenticate with Golioth.

NFC tags are somewhat simple devices. When writing to a tag, the phone (or other writer) does all the work of formatting the information, the tag simply stores the bits and spits them back at you when read it later. This made my project mostly an exercising in parsing NFC NDEF messages.

static void nfc_callback(void *context,
             nfc_t4t_event_t event,
             const uint8_t *data,
             size_t data_length,
             uint32_t flags)
{
    switch (event) {
    case NFC_T4T_EVENT_FIELD_ON:
        LOG_INF("Field on");
        break;
    case NFC_T4T_EVENT_FIELD_OFF:
        LOG_INF("Field off");
        break;
    case NFC_T4T_EVENT_NDEF_UPDATED:
        /* Parse the data here */
        LOG_INF("New NFC Data Received");
        break;
    default:
        break;
    }
}

The sample code has a callback for NFC events. You are provided an event to key off of, as well as a pointer to a data buffer and length for that buffer. This was a bit of a goose-chase. The data buffer is longer than the the data_length provided. That’s because there’s an NLEN value at the beginning that you need to offset.

From there, you need to know how the NDEF headers work. I dug through the Text Record Type Definition specification which the NFC Forum will be happy to sell to you. There are a number of variable-sized fields in the header. I punted and wrote some brittle code to match the header I was expecting.  The result is code that calculates a pointer and length for the actual text data present in the NFC data that was received:

const uint8_t *ndef = data + NFC_NDEF_FILE_NLEN_FIELD_SIZE;
LOG_HEXDUMP_WRN(ndef, data_length, "NFC Data");

if ((ndef[0] & 1<<4) == 0)
{
    LOG_ERR("Only Short Records are supported");
    break;
}

if ((ndef[0] & 1<<3) != 0)
{
    LOG_ERR("ID_LENGTH not supported");
    break;
}

size_t payload_len = ndef[2];
char record_type = ndef[3];
size_t lang_code_size = ndef[4];

const uint8_t *body = ndef + 5 + lang_code_size;
size_t body_len = payload_len - lang_code_size - 1;

if (record_type == 'T')
{
    LOG_WRN("Received text payload");
    LOG_HEXDUMP_WRN(body, body_len, "Data");
}

The final bit of the puzzle is tokenizing the data. Since I control what is written to the tag, I simply provided three values separated by semicolons (GOLIOTHPSK;my-device@my-project;my-psk) and use a fast and dirty tokenizer.

char *decode_str = malloc(body_len + 1);
strncpy(decode_str, body, body_len + 1);

uint8_t idx_arr[IDX_ARR_SIZE] = {0};
int iter = 0;


char *token = strtok(decode_str, ";");
while(token)
{
    if (iter < IDX_ARR_SIZE)
    {
        idx_arr[iter] = token - decode_str;
    }
    token = strtok(NULL, ";");
    iter++;
}

Writing credentials to the storage partition

Once the PSK-ID and PSK are found, that’s really the whole ballgame. But for completeness, I utilized the runtime credential option available in Golioth sample code to store the credentials on the device storage partition.

First, the values were validated and stored in a global struct. Upon success, the system work queue is used to store the credentials and reboot the device outside of the NFC callback.

if (idx_arr[IDX_ARR_SIZE - 1] > 0)
{
    if (strcmp(decode_str, GOLIOTH_PSK_KEY) == 0)
    {
        LOG_WRN("Found PSK Command!");
        if (k_mutex_lock(&mut_decoded_psk, K_NO_WAIT) == 0)
        {
            decoded_psk.psk_id = decode_str + idx_arr[1];
            decoded_psk.psk = decode_str + idx_arr[2];
            k_work_submit(&update_psk_work);

            /* FIXME: Change NDEF */

            k_mutex_unlock(&mut_decoded_psk);
        }
        else
        {
            LOG_ERR("Unable to lock mutex. PSK update dropped.");
            free(decode_str);
        }
    }
}

And finally, the two work handlers that manage updating the credential and rebooting the device:

static void reboot_work_handler(struct k_work *work)
{
    for (int8_t i = 5; i >= 0; i--) {
        if (i) {
            LOG_INF("Rebooting in %d seconds...", i);
        }
        k_sleep(K_SECONDS(1));
    }

    /* Sync logs before reboot */
    LOG_PANIC();

    sys_reboot(SYS_REBOOT_COLD);
}
K_WORK_DEFINE(reboot_work, reboot_work_handler);

void update_psk_work_handler(struct k_work *work) {
    k_mutex_lock(&mut_decoded_psk, K_FOREVER);

    LOG_WRN("PSK-ID: %s", decoded_psk.psk_id);
    LOG_WRN("PSK: %s", decoded_psk.psk);

    int err;
    err = settings_save_one("golioth/psk-id", decoded_psk.psk_id, strlen(decoded_psk.psk_id));
    if (err)
    {
        LOG_ERR("Failed to save PSK-ID");
    }

    err = settings_save_one("golioth/psk", decoded_psk.psk, strlen(decoded_psk.psk));
    if (err)
    {
        LOG_ERR("Failed to save PSK");
    }

    k_work_submit(&reboot_work);
    k_mutex_unlock(&mut_decoded_psk);

}
K_WORK_DEFINE(update_psk_work, update_psk_work_handler);

The resulting output from the device showcases hex dumps of the raw data and the string after NDEF parsing. The values are then logged, stored in NVS, and the device reboots using the new credential.

Ideas for the Future

This project was more of a reason for me to explore NFC than anything else. With the hardware that I target in this project, it probably makes more sense to use NFC to set up a Bluetooth connection and then transfer data over that. However, given the sharp falloff of readable RF energy at a distance, NFC is harder for a malicious actor to sniff. Maybe there is something to be said for sending encryption credentials this way rather than via Bluetooth. As always, we recommend device certificates as the most secure way to validate your devices onto the cloud.

One thing that I didn’t have time for is updating the NDEF file so that the device ID can be read via NFC. If you’re running a fleet of Bluetooth nodes in one particular location, being able to use any phone to read the device ID or other identifying information is quite useful. Imagine a technician walking up to a device sealed in a waterproof case, being able to directly read the device instead of relying on a weathered external sticker.

With both read and write functionality, SoCs that include NFC capabilities were something I have been ignoring for quite some time. Now I have a pretty good handle on how the technologies work and I’ll be on the hunt for applications where this may shine.

Previous article
Mike Szczys
Mike Szczys
Mike is a Firmware Engineer at Golioth. His deep love of microcontrollers began in the early 2000s, growing from the desire to make more of the BEAM robotics he was building. During his 12 years at Hackaday (eight of them as Editor in Chief), he had a front-row seat for the growth of the industry, and was active in developing a number of custom electronic conference badges. When he's not reading data sheets he's busy as an orchestra musician in Madison, Wisconsin.

Post Comments

No comments yet! Start the discussion at forum.golioth.io

More from this author

Related posts

spot_img

Latest posts

Unwrapping Certificates

Certificates are the most secure way to connect your device to the cloud. This article reduces confusion surrounding them with simple examples.

A Device That Can’t Be Updated Is a Device That Can’t Be Trusted

Over-the-air updates are a crucial part of building and deploying secured devices, yet many product companies skip this step. This post outlines why it's difficult and how Golioth is making it easier.

Spring Cleaning with Golioth: Dust off your Zephyr skills in April 2025

Spring is a time of renewal, growth, and fresh starts—and your IoT projects deserve some spring cleaning, too! Dust off those ideas sitting idle, sharpen your embedded development toolkit, and join us for a free Zephyr RTOS Training on April 30th, 2025.

Want to stay up to date with the latest news?

Subscribe to our newsletter and get updates every 2 weeks. Follow the latest blogs and industry trends.