unleashed-firmware/applications/main/nfc/plugins/supported_cards/ndef.c

475 lines
16 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
// Documentation and sources indicated where relevant
// Made by @Willy-JL
#include "nfc_supported_card_plugin.h"
#include <nfc/helpers/nfc_util.h>
#include <flipper_application/flipper_application.h>
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>
#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;
}