// 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, SmartPoster // Documentation and sources indicated where relevant // Made by @Willy-JL // Mifare Ultralight support by @Willy-JL // Mifare Classic support by @luu176 & @Willy-JL // SLIX support by @Willy-JL // We use an arbitrary position system here, in order to support more protocols. // Each protocol parses basic structure of the card, then starts ndef_parse_tlv() // using an arbitrary position value that it can understand. When accessing data // to parse NDEF content, ndef_get() will then map this arbitrary value to the // card using state in Ndef struct, skipping blocks or sectors as needed. This // way, NDEF parsing code does not need to know details of card layout. #include "nfc_supported_card_plugin.h" #include #include #include #include #include #define TAG "NDEF" #define NDEF_PROTO_INVALID (-1) #define NDEF_PROTO_RAW (0) // For parsing data fed manually #define NDEF_PROTO_UL (1) #define NDEF_PROTO_MFC (2) #define NDEF_PROTO_SLIX (3) #define NDEF_PROTO_TOTAL (4) #ifndef NDEF_PROTO #error Must specify what protocol to use with NDEF_PROTO define! #endif #if NDEF_PROTO <= NDEF_PROTO_INVALID || NDEF_PROTO >= NDEF_PROTO_TOTAL #error Invalid NDEF_PROTO specified! #endif #define NDEF_TITLE(device, parsed_data) \ furi_string_printf( \ parsed_data, \ "\e#NDEF Format Data\nCard type: %s\n", \ nfc_device_get_name(device, NfcDeviceNameTypeFull)) // ---=== structures ===--- // TLV structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data typedef enum FURI_PACKED { NdefTlvPadding = 0x00, NdefTlvLockControl = 0x01, NdefTlvMemoryControl = 0x02, NdefTlvNdefMessage = 0x03, NdefTlvProprietary = 0xFD, NdefTlvTerminator = 0xFE, } NdefTlv; // Type Name Format values: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf typedef enum FURI_PACKED { NdefTnfEmpty = 0x00, NdefTnfWellKnownType = 0x01, NdefTnfMediaType = 0x02, NdefTnfAbsoluteUri = 0x03, NdefTnfExternalType = 0x04, NdefTnfUnknown = 0x05, NdefTnfUnchanged = 0x06, NdefTnfReserved = 0x07, } NdefTnf; // Flags and TNF structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf typedef struct FURI_PACKED { // Reversed due to endianness NdefTnf type_name_format : 3; bool id_length_present : 1; bool short_record : 1; bool chunk_flag : 1; bool message_end : 1; bool message_begin : 1; } NdefFlagsTnf; _Static_assert(sizeof(NdefFlagsTnf) == 1); // URI payload format: // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 static const char* ndef_uri_prepends[] = { [0x00] = NULL, // Allows detecting no prepend and checking schema for type [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:", }; // ---=== card memory layout abstraction ===--- // Shared context and state, read above typedef struct { FuriString* output; #if NDEF_PROTO == NDEF_PROTO_RAW struct { const uint8_t* data; size_t size; } raw; #elif NDEF_PROTO == NDEF_PROTO_UL struct { const uint8_t* start; size_t size; } ul; #elif NDEF_PROTO == NDEF_PROTO_MFC struct { const MfClassicBlock* blocks; size_t size; } mfc; #elif NDEF_PROTO == NDEF_PROTO_SLIX struct { const uint8_t* start; size_t size; } slix; #endif } Ndef; static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { #if NDEF_PROTO == NDEF_PROTO_RAW // Using user-provided pointer, simply need to remap to it if(pos + len > ndef->raw.size) return false; memcpy(buf, ndef->raw.data + pos, len); return true; #elif NDEF_PROTO == NDEF_PROTO_UL // Memory space is contiguous, simply need to remap to data pointer if(pos + len > ndef->ul.size) return false; memcpy(buf, ndef->ul.start + pos, len); return true; #elif NDEF_PROTO == NDEF_PROTO_MFC // We need to skip sector trailers and MAD2, NDEF parsing just uses // a position offset in data space, as if it were contiguous. // Start with a simple data space size check if(pos + len > ndef->mfc.size) return false; // First 128 blocks are 32 sectors: 3 data blocks, 1 sector trailer. // Sector 16 contains MAD2 and we need to skip this. // So the first 93 (31*3) data blocks correspond to 128 real blocks. // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. // So the last 120 (8*15) data blocks correspond to 128 real blocks. div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); size_t large_sector_data_blocks = 0; if(small_sector_data_blocks.quot > 93) { large_sector_data_blocks = small_sector_data_blocks.quot - 93; small_sector_data_blocks.quot = 93; } div_t small_sectors = div(small_sector_data_blocks.quot, 3); size_t real_block = small_sectors.quot * 4 + small_sectors.rem; if(small_sectors.quot >= 16) { real_block += 4; // Skip MAD2 } if(large_sector_data_blocks) { div_t large_sectors = div(large_sector_data_blocks, 15); real_block += large_sectors.quot * 16 + large_sectors.rem; } const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; while(len) { size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; size_t chunk_len = MIN((size_t)(end - cur), len); memcpy(buf, cur, chunk_len); len -= chunk_len; if(len) { real_block = sector_trailer + 1; if(real_block == 64) { real_block += 4; // Skip MAD2 } cur = &ndef->mfc.blocks[real_block].data[0]; } } return true; #elif NDEF_PROTO == NDEF_PROTO_SLIX // Memory space is contiguous, simply need to remap to data pointer if(pos + len > ndef->slix.size) return false; memcpy(buf, ndef->slix.start + pos, len); return true; #else UNUSED(ndef); UNUSED(pos); UNUSED(len); UNUSED(buf); return false; #endif } // ---=== output helpers ===--- static inline bool is_printable(char c) { return (c >= ' ' && c <= '~') || c == '\r' || c == '\n'; } static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { if(!is_printable(buf[i])) return false; } return true; } static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bool force_hex) { if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); // We don't have direct access to memory chunks due to different card layouts // Making a temporary buffer is wasteful of RAM and we can't afford this // So while iterating like this is inefficient, it saves RAM and works between multiple card types if(!force_hex) { // If we find a non-printable character along the way, reset string to prev state and re-do as hex size_t string_prev = furi_string_size(ndef->output); for(size_t i = 0; i < len; i++) { char c; if(!ndef_get(ndef, pos + i, 1, &c)) return false; if(!is_printable(c)) { furi_string_left(ndef->output, string_prev); force_hex = true; break; } furi_string_push_back(ndef->output, c); } } if(force_hex) { for(size_t i = 0; i < len; i++) { uint8_t b; if(!ndef_get(ndef, pos + i, 1, &b)) return false; furi_string_cat_printf(ndef->output, "%02X ", b); } } furi_string_cat(ndef->output, "\n"); return true; } static void ndef_print(Ndef* ndef, const char* prefix, const void* buf, size_t len, bool force_hex) { if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); if(!force_hex && is_text(buf, len)) { furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); } else { for(size_t i = 0; i < len; i++) { furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); } } furi_string_cat(ndef->output, "\n"); } // ---=== payload parsing ===--- static inline uint8_t hex_to_int(char c) { if(c >= '0' && c <= '9') return c - '0'; if(c >= 'A' && c <= 'F') return c - 'A' + 10; if(c >= 'a' && c <= 'f') return c - 'a' + 10; return 0; } static char url_decode_char(const char* str) { return (hex_to_int(str[0]) << 4) | hex_to_int(str[1]); } static bool ndef_parse_uri(Ndef* ndef, size_t pos, size_t len) { const char* type = "URI"; // Parse URI prepend type const char* prepend = NULL; uint8_t prepend_type; if(!ndef_get(ndef, pos++, 1, &prepend_type)) return false; len--; if(prepend_type < COUNT_OF(ndef_uri_prepends)) { prepend = ndef_uri_prepends[prepend_type]; } if(prepend) { if(strncmp(prepend, "http", 4) == 0) { type = "URL"; } else if(strncmp(prepend, "tel:", 4) == 0) { type = "Phone"; prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output } else if(strncmp(prepend, "mailto:", 7) == 0) { type = "Mail"; prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output } } // Parse and optionally skip schema, if no prepend was specified if(!prepend) { char schema[7] = {0}; // Longest schema we check is 7 char long without terminator if(!ndef_get(ndef, pos, MIN(sizeof(schema), len), schema)) return false; if(strncmp(schema, "http", 4) == 0) { type = "URL"; } else if(strncmp(schema, "tel:", 4) == 0) { type = "Phone"; pos += 4; len -= 4; } else if(strncmp(schema, "mailto:", 7) == 0) { type = "Mail"; pos += 7; len -= 7; } } // Print static data as-is furi_string_cat_printf(ndef->output, "%s\n", type); if(prepend) { furi_string_cat(ndef->output, prepend); } // Print URI one char at a time and perform URL decode while(len) { char c; if(!ndef_get(ndef, pos++, 1, &c)) return false; len--; if(c != '%' || len < 2) { // Not encoded, or not enough remaining text for encoded char furi_string_push_back(ndef->output, c); continue; } char enc[2]; if(!ndef_get(ndef, pos, 2, enc)) return false; enc[0] = toupper(enc[0]); enc[1] = toupper(enc[1]); // Only consume and print these 2 characters if they're valid URL encoded // Otherwise they're processed in next iterations and we output the % char if(((enc[0] >= 'A' && enc[0] <= 'F') || (enc[0] >= '0' && enc[0] <= '9')) && ((enc[1] >= 'A' && enc[1] <= 'F') || (enc[1] >= '0' && enc[1] <= '9'))) { pos += 2; len -= 2; c = url_decode_char(enc); } furi_string_push_back(ndef->output, c); } return true; } static bool ndef_parse_text(Ndef* ndef, size_t pos, size_t len) { furi_string_cat(ndef->output, "Text\n"); if(!ndef_dump(ndef, NULL, pos + 3, len - 3, false)) return false; return true; } static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { furi_string_cat(ndef->output, "BT MAC\n"); if(len != 8) return false; if(!ndef_dump(ndef, NULL, pos + 2, len - 2, true)) return false; return true; } static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { // We hide redundant tags the user is probably not interested in. // Would be easier with FuriString checks for start_with() and end_with() // but to do that would waste lots of RAM on a cloned string buffer // so instead we just look for these markers at start/end and shift // pos and len then use ndef_dump() to output one char at a time. // Results in minimal stack and no heap usage at all. static const char* const begin_tag = "BEGIN:VCARD"; static const uint8_t begin_len = strlen(begin_tag); static const char* const version_tag = "VERSION:"; static const uint8_t version_len = strlen(version_tag); static const char* const end_tag = "END:VCARD"; static const uint8_t end_len = strlen(end_tag); char tmp[13] = {0}; // Enough for BEGIN:VCARD\r\n uint8_t skip = 0; // Skip BEGIN tag if(len >= sizeof(tmp)) { if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; if(strncmp(begin_tag, tmp, begin_len) == 0) { skip = begin_len; if(tmp[skip] == '\r') skip++; if(tmp[skip] == '\n') skip++; pos += skip; len -= skip; } } // Skip VERSION tag if(len >= sizeof(tmp)) { if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; if(strncmp(version_tag, tmp, version_len) == 0) { skip = version_len; while(skip < len) { if(!ndef_get(ndef, pos + skip, 1, &tmp[0])) return false; skip++; if(tmp[0] == '\n') break; } pos += skip; len -= skip; } } // Skip END tag if(len >= sizeof(tmp)) { if(!ndef_get(ndef, pos + len - sizeof(tmp), sizeof(tmp), tmp)) return false; // Read more than length of END tag and check multiple offsets, might have some padding after // Worst case: there is END:VCARD\r\n\r\n which is same length as tmp buffer (13) // Not sure if this is in spec but might aswell check static const uint8_t offsets = sizeof(tmp) - end_len + 1; for(uint8_t offset = 0; offset < offsets; offset++) { if(strncmp(end_tag, tmp + offset, end_len) == 0) { skip = sizeof(tmp) - offset; len -= skip; break; } } } furi_string_cat(ndef->output, "Contact\n"); ndef_dump(ndef, NULL, pos, len, false); return true; } // Loosely based on Android WiFi NDEF implementation: // https://android.googlesource.com/platform/packages/apps/Nfc/+/025560080737b43876c9d81feff3151f497947e8/src/com/android/nfc/NfcWifiProtectedSetup.java static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { #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) furi_string_cat(ndef->output, "WiFi\n"); size_t end = pos + len; uint8_t tmp_buf[2]; while(pos < end) { if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; uint16_t field_id = bit_lib_bytes_to_num_be(tmp_buf, 2); pos += 2; if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; uint16_t field_len = bit_lib_bytes_to_num_be(tmp_buf, 2); pos += 2; FURI_LOG_D(TAG, "wifi field: %04X len: %d", field_id, field_len); if(pos + field_len > end) { return false; } if(field_id == CREDENTIAL_FIELD_ID) { size_t field_end = pos + field_len; while(pos < field_end) { if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; uint16_t cfg_id = bit_lib_bytes_to_num_be(tmp_buf, 2); pos += 2; if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; uint16_t cfg_len = bit_lib_bytes_to_num_be(tmp_buf, 2); pos += 2; FURI_LOG_D(TAG, "wifi cfg: %04X len: %d", cfg_id, cfg_len); if(pos + cfg_len > field_end) { return false; } switch(cfg_id) { case SSID_FIELD_ID: if(!ndef_dump(ndef, "SSID", pos, cfg_len, false)) return false; pos += cfg_len; break; case NETWORK_KEY_FIELD_ID: if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) { return false; } if(!ndef_dump(ndef, "PWD", pos, cfg_len, false)) return false; pos += cfg_len; break; case AUTH_TYPE_FIELD_ID: if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) { return false; } if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; uint16_t auth_type = bit_lib_bytes_to_num_be(tmp_buf, 2); pos += 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; } ndef_print(ndef, "AUTH", auth, strlen(auth), false); break; default: pos += cfg_len; break; } } return true; } pos += field_len; } furi_string_cat(ndef->output, "No data parsed\n"); return true; } // ---=== ndef layout parsing ===--- bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, NdefTnf tnf, const char* type, uint8_t type_len); bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed); bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, NdefTnf tnf, const char* type, uint8_t type_len) { FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len); if(!len) { furi_string_cat(ndef->output, "Empty\n"); return true; } switch(tnf) { case NdefTnfWellKnownType: if(strncmp("Sp", type, type_len) == 0) { furi_string_cat(ndef->output, "SmartPoster\nContained records below\n\n"); return ndef_parse_message(ndef, pos, len, 0, true); } else if(strncmp("U", type, type_len) == 0) { return ndef_parse_uri(ndef, pos, len); } else if(strncmp("T", type, type_len) == 0) { return ndef_parse_text(ndef, pos, len); } // Dump data without parsing furi_string_cat(ndef->output, "Unknown\n"); ndef_print(ndef, "Well-known Type", type, type_len, false); if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; return true; case NdefTnfMediaType: if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { return ndef_parse_bt(ndef, pos, len); } else if(strncmp("text/vcard", type, type_len) == 0) { return ndef_parse_vcard(ndef, pos, len); } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { return ndef_parse_wifi(ndef, pos, len); } // Dump data without parsing furi_string_cat(ndef->output, "Unknown\n"); ndef_print(ndef, "Media Type", type, type_len, false); if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; return true; case NdefTnfEmpty: case NdefTnfAbsoluteUri: case NdefTnfExternalType: case NdefTnfUnknown: case NdefTnfUnchanged: case NdefTnfReserved: default: // Dump data without parsing furi_string_cat(ndef->output, "Unsupported\n"); ndef_print(ndef, "Type name format", &tnf, 1, true); ndef_print(ndef, "Type", type, type_len, false); if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; return true; } } // NDEF message structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { size_t end = pos + len; size_t record_num = 0; bool last_record = false; while(pos < end) { // Flags and TNF NdefFlagsTnf flags_tnf; if(!ndef_get(ndef, pos++, 1, &flags_tnf)) return false; FURI_LOG_D(TAG, "flags_tnf: %02X", *(uint8_t*)&flags_tnf); FURI_LOG_D(TAG, "flags_tnf.message_begin: %d", flags_tnf.message_begin); FURI_LOG_D(TAG, "flags_tnf.message_end: %d", flags_tnf.message_end); FURI_LOG_D(TAG, "flags_tnf.chunk_flag: %d", flags_tnf.chunk_flag); FURI_LOG_D(TAG, "flags_tnf.short_record: %d", flags_tnf.short_record); FURI_LOG_D(TAG, "flags_tnf.id_length_present: %d", flags_tnf.id_length_present); FURI_LOG_D(TAG, "flags_tnf.type_name_format: %02X", flags_tnf.type_name_format); // Message Begin should only be set on first record if(record_num++ && flags_tnf.message_begin) return false; // Message End should only be set on last record if(last_record) return false; if(flags_tnf.message_end) last_record = true; // Chunk Flag not supported if(flags_tnf.chunk_flag) return false; // Type Length uint8_t type_len; if(!ndef_get(ndef, pos++, 1, &type_len)) return false; // Payload Length field of 1 or 4 bytes uint32_t payload_len; if(flags_tnf.short_record) { uint8_t payload_len_short; if(!ndef_get(ndef, pos++, 1, &payload_len_short)) return false; payload_len = payload_len_short; } else { if(!ndef_get(ndef, pos, sizeof(payload_len), &payload_len)) return false; payload_len = bit_lib_bytes_to_num_be((void*)&payload_len, sizeof(payload_len)); pos += sizeof(payload_len); } // ID Length uint8_t id_len = 0; if(flags_tnf.id_length_present) { if(!ndef_get(ndef, pos++, 1, &id_len)) return false; } // Payload Type char type_buf[32]; // Longest type supported in ndef_parse_record() is 32 chars excl terminator char* type = type_buf; bool type_was_allocated = false; if(type_len) { if(type_len > sizeof(type_buf)) { type = malloc(type_len); type_was_allocated = true; } if(!ndef_get(ndef, pos, type_len, type)) { if(type_was_allocated) free(type); return false; } pos += type_len; } // Payload ID pos += id_len; if(smart_poster) { furi_string_cat_printf(ndef->output, "\e*> SP-R%zu: ", record_num); } else { furi_string_cat_printf(ndef->output, "\e*> M%zu-R%zu: ", message_num, record_num); } if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { if(type_was_allocated) free(type); return false; } pos += payload_len; if(type_was_allocated) free(type); furi_string_trim(ndef->output, "\n"); furi_string_cat(ndef->output, "\n\n"); } if(record_num == 0) { if(smart_poster) { furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); } else { furi_string_cat_printf(ndef->output, "\e*> M%zu: Empty\n\n", message_num); } } return pos == end && (last_record || record_num == 0); } // TLV structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed) { size_t end = pos + len; size_t message_num = 0; while(pos < end) { NdefTlv tlv; if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; FURI_LOG_D(TAG, "tlv: %02X", tlv); switch(tlv) { default: // Unknown, bail to avoid problems return 0; case NdefTlvPadding: // Has no length, skip to next byte break; case NdefTlvTerminator: // NDEF message finished, return whether we parsed something return message_num; case NdefTlvLockControl: case NdefTlvMemoryControl: case NdefTlvProprietary: case NdefTlvNdefMessage: { // Calculate length uint16_t len; uint8_t len_type; if(!ndef_get(ndef, pos++, 1, &len_type)) return 0; if(len_type < 0xFF) { // 1 byte length len = len_type; } else { // 3 byte length (0xFF marker + 2 byte integer) if(!ndef_get(ndef, pos, sizeof(len), &len)) return 0; len = bit_lib_bytes_to_num_be((void*)&len, sizeof(len)); pos += sizeof(len); } if(tlv != NdefTlvNdefMessage) { // We don't care, skip this TLV block to next one pos += len; break; } if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed, false)) return 0; pos += len; break; } } } // Reached data end with no TLV terminator, // but also no errors, treat this as a success return message_num; } #if NDEF_PROTO != NDEF_PROTO_RAW // ---=== protocol entry-points ===--- #if NDEF_PROTO == NDEF_PROTO_UL // MF UL memory layout: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#memory_layout static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight); // Check card type can contain NDEF if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K && data->type != MfUltralightTypeNTAGI2CPlus1K && data->type != MfUltralightTypeNTAGI2CPlus2K) { return false; } // Check Capability Container (CC) values struct { uint8_t nfc_magic_number; uint8_t document_version_number; uint8_t data_area_size; // Usable byte size / 8, only includes user memory uint8_t read_write_access; }* cc = (void*)&data->page[3].data[0]; if(cc->nfc_magic_number != 0xE1) return false; if(cc->document_version_number != 0x10) return false; // Calculate usable data area const uint8_t* start = &data->page[4].data[0]; const uint8_t* end = start + (cc->data_area_size * 8); size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; end = MIN(end, &data->page[0].data[0] + max_size); size_t data_start = 0; size_t data_size = end - start; NDEF_TITLE(device, parsed_data); Ndef ndef = { .output = parsed_data, .ul = { .start = start, .size = data_size, }, }; size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); if(parsed) { furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { furi_string_reset(parsed_data); } return parsed > 0; } #elif NDEF_PROTO == NDEF_PROTO_MFC // MFC MAD datasheet: // https://www.nxp.com/docs/en/application-note/AN10787.pdf #define AID_SIZE (2) static const uint64_t mad_key = 0xA0A1A2A3A4A5; // NDEF on MFC breakdown: // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#storing-ndef-messages-in-mifare-sectors-607778 static const uint8_t ndef_aid[AID_SIZE] = {0x03, 0xE1}; static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); // Check card type can contain NDEF if(data->type != MfClassicType1k && data->type != MfClassicType4k && data->type != MfClassicTypeMini) { return false; } // Check MADs for what sectors contain NDEF data AIDs bool sectors_with_ndef[MF_CLASSIC_TOTAL_SECTORS_MAX] = {0}; const size_t sector_count = mf_classic_get_total_sectors_num(data->type); const struct { size_t block; uint8_t aid_count; } mads[2] = { {1, 15}, {64, 23}, }; for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { const size_t block = mads[mad].block; const size_t sector = mf_classic_get_sector_by_block(block); if(sector_count <= sector) break; // Skip this MAD if not present // Check MAD key const MfClassicSectorTrailer* sector_trailer = mf_classic_get_sector_trailer_by_sector(data, sector); const uint64_t sector_key_a = bit_lib_bytes_to_num_be( sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); if(sector_key_a != mad_key) return false; // Find NDEF AIDs for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { sectors_with_ndef[aid_index + 1] = true; } } } NDEF_TITLE(device, parsed_data); // Calculate how large the data space is, so excluding sector trailers and MAD2. // Makes sure we stay within this card's actual content when parsing. // First 32 sectors: 3 data blocks, 1 sector trailer. // Sector 16 contains MAD2 and we need to skip this. // So the first 32 sectors correspond to 93 (31*3) data blocks. // Last 8 sectors: 15 data blocks, 1 sector trailer. // So the last 8 sectors correspond to 120 (8*15) data blocks. size_t data_size; if(sector_count > 32) { data_size = 93 + (sector_count - 32) * 15; } else { data_size = sector_count * 3; if(sector_count >= 16) { data_size -= 3; // Skip MAD2 } } data_size *= MF_CLASSIC_BLOCK_SIZE; Ndef ndef = { .output = parsed_data, .mfc = { .blocks = data->block, .size = data_size, }, }; size_t total_parsed = 0; for(size_t sector = 0; sector < sector_count; sector++) { if(!sectors_with_ndef[sector]) continue; FURI_LOG_D(TAG, "sector: %d", sector); size_t string_prev = furi_string_size(parsed_data); // Convert real sector number to data block number // to skip sector trailers and MAD2 size_t data_block; if(sector < 32) { data_block = sector * 3; if(sector >= 16) { data_block -= 3; // Skip MAD2 } } else { data_block = 93 + (sector - 32) * 15; } FURI_LOG_D(TAG, "data_block: %zu", data_block); size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); if(parsed) { total_parsed += parsed; furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { furi_string_left(parsed_data, string_prev); } } if(!total_parsed) { furi_string_reset(parsed_data); } return total_parsed > 0; } #elif NDEF_PROTO == NDEF_PROTO_SLIX // SLIX NDEF memory layout: // https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/nfc/7583/1/EEOL_2011FEB16_EMS_RFD_AN_01.pdf static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const Iso15693_3Data* data = nfc_device_get_data(device, NfcProtocolIso15693_3); const uint8_t block_size = iso15693_3_get_block_size(data); const uint16_t block_count = iso15693_3_get_block_count(data); const uint8_t* blocks = simple_array_cget_data(data->block_data); // TODO(-nofl): Find some way to check for other iso15693 NDEF cards and // split this to also support non-slix iso15693 NDEF tags // Rest of the code works on iso15693 too, but uses SLIX layout assumptions if(block_size != SLIX_BLOCK_SIZE) { return false; } // Check Capability Container (CC) values struct { uint8_t nfc_magic_number; uint8_t read_write_access : 4; // Reversed due to endianness uint8_t document_version_number : 4; uint8_t data_area_size; // Total byte size / 8, includes block 0 uint8_t mbread_ipread; }* cc = (void*)&blocks[0 * block_size]; if(cc->nfc_magic_number != 0xE1) return false; if(cc->document_version_number != 0x4) return false; // Calculate usable data area const uint8_t* start = &blocks[1 * block_size]; const uint8_t* end = blocks + (cc->data_area_size * 8); size_t max_size = block_count * block_size; end = MIN(end, blocks + max_size); size_t data_start = 0; size_t data_size = end - start; NDEF_TITLE(device, parsed_data); Ndef ndef = { .output = parsed_data, .slix = { .start = start, .size = data_size, }, }; size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); if(parsed) { furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { furi_string_reset(parsed_data); } return parsed > 0; } #endif // ---=== boilerplate ===--- /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin ndef_plugin = { .verify = NULL, .read = NULL, #if NDEF_PROTO == NDEF_PROTO_UL .parse = ndef_ul_parse, .protocol = NfcProtocolMfUltralight, #elif NDEF_PROTO == NDEF_PROTO_MFC .parse = ndef_mfc_parse, .protocol = NfcProtocolMfClassic, #elif NDEF_PROTO == NDEF_PROTO_SLIX .parse = ndef_slix_parse, .protocol = NfcProtocolSlix, #endif }; /* 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(void) { return &ndef_plugin_descriptor; } #endif