diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index fec3b6c76..4ea2e6928 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -164,6 +164,15 @@ App( sources=["plugins/supported_cards/washcity.c"], ) +App( + appid="ndef_parser", + apptype=FlipperAppType.PLUGIN, + 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 000000000..a40f59a16 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -0,0 +1,475 @@ +// 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; +}