diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index a11e09d6b3d..6dbde7c3718 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -218,6 +218,35 @@ App( sources=["plugins/supported_cards/trt.c"], ) +App( + appid="ndef_ul_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_UL")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) +App( + appid="ndef_mfc_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_MFC")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + +App( + appid="ndef_slix_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_SLIX")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + App( appid="nfc_start", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c new file mode 100644 index 00000000000..fb2c4da480a --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -0,0 +1,1065 @@ +// 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