mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-04-26 03:39:58 -07:00
* NFC: Add NDEF Parser for MFUL, MFC and SLIX * Fix inefficiency in MAD checking * NFC: NDEF parser less RAM waste for contact vcards * Fix typo * Make PVS Happy * NFC: hide TODO in 3rd party plugin Co-authored-by: あく <alleteam@gmail.com>
1066 lines
36 KiB
C
1066 lines
36 KiB
C
// 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 <flipper_application.h>
|
|
|
|
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>
|
|
#include <nfc/protocols/mf_classic/mf_classic.h>
|
|
#include <nfc/protocols/slix/slix.h>
|
|
|
|
#include <bit_lib.h>
|
|
|
|
#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
|