From cdca934f7e9f04ba4a6ce29dd2de97d0be547283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Sat, 22 Aug 2026 02:11:29 +0800 Subject: [PATCH] feat: hf mfu ndefwrite command --- CHANGELOG.md | 1 + client/src/cmdhfmfu.c | 426 ++++++++++++++++++++++++++++++++++++++++++ client/src/cmdhfmfu.h | 3 +- client/src/cmdnfc.c | 185 +++++++++++++++++- client/src/nfc/ndef.c | 265 ++++++++++++++++++++++++++ client/src/nfc/ndef.h | 31 +++ 6 files changed, 908 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a80c7790f..c6e1b02bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `nfc encode` and `hf mfu ndefwrite` - build NDEF URI/Text/AAR records and write them to MIFARE Ultralight / NTAG tags, plus a reusable NDEF encoder in `nfc/ndef.c` (@0x6r1an0y) - Fixed `hf iclass sam` - removed the old implementation (@iceman1001) - Fixed Proxmark5 (PM5/AT32) failing to re-enter the bootloader (@nemanjan00) - Fixed `hw fpga config` on Proxmark5 (PM5/AT32) now powers on the FPGA 24MHz clock (@nemanjan00) diff --git a/client/src/cmdhfmfu.c b/client/src/cmdhfmfu.c index 74ec9cbe7..1b307e2bd 100644 --- a/client/src/cmdhfmfu.c +++ b/client/src/cmdhfmfu.c @@ -7402,6 +7402,431 @@ int CmdHF14MfuNDEFRead(const char *Cmd) { return status; } + +// Build a NDEF message from the CLI options and write it into the data area of a +// MIFARE Ultralight / NTAG tag. The tag has to be NDEF formatted already, this +// command never touches the Capability Container in block 3. +int CmdHF14MfuNDEFWrite(const char *Cmd) { + + int ak_len = 0; + bool has_auth_key = false; + bool has_pwd = false; + bool swap_endian = false; + + iso14a_card_select_t card; + uint8_t data[16] = {0x00}; + uint8_t authenticationkey[16] = {0x00}; + uint8_t *auth_key_ptr = authenticationkey; + uint8_t pack[4] = {0, 0, 0, 0}; + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfu ndefwrite", + "Write NFC Data Exchange Format (NDEF) records to a MIFARE Ultralight / NTAG tag.\n" + "The tag must already be NDEF formatted, ie carry a Capability Container in block 3.\n" + "\n" + "Combine several of --uri, --text and --aar to build a multi record message,\n" + "records are added in that order. Alternatively supply raw NDEF bytes with -d\n" + "or -f, those get wrapped in a TLV container automatically when they are not\n" + "already. Use `nfc encode` to build such raw bytes offline.\n" + "\n" + "Note: the tag is re-selected and re-authenticated for every block written,\n" + "so a large message takes a while.", + "hf mfu ndefwrite --uri https://proxmark.com\n" + "hf mfu ndefwrite --uri tel:+123456789\n" + "hf mfu ndefwrite --text \"hello world\"\n" + "hf mfu ndefwrite --aar com.example.app\n" + "hf mfu ndefwrite --uri https://proxmark.com --aar com.example.app\n" + "hf mfu ndefwrite --uri https://proxmark.com -k ffffffff\n" + "hf mfu ndefwrite -d 0311D1010D550270726F786D61726B2E636F6DFE\n" + "hf mfu ndefwrite -f myfilename" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0("k", "key", "", "Authentication key (UL-C/UL-AES 16 bytes, EV1/NTAG 4 bytes)"), + arg_lit0("l", NULL, "Swap entered key endianness"), + arg_str0(NULL, "uri", "", "URI record. URL, tel:, mailto:, ..."), + arg_str0(NULL, "text", "", "Text record"), + arg_str0(NULL, "lang", "", "language code for the text record (default: en)"), + arg_str0(NULL, "aar", "", "Android Application Record, ie an app package name"), + arg_str0("d", "data", "", "Raw NDEF bytes to write"), + arg_str0("f", "file", "", "Raw NDEF file to write"), + arg_lit0("v", "verbose", "Verbose output"), + arg_lit0(NULL, "schann", "use secure channel. Must have key"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + CLIGetHexWithReturn(ctx, 1, authenticationkey, &ak_len); + swap_endian = arg_get_lit(ctx, 2); + + // CLIParamStrToBuf copies the trailing NUL as well but only rejects lengths + // strictly above maxdatalen, so leave room for that byte + int urilen = 0; + char uri[1024] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)uri, sizeof(uri) - 1, &urilen); + + int textlen = 0; + char text[1024] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)text, sizeof(text) - 1, &textlen); + + int langlen = 0; + char lang[32] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 5), (uint8_t *)lang, sizeof(lang) - 1, &langlen); + + int aarlen = 0; + char aar[256] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)aar, sizeof(aar) - 1, &aarlen); + + int rawlen = 0; + uint8_t raw[MFU_NDEF_MAX_BYTES] = {0}; + CLIGetHexWithReturn(ctx, 7, raw, &rawlen); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 8), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + + bool verbose = arg_get_lit(ctx, 9); + bool use_schann = arg_get_lit(ctx, 10); + CLIParserFree(ctx); + + bool has_records = (urilen || textlen || aarlen); + int sources = (has_records ? 1 : 0) + (rawlen ? 1 : 0) + (fnlen ? 1 : 0); + if (sources == 0) { + PrintAndLogEx(ERR, "Nothing to write. See `" _YELLOW_("hf mfu ndefwrite -h") "`"); + return PM3_EINVARG; + } + if (sources > 1) { + PrintAndLogEx(ERR, "Use either the record options, -d or -f, not a mix of them"); + return PM3_EINVARG; + } + + if ((langlen != 0) && (textlen == 0)) { + PrintAndLogEx(WARNING, "--lang only applies to a text record, ignoring"); + } + + switch (ak_len) { + case 0: + break; + case 4: + has_pwd = true; + break; + case 16: + has_auth_key = true; + break; + default: + PrintAndLogEx(WARNING, "ERROR: Key is incorrect length\n"); + return PM3_EINVARG; + } + + if (use_schann && (has_auth_key == false)) { + PrintAndLogEx(WARNING, "Secure channel must be called with key"); + return PM3_EINVARG; + } + + // ------------------------------------------------------------------ + // build the TLV block that goes onto the tag + // ------------------------------------------------------------------ + uint8_t tlv[MFU_NDEF_MAX_BYTES] = {0}; + size_t tlv_len = 0; + int res = PM3_SUCCESS; + + if (has_records) { + + NDEFRecordDesc_t recs[3] = {{0}}; + size_t count = 0; + + uint8_t p_uri[sizeof(uri) + 1] = {0}; + size_t p_uri_len = 0; + if (urilen) { + res = NDEFEncodePayloadURI(uri, p_uri, sizeof(p_uri), &p_uri_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode URI record"); + return res; + } + recs[count].tnf = tnfWellKnownRecord; + recs[count].type = (const uint8_t *)NDEF_TYPE_URI; + recs[count].typeLen = strlen(NDEF_TYPE_URI); + recs[count].payload = p_uri; + recs[count].payloadLen = p_uri_len; + count++; + } + + uint8_t p_text[sizeof(text) + sizeof(lang) + 1] = {0}; + size_t p_text_len = 0; + if (textlen) { + res = NDEFEncodePayloadText(text, lang, p_text, sizeof(p_text), &p_text_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode Text record"); + return res; + } + recs[count].tnf = tnfWellKnownRecord; + recs[count].type = (const uint8_t *)NDEF_TYPE_TEXT; + recs[count].typeLen = strlen(NDEF_TYPE_TEXT); + recs[count].payload = p_text; + recs[count].payloadLen = p_text_len; + count++; + } + + uint8_t p_aar[sizeof(aar)] = {0}; + size_t p_aar_len = 0; + if (aarlen) { + res = NDEFEncodePayloadAAR(aar, p_aar, sizeof(p_aar), &p_aar_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode Android Application Record"); + return res; + } + recs[count].tnf = tnfExternalRecord; + recs[count].type = (const uint8_t *)NDEF_ANDROID_AAR; + recs[count].typeLen = strlen(NDEF_ANDROID_AAR); + recs[count].payload = p_aar; + recs[count].payloadLen = p_aar_len; + count++; + } + + uint8_t msg[MFU_NDEF_MAX_BYTES] = {0}; + size_t msg_len = 0; + res = NDEFEncodeMessage(recs, count, msg, sizeof(msg), &msg_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode NDEF message"); + return res; + } + + res = NDEFEncodeTLV(msg, msg_len, tlv, sizeof(tlv), &tlv_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to wrap NDEF message in a TLV container"); + return res; + } + + } else { + + uint8_t *src = raw; + size_t src_len = (size_t)rawlen; + uint8_t *dump = NULL; + + if (fnlen) { + size_t bytes_read = 0; + res = pm3_load_dump(filename, (void **)&dump, &bytes_read, sizeof(raw)); + if (res != PM3_SUCCESS) { + return res; + } + src = dump; + src_len = bytes_read; + } + + if (src_len == 0) { + PrintAndLogEx(ERR, "No NDEF data supplied"); + free(dump); + return PM3_EINVARG; + } + + // is it a TLV block already? + bool is_tlv = false; + switch (src[0]) { + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0xFD: + case 0xFE: + is_tlv = true; + break; + default: + break; + } + + if (is_tlv) { + if (src_len > sizeof(tlv)) { + PrintAndLogEx(ERR, "NDEF data too large, %zu bytes", src_len); + free(dump); + return PM3_EINVARG; + } + memcpy(tlv, src, src_len); + tlv_len = src_len; + } else { + if (verbose) { + PrintAndLogEx(INFO, "Raw data is not TLV wrapped, adding container"); + } + res = NDEFEncodeTLV(src, src_len, tlv, sizeof(tlv), &tlv_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to wrap NDEF message in a TLV container"); + free(dump); + return res; + } + } + + free(dump); + } + + if (verbose) { + PrintAndLogEx(INFO, "TLV block [%zu]...", tlv_len); + print_hex_noascii_break(tlv, tlv_len, 32); + } + + // ------------------------------------------------------------------ + // find the tag and check the message fits + // ------------------------------------------------------------------ + uint64_t tagtype = GetHF14AMfU_Type(); + if (tagtype == MFU_TT_UL_ERROR) { + PrintAndLogEx(WARNING, "No Ultralight / NTAG based tag found"); + return PM3_ESOFT; + } + + ul_print_type(tagtype, 0); + + if (swap_endian) { + if (ak_len == 16) { + if (((tagtype & MFU_TT_UL_C) == MFU_TT_UL_C)) { + auth_key_ptr = SwapEndian64(authenticationkey, ak_len, 8); + } else if (((tagtype & MFU_TT_UL_AES) == MFU_TT_UL_AES)) { + auth_key_ptr = SwapEndian64(authenticationkey, ak_len, 16); + } + } else if (ak_len == 4) { + auth_key_ptr = SwapEndian64(authenticationkey, ak_len, 4); + } + } + + if (ul_auth_select(&card, tagtype, (has_auth_key || has_pwd), auth_key_ptr, pack, sizeof(pack), use_schann) == PM3_ESOFT) { + return PM3_ESOFT; + } + + // read blocks 0..3 to get at the Capability Container + int status = ul_read(0, data, sizeof(data), use_schann); + if (status <= 0) { + DropField(); + PrintAndLogEx(ERR, "Error: tag didnt answer to READ"); + return PM3_ESOFT; + } + + if (status != 16) { + DropField(); + PrintAndLogEx(ERR, "Error: tag returned %d bytes, need 16 to read the NDEF Container", status); + return PM3_ESOFT; + } + + if (ndef_print_CC(data + 12) == PM3_ESOFT) { + DropField(); + PrintAndLogEx(ERR, "Error: tag didnt contain a NDEF Container"); + PrintAndLogEx(HINT, "Hint: the tag needs to be NDEF formatted first"); + return PM3_ESOFT; + } + + // size of the NDEF data area + uint16_t ndef_size = ndef_get_maxsize(data + 12); + if (ndef_size == 0) { + DropField(); + PrintAndLogEx(ERR, "Error: tag announces an empty NDEF data area"); + return PM3_ESOFT; + } + + // The data area starts at block 4, so cap the announced size to what the + // identified tag can physically hold. UL_MEMORY_ARRAY holds the last valid + // block number, hence the +1. + for (uint8_t idx = 1; idx < ARRAYLEN(UL_TYPES_ARRAY); idx++) { + if ((tagtype & UL_TYPES_ARRAY[idx]) == UL_TYPES_ARRAY[idx]) { + + int avail = (UL_MEMORY_ARRAY[idx] + 1 - MFU_NDEF_FIRST_BLOCK) * MFU_BLOCK_SIZE; + if (avail > 0 && ndef_size > avail) { + PrintAndLogEx(INFO, "NDEF data area (%u bytes) is larger than the tag (%d bytes), using tag size" + , ndef_size + , avail + ); + ndef_size = avail; + } + break; + } + } + + // MLEN can announce up to 2040 bytes but WRITE addresses blocks with a single + // byte, so block 255 is the last one we can reach. + if (ndef_size > MFU_NDEF_MAX_BYTES) { + ndef_size = MFU_NDEF_MAX_BYTES; + } + + if (tlv_len > ndef_size) { + DropField(); + PrintAndLogEx(ERR, "NDEF message is too large for this tag"); + PrintAndLogEx(ERR, " message..... " _RED_("%zu") " bytes", tlv_len); + PrintAndLogEx(ERR, " tag holds... " _GREEN_("%u") " bytes", ndef_size); + return PM3_EINVARG; + } + + DropField(); + + // ------------------------------------------------------------------ + // write it out, one block at a time + // ------------------------------------------------------------------ + uint8_t keytype = 0; + if (has_auth_key || has_pwd) { + if ((tagtype & MFU_TT_UL_C) == MFU_TT_UL_C) { + keytype = 1; // UL_C auth + } else if ((tagtype & MFU_TT_UL_AES) == MFU_TT_UL_AES) { + keytype = 3; // UL_AES auth + } else { + keytype = 2; // UL_EV1/NTAG auth + } + } + + // pad the tail so the last block is complete + size_t padded_len = (tlv_len + (MFU_BLOCK_SIZE - 1)) & ~((size_t)MFU_BLOCK_SIZE - 1); + uint16_t blocks = (uint16_t)(padded_len / MFU_BLOCK_SIZE); + + PrintAndLogEx(INFO, "Writing " _YELLOW_("%u") " blocks ( %zu bytes ) from block " _YELLOW_("%d") + , blocks + , padded_len + , MFU_NDEF_FIRST_BLOCK + ); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort"); + + for (uint16_t i = 0; i < blocks; i++) { + + if (kbd_enter_pressed()) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, "aborted via keyboard!"); + if (i > 0) { + PrintAndLogEx(WARNING, "tag holds a partially written NDEF message, blocks %d..%u" + , MFU_NDEF_FIRST_BLOCK + , MFU_NDEF_FIRST_BLOCK + i - 1 + ); + } + return PM3_EOPABORTED; + } + + uint8_t blockno = (uint8_t)(MFU_NDEF_FIRST_BLOCK + i); + + res = mfu_write_block(tlv + (i * MFU_BLOCK_SIZE), MFU_BLOCK_SIZE, keytype, auth_key_ptr, blockno, use_schann); + if (res != PM3_SUCCESS) { + PrintAndLogEx(NORMAL, ""); + + // a protected tag simply stops answering, so the write times out + // rather than coming back with an explicit error + if (res == PM3_ETIMEOUT) { + PrintAndLogEx(FAILED, "Write block %u ( " _RED_("timeout") " )", blockno); + } else { + PrintAndLogEx(FAILED, "Write block %u ( " _RED_("fail") " )", blockno); + } + PrintAndLogEx(HINT, "Hint: Check password / key!"); + + if (i > 0) { + PrintAndLogEx(WARNING, "tag holds a partially written NDEF message, blocks %d..%u" + , MFU_NDEF_FIRST_BLOCK + , blockno - 1 + ); + } + return PM3_ESOFT; + } + + PrintAndLogEx(INPLACE, "Block %u / %u", i + 1, blocks); + } + + DropField(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "Write ( " _GREEN_("ok") " )"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf mfu ndefread") "` to verify"); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} // utility function. Retrieves emulator memory static int GetMfuDumpFromEMul(mfu_dump_t **buf) { @@ -8274,6 +8699,7 @@ static command_t CommandTable[] = { {"incr", CmdHF14AMfUIncr, IfPm3Iso14443a, "Increments Ev1/NTAG counter"}, {"info", CmdHF14AMfUInfo, IfPm3Iso14443a, "Tag information"}, {"ndefread", CmdHF14MfuNDEFRead, IfPm3Iso14443a, "Prints NDEF records from card"}, + {"ndefwrite", CmdHF14MfuNDEFWrite, IfPm3Iso14443a, "Write NDEF records to card"}, {"rdbl", CmdHF14AMfURdBl, IfPm3Iso14443a, "Read block"}, {"restore", CmdHF14AMfURestore, IfPm3Iso14443a, "Restore a dump file onto a tag"}, {"tamper", CmdHF14MfUTamper, IfPm3Iso14443a, "NTAG 213TT - Configure the tamper feature"}, diff --git a/client/src/cmdhfmfu.h b/client/src/cmdhfmfu.h index d283665ad..1eb49488b 100644 --- a/client/src/cmdhfmfu.h +++ b/client/src/cmdhfmfu.h @@ -57,7 +57,8 @@ int mfu_get_version_uid(uint8_t *version, uint8_t *uid); int mfuc_test_authentication_support(void); int CmdHFMFUltra(const char *Cmd); -int CmdHF14MfuNDEFRead(const char *Cmd); +int CmdHF14MfuNDEFRead(const char *Cmd); // used by cmdnfc.c +int CmdHF14MfuNDEFWrite(const char *Cmd); // used by cmdnfc.c int CmdHF14MfUTamper(const char *Cmd); #define MFU_TT_UNKNOWN 0x0ULL diff --git a/client/src/cmdnfc.c b/client/src/cmdnfc.c index fe58e5aba..67dfa2720 100644 --- a/client/src/cmdnfc.c +++ b/client/src/cmdnfc.c @@ -178,6 +178,183 @@ static int CmdNfcDecode(const char *Cmd) { return res; } +static int CmdNfcEncode(const char *Cmd) { + +#ifndef MAX_NDEF_LEN +#define MAX_NDEF_LEN 2048 +#endif + + CLIParserContext *ctx; + CLIParserInit(&ctx, "nfc encode", + "Encode NFC Data Exchange Format (NDEF) records.\n" + "Combine several options to build a multi record message,\n" + "records are added in the order listed below.\n" + "\n" + "By default a bare NDEF message is emitted. Use `--tlv` to wrap it in a\n" + "NFC Forum Type 2 tag container ( 03 ... FE ), which is what the\n" + "`-d` parameter of `hf mf ndefwrite` and `hf 14a ndefwrite` expects.\n" + "`hf mfu ndefwrite` adds the container itself, so it does not need `--tlv`", + "nfc encode --uri https://proxmark.com\n" + "nfc encode --uri tel:+123456789\n" + "nfc encode --text \"hello world\"\n" + "nfc encode --aar com.example.app\n" + "nfc encode --uri https://proxmark.com --aar com.example.app\n" + "nfc encode --uri https://proxmark.com --tlv -f myfilename" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "uri", "", "URI record. URL, tel:, mailto:, ..."), + arg_str0(NULL, "text", "", "Text record"), + arg_str0(NULL, "lang", "", "language code for the text record (default: en)"), + arg_str0(NULL, "aar", "", "Android Application Record, ie an app package name"), + arg_lit0(NULL, "tlv", "wrap message in a NFC Forum Type 2 tag TLV container"), + arg_str0("f", "file", "", "save raw bytes to file"), + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + // CLIParamStrToBuf copies the trailing NUL as well but only rejects lengths + // strictly above maxdatalen, so leave room for that byte + int urilen = 0; + char uri[1024] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)uri, sizeof(uri) - 1, &urilen); + + int textlen = 0; + char text[1024] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)text, sizeof(text) - 1, &textlen); + + int langlen = 0; + char lang[32] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)lang, sizeof(lang) - 1, &langlen); + + int aarlen = 0; + char aar[256] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)aar, sizeof(aar) - 1, &aarlen); + + bool use_tlv = arg_get_lit(ctx, 5); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + + bool verbose = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if ((urilen == 0) && (textlen == 0) && (aarlen == 0)) { + PrintAndLogEx(ERR, "Must specify at least one record. See `" _YELLOW_("nfc encode -h") "`"); + return PM3_EINVARG; + } + + if ((langlen != 0) && (textlen == 0)) { + PrintAndLogEx(WARNING, "`--lang` only applies to a text record, ignoring"); + } + + NDEFRecordDesc_t recs[3] = {{0}}; + size_t count = 0; + int res = PM3_SUCCESS; + + uint8_t p_uri[sizeof(uri) + 1] = {0}; + size_t p_uri_len = 0; + if (urilen) { + res = NDEFEncodePayloadURI(uri, p_uri, sizeof(p_uri), &p_uri_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode URI record"); + return res; + } + recs[count].tnf = tnfWellKnownRecord; + recs[count].type = (const uint8_t *)NDEF_TYPE_URI; + recs[count].typeLen = strlen(NDEF_TYPE_URI); + recs[count].payload = p_uri; + recs[count].payloadLen = p_uri_len; + count++; + } + + uint8_t p_text[sizeof(text) + sizeof(lang) + 1] = {0}; + size_t p_text_len = 0; + if (textlen) { + res = NDEFEncodePayloadText(text, lang, p_text, sizeof(p_text), &p_text_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode Text record"); + return res; + } + recs[count].tnf = tnfWellKnownRecord; + recs[count].type = (const uint8_t *)NDEF_TYPE_TEXT; + recs[count].typeLen = strlen(NDEF_TYPE_TEXT); + recs[count].payload = p_text; + recs[count].payloadLen = p_text_len; + count++; + } + + uint8_t p_aar[sizeof(aar)] = {0}; + size_t p_aar_len = 0; + if (aarlen) { + res = NDEFEncodePayloadAAR(aar, p_aar, sizeof(p_aar), &p_aar_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode Android Application Record"); + return res; + } + recs[count].tnf = tnfExternalRecord; + recs[count].type = (const uint8_t *)NDEF_ANDROID_AAR; + recs[count].typeLen = strlen(NDEF_ANDROID_AAR); + recs[count].payload = p_aar; + recs[count].payloadLen = p_aar_len; + count++; + } + + uint8_t msg[MAX_NDEF_LEN] = {0}; + size_t msglen = 0; + res = NDEFEncodeMessage(recs, count, msg, sizeof(msg), &msglen); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode NDEF message"); + return res; + } + + uint8_t tlv[MAX_NDEF_LEN] = {0}; + uint8_t *out = msg; + size_t outlen = msglen; + + if (use_tlv) { + size_t tlvlen = 0; + res = NDEFEncodeTLV(msg, msglen, tlv, sizeof(tlv), &tlvlen); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to wrap NDEF message in a TLV container"); + return res; + } + out = tlv; + outlen = tlvlen; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("NDEF encoding") " ----------------"); + PrintAndLogEx(SUCCESS, "Records......... " _YELLOW_("%zu"), count); + PrintAndLogEx(SUCCESS, "Message size.... " _YELLOW_("%zu") " bytes", msglen); + if (use_tlv) { + PrintAndLogEx(SUCCESS, "TLV size........ " _YELLOW_("%zu") " bytes", outlen); + } + + PrintAndLogEx(NORMAL, ""); + if (outlen > 64) { + print_hex_noascii_break(out, outlen, 32); + } else { + PrintAndLogEx(SUCCESS, _GREEN_("%s"), sprint_hex_inrow(out, outlen)); + } + + if (fnlen) { + saveFile(filename, ".bin", out, outlen); + } + + // round trip it back through the decoder, so the user can see what a reader will see + if (use_tlv) { + NDEFDecodeAndPrint(out, outlen, verbose); + } else { + NDEFRecordsDecodeAndPrint(out, outlen, verbose); + } + + return PM3_SUCCESS; +} + static int CmdNFCType1Read(const char *Cmd) { return CmdHFTopazInfo(Cmd); } @@ -210,6 +387,10 @@ static int CmdNFCType2Read(const char *Cmd) { return CmdHF14MfuNDEFRead(Cmd); } +static int CmdNFCType2Write(const char *Cmd) { + return CmdHF14MfuNDEFWrite(Cmd); +} + static int CmdNFCType2Help(const char *Cmd); static command_t CommandNFCType2Table[] = { @@ -217,7 +398,7 @@ static command_t CommandNFCType2Table[] = { {"--------", CmdNFCType2Help, AlwaysAvailable, "-------------- " _CYAN_("NFC Forum Tag Type 2") " ---------------"}, // {"format", CmdNFCType2Format, IfPm3Iso14443a, "format ISO-14443-a tag as NFC Tag"}, {"read", CmdNFCType2Read, IfPm3Iso14443a, "read NFC Forum Tag Type 2"}, -// {"write", CmdNFCType2Write, IfPm3Iso14443a, "write NFC Forum Tag Type 2"}, + {"write", CmdNFCType2Write, IfPm3Iso14443a, "write NFC Forum Tag Type 2"}, {"--------", CmdNFCType2Help, AlwaysAvailable, "--------------------- " _CYAN_("General") " ---------------------"}, {"help", CmdNFCType2Help, AlwaysAvailable, "This help"}, {NULL, NULL, NULL, NULL} @@ -459,7 +640,7 @@ static command_t CommandTable[] = { {"--------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("General") " ---------------------"}, {"help", CmdHelp, AlwaysAvailable, "This help"}, {"decode", CmdNfcDecode, AlwaysAvailable, "Decode NDEF records"}, -// {"encode", CmdNfcEncode, AlwaysAvailable, "Encode NDEF records"}, + {"encode", CmdNfcEncode, AlwaysAvailable, "Encode NDEF records"}, {NULL, NULL, NULL, NULL} }; diff --git a/client/src/nfc/ndef.c b/client/src/nfc/ndef.c index 6acf53cc0..d02f050d5 100644 --- a/client/src/nfc/ndef.c +++ b/client/src/nfc/ndef.c @@ -998,6 +998,17 @@ static int ndefDecodeExternal_record(NDEFHeader_t *ndef) { return PM3_SUCCESS; } + // Android Application Record + if ((ndef->TypeLen == strlen(NDEF_ANDROID_AAR)) && + (strncmp((char *)ndef->Type, NDEF_ANDROID_AAR, ndef->TypeLen) == 0)) { + PrintAndLogEx(INFO + , " Android app... " _GREEN_("%.*s") + , (int)ndef->PayloadLen + , ndef->Payload + ); + return PM3_SUCCESS; + } + PrintAndLogEx(INFO , " URN... " _GREEN_("urn:nfc:ext:%.*s") , (int)ndef->TypeLen @@ -1393,3 +1404,257 @@ int NDEFGetTotalLength(uint8_t *ndef, size_t ndeflen, size_t *outlen) { *outlen = idx; return PM3_SUCCESS; } + +//----------------------------------------------------------------------------- +// NDEF encoding +//----------------------------------------------------------------------------- + +// Find the longest matching abbreviation from URI_s[]. +// Returns the prefix code and sets prefixLen to how many bytes of uri it covers. +// Code 0x00 means "no abbreviation", the whole string gets stored verbatim. +static uint8_t ndefEncodeURIPrefix(const char *uri, size_t *prefixLen) { + + uint8_t code = 0; + size_t best = 0; + + for (uint8_t i = 1; i < ARRAYLEN(URI_s); i++) { + size_t n = strlen(URI_s[i]); + if ((n > best) && (strncmp(uri, URI_s[i], n) == 0)) { + best = n; + code = i; + } + } + + *prefixLen = best; + return code; +} + +int NDEFEncodePayloadURI(const char *uri, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((uri == NULL) || (buf == NULL) || (outLen == NULL)) { + return PM3_EINVARG; + } + + size_t uri_len = strlen(uri); + if (uri_len == 0) { + return PM3_EINVARG; + } + + size_t prefix_len = 0; + uint8_t code = ndefEncodeURIPrefix(uri, &prefix_len); + + size_t n = 1 + (uri_len - prefix_len); + if (n > bufLen) { + return PM3_EOVFLOW; + } + + buf[0] = code; + memcpy(buf + 1, uri + prefix_len, uri_len - prefix_len); + + *outLen = n; + return PM3_SUCCESS; +} + +int NDEFEncodePayloadText(const char *text, const char *lang, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((text == NULL) || (buf == NULL) || (outLen == NULL)) { + return PM3_EINVARG; + } + + if ((lang == NULL) || (strlen(lang) == 0)) { + lang = "en"; + } + + size_t lang_len = strlen(lang); + size_t text_len = strlen(text); + + // the status byte carries the language code length in its lower 6 bits + if (lang_len > 0x3F) { + return PM3_EINVARG; + } + + size_t n = 1 + lang_len + text_len; + if (n > bufLen) { + return PM3_EOVFLOW; + } + + // bit 7 clear == UTF-8 + buf[0] = (uint8_t)(lang_len & 0x3F); + memcpy(buf + 1, lang, lang_len); + memcpy(buf + 1 + lang_len, text, text_len); + + *outLen = n; + return PM3_SUCCESS; +} + +int NDEFEncodePayloadAAR(const char *pkg, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((pkg == NULL) || (buf == NULL) || (outLen == NULL)) { + return PM3_EINVARG; + } + + size_t n = strlen(pkg); + if (n == 0) { + return PM3_EINVARG; + } + + if (n > bufLen) { + return PM3_EOVFLOW; + } + + memcpy(buf, pkg, n); + + *outLen = n; + return PM3_SUCCESS; +} + +int NDEFEncodeRecord(const NDEFRecordDesc_t *rec, bool mb, bool me, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((rec == NULL) || (buf == NULL) || (outLen == NULL)) { + return PM3_EINVARG; + } + + if ((rec->typeLen > 255) || (rec->idLen > 255)) { + return PM3_EINVARG; + } + +#if SIZE_MAX > 0xFFFFFFFF + // the long form payload length field is only four bytes wide, a larger payload + // cannot be expressed. unreachable where size_t is no wider than the field. + if (rec->payloadLen > 0xFFFFFFFF) { + return PM3_EINVARG; + } +#endif + + if ((rec->typeLen && (rec->type == NULL)) || + (rec->idLen && (rec->id == NULL)) || + (rec->payloadLen && (rec->payload == NULL))) { + return PM3_EINVARG; + } + + bool sr = (rec->payloadLen < 256); + bool il = (rec->idLen > 0); + + size_t head = 1 + 1 + (sr ? 1 : 4) + (il ? 1 : 0) + rec->typeLen + rec->idLen; + + // keep the total from wrapping before it reaches the bufLen check below + if (rec->payloadLen > SIZE_MAX - head) { + return PM3_EINVARG; + } + + size_t n = head + rec->payloadLen; + if (n > bufLen) { + return PM3_EOVFLOW; + } + + size_t idx = 0; + + // MB | ME | CF | SR | IL | TNF. chunked records are never emitted + buf[idx] = (uint8_t)(rec->tnf & 0x07); + if (mb) { + buf[idx] |= 0x80; + } + if (me) { + buf[idx] |= 0x40; + } + if (sr) { + buf[idx] |= 0x10; + } + if (il) { + buf[idx] |= 0x08; + } + idx++; + + buf[idx++] = (uint8_t)rec->typeLen; + + if (sr) { + buf[idx++] = (uint8_t)rec->payloadLen; + } else { + buf[idx++] = (uint8_t)(rec->payloadLen >> 24); + buf[idx++] = (uint8_t)(rec->payloadLen >> 16); + buf[idx++] = (uint8_t)(rec->payloadLen >> 8); + buf[idx++] = (uint8_t)(rec->payloadLen); + } + + if (il) { + buf[idx++] = (uint8_t)rec->idLen; + } + + if (rec->typeLen) { + memcpy(buf + idx, rec->type, rec->typeLen); + idx += rec->typeLen; + } + + if (rec->idLen) { + memcpy(buf + idx, rec->id, rec->idLen); + idx += rec->idLen; + } + + if (rec->payloadLen) { + memcpy(buf + idx, rec->payload, rec->payloadLen); + idx += rec->payloadLen; + } + + *outLen = idx; + return PM3_SUCCESS; +} + +int NDEFEncodeMessage(const NDEFRecordDesc_t *recs, size_t count, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((recs == NULL) || (buf == NULL) || (outLen == NULL) || (count == 0)) { + return PM3_EINVARG; + } + + size_t idx = 0; + for (size_t i = 0; i < count; i++) { + + size_t n = 0; + int res = NDEFEncodeRecord(&recs[i], (i == 0), (i == (count - 1)), buf + idx, bufLen - idx, &n); + if (res != PM3_SUCCESS) { + return res; + } + idx += n; + } + + *outLen = idx; + return PM3_SUCCESS; +} + +int NDEFEncodeTLV(const uint8_t *msg, size_t msgLen, uint8_t *buf, size_t bufLen, size_t *outLen) { + + if ((msg == NULL) || (buf == NULL) || (outLen == NULL) || (msgLen == 0)) { + return PM3_EINVARG; + } + + // 0xFFFF is reserved, and the terminator has to fit as well + if (msgLen > 0xFFFE) { + return PM3_EINVARG; + } + + // one byte length form up to 254, three byte form above that + size_t hdr = (msgLen < 0xFF) ? 2 : 4; + + size_t n = hdr + msgLen + 1; + if (n > bufLen) { + return PM3_EOVFLOW; + } + + size_t idx = 0; + buf[idx++] = 0x03; + + if (msgLen < 0xFF) { + buf[idx++] = (uint8_t)msgLen; + } else { + buf[idx++] = 0xFF; + buf[idx++] = (uint8_t)(msgLen >> 8); + buf[idx++] = (uint8_t)(msgLen); + } + + memcpy(buf + idx, msg, msgLen); + idx += msgLen; + + buf[idx++] = 0xFE; + + *outLen = idx; + return PM3_SUCCESS; +} diff --git a/client/src/nfc/ndef.h b/client/src/nfc/ndef.h index 31967c61a..ddc78dc9f 100644 --- a/client/src/nfc/ndef.h +++ b/client/src/nfc/ndef.h @@ -77,4 +77,35 @@ typedef struct { int NDEFDecodeAndPrint(uint8_t *ndef, size_t ndefLen, bool verbose); int NDEFRecordsDecodeAndPrint(uint8_t *ndefRecord, size_t ndefRecordLen, bool verbose); int NDEFGetTotalLength(uint8_t *ndef, size_t ndeflen, size_t *outlen); + +// NFC Forum well known types +#define NDEF_TYPE_URI "U" +#define NDEF_TYPE_TEXT "T" + +// Android Application Record, TNF external +#define NDEF_ANDROID_AAR "android.com:pkg" + +// Describes one record to be encoded. +// Field order intentionally matches gst_ndef_record_t in cmdhfgst.c so the two +// can eventually be folded together. +typedef struct { + TypeNameFormat_t tnf; + const uint8_t *type; + size_t typeLen; + const uint8_t *id; + size_t idLen; + const uint8_t *payload; + size_t payloadLen; +} NDEFRecordDesc_t; + +// payload builders +int NDEFEncodePayloadURI(const char *uri, uint8_t *buf, size_t bufLen, size_t *outLen); +int NDEFEncodePayloadText(const char *text, const char *lang, uint8_t *buf, size_t bufLen, size_t *outLen); +int NDEFEncodePayloadAAR(const char *pkg, uint8_t *buf, size_t bufLen, size_t *outLen); + +// record / message / TLV assembly +int NDEFEncodeRecord(const NDEFRecordDesc_t *rec, bool mb, bool me, uint8_t *buf, size_t bufLen, size_t *outLen); +int NDEFEncodeMessage(const NDEFRecordDesc_t *recs, size_t count, uint8_t *buf, size_t bufLen, size_t *outLen); +int NDEFEncodeTLV(const uint8_t *msg, size_t msgLen, uint8_t *buf, size_t bufLen, size_t *outLen); + #endif // _NDEF_H_