// Parser for NDEF format data // Supports multiple NDEF messages and records in same tag // Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty // Documentation and sources indicated where relevant // Made by @Willy-JL #include "nfc_supported_card_plugin.h" #include #include #include #define TAG "NDEF" static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { const char c = buf[i]; if((c < ' ' || c > '~') && c != '\r' && c != '\n') { return false; } } return true; } static void print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { if(prefix) furi_string_cat_printf(str, "%s: ", prefix); if(!force_hex && is_text(buf, len)) { char* tmp = malloc(len + 1); memcpy(tmp, buf, len); tmp[len] = '\0'; furi_string_cat_printf(str, "%s", tmp); free(tmp); } else { for(uint8_t i = 0; i < len; i++) { furi_string_cat_printf(str, "%02X ", buf[i]); } } furi_string_cat(str, "\n"); } static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 const char* prepends[] = { [0x00] = "", [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:", }; const char* prepend = ""; uint8_t prepend_type = payload[0]; if(prepend_type < COUNT_OF(prepends)) { prepend = prepends[prepend_type]; } size_t prepend_len = strlen(prepend); size_t uri_len = prepend_len + (payload_len - 1); char* const uri_buf = malloc(uri_len); memcpy(uri_buf, prepend, prepend_len); memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); char* uri = uri_buf; const char* type = "URI"; if(strncmp(uri, "http", strlen("http")) == 0) { type = "URL"; } else if(strncmp(uri, "tel:", strlen("tel:")) == 0) { type = "Phone"; uri += strlen("tel:"); uri_len -= strlen("tel:"); } else if(strncmp(uri, "mailto:", strlen("mailto:")) == 0) { type = "Mail"; uri += strlen("mailto:"); uri_len -= strlen("mailto:"); } furi_string_cat_printf(str, "%s\n", type); print_data(str, NULL, (uint8_t*)uri, uri_len, false); free(uri_buf); } static void parse_ndef_text(FuriString* str, const uint8_t* payload, uint32_t payload_len) { furi_string_cat(str, "Text\n"); print_data(str, NULL, payload + 3, payload_len - 3, false); } static void parse_ndef_bt(FuriString* str, const uint8_t* payload, uint32_t payload_len) { furi_string_cat(str, "BT MAC\n"); print_data(str, NULL, payload + 2, payload_len - 2, true); } static void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t payload_len) { char* tmp = malloc(payload_len + 1); memcpy(tmp, payload, payload_len); tmp[payload_len] = '\0'; FuriString* fmt = furi_string_alloc_set(tmp); free(tmp); furi_string_trim(fmt); if(furi_string_start_with(fmt, "BEGIN:VCARD")) { furi_string_right(fmt, furi_string_search_char(fmt, '\n')); if(furi_string_end_with(fmt, "END:VCARD")) { furi_string_left(fmt, furi_string_search_rchar(fmt, '\n')); } furi_string_trim(fmt); if(furi_string_start_with(fmt, "VERSION:")) { furi_string_right(fmt, furi_string_search_char(fmt, '\n')); furi_string_trim(fmt); } } furi_string_cat(str, "Contact\n"); print_data(str, NULL, (uint8_t*)furi_string_get_cstr(fmt), furi_string_size(fmt), false); furi_string_free(fmt); } static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t payload_len) { // https://android.googlesource.com/platform/packages/apps/Nfc/+/refs/heads/main/src/com/android/nfc/NfcWifiProtectedSetup.java #define CREDENTIAL_FIELD_ID (0x100E) #define SSID_FIELD_ID (0x1045) #define NETWORK_KEY_FIELD_ID (0x1027) #define AUTH_TYPE_FIELD_ID (0x1003) #define AUTH_TYPE_EXPECTED_SIZE (2) #define AUTH_TYPE_OPEN (0x0001) #define AUTH_TYPE_WPA_PSK (0x0002) #define AUTH_TYPE_WPA_EAP (0x0008) #define AUTH_TYPE_WPA2_EAP (0x0010) #define AUTH_TYPE_WPA2_PSK (0x0020) #define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022) #define MAX_NETWORK_KEY_SIZE_BYTES (64) size_t i = 0; while(i < payload_len) { uint16_t field_id = nfc_util_bytes2num(payload + i, 2); i += 2; uint16_t field_len = nfc_util_bytes2num(payload + i, 2); i += 2; if(field_id == CREDENTIAL_FIELD_ID) { furi_string_cat(str, "WiFi\n"); size_t start_position = i; while(i < start_position + field_len) { uint16_t cfg_id = nfc_util_bytes2num(payload + i, 2); i += 2; uint16_t cfg_len = nfc_util_bytes2num(payload + i, 2); i += 2; if(i + cfg_len > start_position + field_len) { return; } switch(cfg_id) { case SSID_FIELD_ID: print_data(str, "SSID", payload + i, cfg_len, false); i += cfg_len; break; case NETWORK_KEY_FIELD_ID: if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) { return; } print_data(str, "PWD", payload + i, cfg_len, false); i += cfg_len; break; case AUTH_TYPE_FIELD_ID: if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) { return; } short auth_type = nfc_util_bytes2num(payload + i, 2); i += 2; const char* auth; switch(auth_type) { case AUTH_TYPE_OPEN: auth = "Open"; break; case AUTH_TYPE_WPA_PSK: auth = "WPA Personal"; break; case AUTH_TYPE_WPA_EAP: auth = "WPA Enterprise"; break; case AUTH_TYPE_WPA2_EAP: auth = "WPA2 Enterprise"; break; case AUTH_TYPE_WPA2_PSK: auth = "WPA2 Personal"; break; case AUTH_TYPE_WPA_AND_WPA2_PSK: auth = "WPA/WPA2 Personal"; break; default: auth = "Unknown"; break; } print_data(str, "AUTH", (uint8_t*)auth, strlen(auth), false); break; default: i += cfg_len; break; } } return; } i += field_len; } } static void parse_ndef_payload( FuriString* str, uint8_t tnf, const char* type, uint8_t type_len, const uint8_t* payload, uint32_t payload_len) { if(!payload_len) { furi_string_cat(str, "Empty\n"); return; } switch(tnf) { case 0x01: // NFC Forum well-known type [NFC RTD] if(strncmp("U", type, type_len) == 0) { parse_ndef_uri(str, payload, payload_len); } else if(strncmp("T", type, type_len) == 0) { parse_ndef_text(str, payload, payload_len); } else { print_data(str, "Well-known type", (uint8_t*)type, type_len, false); print_data(str, "Payload", payload, payload_len, false); } break; case 0x02: // Media-type [RFC 2046] if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { parse_ndef_bt(str, payload, payload_len); } else if(strncmp("text/vcard", type, type_len) == 0) { parse_ndef_vcard(str, payload, payload_len); } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { parse_ndef_wifi(str, payload, payload_len); } else { print_data(str, "Media Type", (uint8_t*)type, type_len, false); print_data(str, "Payload", payload, payload_len, false); } break; case 0x00: // Empty case 0x03: // Absolute URI [RFC 3986] case 0x04: // NFC Forum external type [NFC RTD] case 0x05: // Unknown case 0x06: // Unchanged case 0x07: // Reserved default: // Unknown // Dump data without parsing print_data(str, "Type name format", &tnf, 1, true); print_data(str, "Type", (uint8_t*)type, type_len, false); print_data(str, "Payload", payload, payload_len, false); break; } } static const uint8_t* parse_ndef_message( FuriString* str, size_t message_num, const uint8_t* cur, const uint8_t* message_end) { // NDEF message and record documentation: // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/protocols/nfc/index.html#ndef-message-and-record-format size_t record_num = 0; bool last_record = false; while(cur < message_end) { // Flags and TNF uint8_t flags_tnf = *cur++; // Message Begin should only be set on first record if(record_num++ && flags_tnf & (1 << 7)) break; // Message End should only be set on last record if(last_record) break; if(flags_tnf & (1 << 6)) last_record = true; // Chunked Flag not supported if(flags_tnf & (1 << 5)) break; // Payload Length field of 1 vs 4 bytes bool short_record = flags_tnf & (1 << 4); // Is payload ID length and value present bool id_present = flags_tnf & (1 << 3); // Type Name Format 3 bit value uint8_t tnf = flags_tnf & 0b00000111; // Type Length uint8_t type_len = *cur++; // Payload Length uint32_t payload_len; if(short_record) { payload_len = *cur++; } else { payload_len = nfc_util_bytes2num(cur, 4); cur += 4; } // ID Length uint8_t id_len = 0; if(id_present) { id_len = *cur++; } // Payload Type char* type = NULL; if(type_len) { type = malloc(type_len); memcpy(type, cur, type_len); cur += type_len; } // Payload ID cur += id_len; furi_string_cat_printf(str, "\e*> M:%d R:%d - ", message_num, record_num); parse_ndef_payload(str, tnf, type, type_len, cur, payload_len); cur += payload_len; free(type); furi_string_trim(str, "\n"); furi_string_cat(str, "\n\n"); } return cur; } static bool ndef_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight); bool parsed = false; do { // Memory layout documentation: // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrfxlib/nfc/doc/type_2_tag.html#id2 // Double check static values layout // First 4 static reserved pages for UID, internal and lock bytes // (Not sure if NDEF cata can be found in cards with different layout) if(data->page[0].data[0] != 0x04) break; if(data->page[2].data[1] != 0x48) break; // Internal if(data->page[2].data[2] != 0x00) break; // Lock bytes if(data->page[2].data[3] != 0x00) break; // ... if(data->page[3].data[0] != 0xE1) break; // Capability container if(data->page[3].data[1] != 0x10) break; // ... // Data content starts here at 5th page const uint8_t* cur = &data->page[4].data[0]; const uint8_t* end = &data->page[0].data[0] + (mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE); size_t message_num = 0; // Parse as TLV (see docs above) while(cur < end) { switch(*cur++) { case 0x03: { // NDEF message if(cur >= end) break; uint16_t len; if(*cur < 0xFF) { // 1 byte length len = *cur++; } else { // 3 byte length (0xFF marker + 2 byte integer) if(cur + 2 >= end) { cur = end; break; } len = nfc_util_bytes2num(++cur, 2); cur += 2; } if(cur + len >= end) { cur = end; break; } if(message_num++ == 0) { furi_string_printf( parsed_data, "\e#NDEF Format Data\nCard type: %s\n", mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); } const uint8_t* message_end = cur + len; cur = parse_ndef_message(parsed_data, message_num, cur, message_end); if(cur != message_end) cur = end; break; } case 0xFE: // TLV end cur = end; if(message_num != 0) parsed = true; break; case 0x00: // Padding, has no length, skip break; case 0x01: // Lock control case 0x02: // Memory control case 0xFD: // Proprietary // We don't care, skip this TLV block if(cur >= end) break; if(*cur < 0xFF) { // 1 byte length cur += *cur + 1; // Shift by TLV length } else { // 3 byte length (0xFF marker + 2 byte integer) if(cur + 2 >= end) { cur = end; break; } cur += nfc_util_bytes2num(cur + 1, 2) + 3; // Shift by TLV length } break; default: // Unknown, bail to avoid problems cur = end; break; } } if(parsed) { furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { furi_string_reset(parsed_data); } } while(false); return parsed; } /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin ndef_plugin = { .protocol = NfcProtocolMfUltralight, .verify = NULL, .read = NULL, .parse = ndef_parse, }; /* Plugin descriptor to comply with basic plugin specification */ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, .entry_point = &ndef_plugin, }; /* Plugin entry point - must return a pointer to const descriptor */ const FlipperAppPluginDescriptor* ndef_plugin_ep() { return &ndef_plugin_descriptor; }