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.
No comments yet! Start the discussion at forum.golioth.io