From a1053ad70dcf9dc6d2c7677fb554df675de38fe8 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Sat, 29 Aug 2026 20:58:59 +0200 Subject: [PATCH] New individual json dump file formats for ht1, ht2, htS, htU. '.bin' / binary format for hitag has been deprecated --- CHANGELOG.md | 1 + client/src/cmdlfhitag.c | 21 ++++++++-- client/src/cmdlfhitaghts.c | 2 +- client/src/cmdlfhitagu.c | 15 ++++++- client/src/fileutils.c | 80 ++++++++++++++++++++++++++++++++++---- client/src/fileutils.h | 8 +++- 6 files changed, 114 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e8d73c4..19c010eec 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 individual json dump formats for ht2, ht1, htS, htU. No more .bin files either for hitag (@iceman1001) - Change NG: iso14a_raw_cmd_t, convert hf 14a raw, dual-mode reader handler (@iceman1001) - Support for flipper ISO15693.nfc dump files (@iceman100) - Change NG: convert CMD_LF_EM4X50_ESET to em4x50_eset_t (@iceman1001) diff --git a/client/src/cmdlfhitag.c b/client/src/cmdlfhitag.c index 333b92ac8..4a9e567cf 100644 --- a/client/src/cmdlfhitag.c +++ b/client/src/cmdlfhitag.c @@ -1585,7 +1585,7 @@ out: FillFileNameByUID(fptr, data, "-dump", HITAG_UID_SIZE); } - pm3_save_dump(filename, data, HITAG2_MAX_BYTE_SIZE, jsfHitag); + pm3_save_dump(filename, data, HITAG2_MAX_BYTE_SIZE, jsfHitag2); return PM3_SUCCESS; } @@ -1763,13 +1763,15 @@ static int CmdLFHitagSim(const char *Cmd) { CLIParserInit(&ctx, "lf hitag sim", "Simulate Hitag transponder\n" "You need to `lf hitag eload` first", - "lf hitag sim -2" + "lf hitag sim -2\n" + "lf hitag sim -2 -t 30 -> set edge detect threshold to 30" ); void *argtable[] = { arg_param_begin, arg_lit0("1", "ht1", "simulate Hitag 1"), arg_lit0("2", "ht2", "simulate Hitag 2"), + arg_int0("t", "threshold", "", "set edge detect threshold (def: 127)"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -1777,8 +1779,14 @@ static int CmdLFHitagSim(const char *Cmd) { bool use_ht1 = arg_get_lit(ctx, 1); bool use_ht2 = arg_get_lit(ctx, 2); bool use_htm = false; // not implemented yet + int threshold = arg_get_int_def(ctx, 3, 127); CLIParserFree(ctx); + if (threshold < 0 || threshold > 255) { + PrintAndLogEx(ERR, "error, threshold must be 0..255"); + return PM3_EINVARG; + } + if ((use_ht1 + use_ht2 + use_htm) > 1) { PrintAndLogEx(ERR, "error, specify only one Hitag type"); return PM3_EINVARG; @@ -1792,8 +1800,15 @@ static int CmdLFHitagSim(const char *Cmd) { // if (use_ht1) // cmd = CMD_LF_HITAG1_SIMULATE; + // the tag content comes from emulator memory, this only carries the threshold + hitag_sim_t payload = { + .tag_mem_supplied = false, + .rfu = 0, + .threshold = (uint16_t)threshold, + }; + clearCommandBuffer(); - SendCommandNG(cmd, NULL, 0); + SendCommandNG(cmd, (uint8_t *)&payload, sizeof(payload)); return PM3_SUCCESS; } diff --git a/client/src/cmdlfhitaghts.c b/client/src/cmdlfhitaghts.c index 96f5880a8..89cd8c12a 100644 --- a/client/src/cmdlfhitaghts.c +++ b/client/src/cmdlfhitaghts.c @@ -650,7 +650,7 @@ static int CmdLFHitagSDump(const char *Cmd) { FillFileNameByUID(fptr, card->pages[HITAGS_UID_PADR], "-dump", HITAGS_PAGE_SIZE); } - pm3_save_dump(filename, (uint8_t *)card->pages, mem_size, jsfHitag); + pm3_save_dump(filename, (uint8_t *)card->pages, mem_size, jsfHitagS); return PM3_SUCCESS; } diff --git a/client/src/cmdlfhitagu.c b/client/src/cmdlfhitagu.c index a4a338471..b4d02ce5a 100644 --- a/client/src/cmdlfhitagu.c +++ b/client/src/cmdlfhitagu.c @@ -494,6 +494,17 @@ static int CmdLFHitagURead(const char *Cmd) { return PM3_SUCCESS; } +// A Hitag u reports its UID and IC revision separately from its pages, so +// neither survives in the raw dump buffer. saveFileJSON's callback takes only +// the json root, so park them here for the duration of the save. +static uint8_t s_htu_uid[HITAGU_UID_SIZE] = {0}; +static uint8_t s_htu_icr = 0; + +static void htu_json_callback(json_t *root) { + JsonSaveBufAsHexCompact(root, "$.Card.UID", s_htu_uid, sizeof(s_htu_uid)); + JsonSaveBufAsHexCompact(root, "$.Card.ICR", &s_htu_icr, sizeof(s_htu_icr)); +} + static int CmdLFHitagUDump(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "lf hitag htu dump", @@ -587,7 +598,9 @@ static int CmdLFHitagUDump(const char *Cmd) { FillFileNameByUID(fptr, card->uid, "-dump", HITAGU_UID_SIZE); } - pm3_save_dump(filename, (uint8_t *)card->pages, mem_size, jsfHitag); + memcpy(s_htu_uid, card->uid, sizeof(s_htu_uid)); + s_htu_icr = card->icr; + pm3_save_dump_cb(filename, (uint8_t *)card->pages, mem_size, jsfHitagU, htu_json_callback); return PM3_SUCCESS; } diff --git a/client/src/fileutils.c b/client/src/fileutils.c index b5bfe0db8..ebbdc555d 100644 --- a/client/src/fileutils.c +++ b/client/src/fileutils.c @@ -29,6 +29,7 @@ #include "cmdhficlass.h" // pagemap #include "iclass_cmd.h" #include "iso15.h" +#include "hitag.h" #ifdef _WIN32 #include "scandir.h" @@ -668,15 +669,53 @@ int prepareJSON(json_t *root, JSONFileType ftype, uint8_t *data, size_t datalen, } break; } - case jsfHitag: { - uint8_t uid[4] = {0}; - memcpy(uid, data, 4); - JsonSaveStr(root, "FileType", "hitag"); - JsonSaveBufAsHexCompact(root, "$.Card.UID", uid, sizeof(uid)); + // Hitag 1 and Hitag 2 both start with the UID in block 0. Hitag 2 also + // carries its configuration in block 3, so record it when the dump is + // long enough to hold one. + case jsfHitag1: + case jsfHitag2: { + JsonSaveStr(root, "FileType", (ftype == jsfHitag1) ? "hitag1" : "hitag2"); + JsonSaveBufAsHexCompact(root, "$.Card.UID", data, HITAG_UID_SIZE); - for (size_t i = 0; i < (datalen / 4); i++) { + if ((ftype == jsfHitag2) && (datalen >= 16)) { + JsonSaveBufAsHexCompact(root, "$.Card.Config", data + 12, HITAG_BLOCK_SIZE); + } + + JsonSaveInt(root, "$.Card.Blocks", (int)(datalen / HITAG_BLOCK_SIZE)); + + for (size_t i = 0; i < (datalen / HITAG_BLOCK_SIZE); i++) { snprintf(path, sizeof(path), "$.blocks.%zu", i); - JsonSaveBufAsHexCompact(root, path, data + (i * 4), 4); + JsonSaveBufAsHexCompact(root, path, data + (i * HITAG_BLOCK_SIZE), HITAG_BLOCK_SIZE); + } + break; + } + // Hitag S keeps the UID in page 0 and the configuration in page 1 + case jsfHitagS: { + JsonSaveStr(root, "FileType", "hitags"); + JsonSaveBufAsHexCompact(root, "$.Card.UID", data + (HITAGS_UID_PADR * HITAGS_PAGE_SIZE), HITAG_UID_SIZE); + + if (datalen >= ((HITAGS_CONFIG_PADR + 1) * HITAGS_PAGE_SIZE)) { + JsonSaveBufAsHexCompact(root, "$.Card.Config", data + (HITAGS_CONFIG_PADR * HITAGS_PAGE_SIZE), HITAGS_PAGE_SIZE); + } + + JsonSaveInt(root, "$.Card.Blocks", (int)(datalen / HITAGS_PAGE_SIZE)); + + for (size_t i = 0; i < (datalen / HITAGS_PAGE_SIZE); i++) { + snprintf(path, sizeof(path), "$.blocks.%zu", i); + JsonSaveBufAsHexCompact(root, path, data + (i * HITAGS_PAGE_SIZE), HITAGS_PAGE_SIZE); + } + break; + } + // Hitag u has a 6 byte UID and an ICR that the tag reports separately from + // its pages, so neither can be recovered from `data`. The caller supplies + // them through the save callback, see pm3_save_dump_cb(). + case jsfHitagU: { + JsonSaveStr(root, "FileType", "hitagu"); + JsonSaveInt(root, "$.Card.Blocks", (int)(datalen / HITAGU_BLOCK_SIZE)); + + for (size_t i = 0; i < (datalen / HITAGU_BLOCK_SIZE); i++) { + snprintf(path, sizeof(path), "$.blocks.%zu", i); + JsonSaveBufAsHexCompact(root, path, data + (i * HITAGU_BLOCK_SIZE), HITAGU_BLOCK_SIZE); } break; } @@ -2061,7 +2100,20 @@ int loadFileJSONex(const char *preferredName, void *data, size_t maxdatalen, siz goto out; } + // The four Hitag flavours share one block layout on disk and differ only in + // how many blocks they carry, so one reader serves all of them. A file + // written before the per-flavour types existed says only "hitag" and cannot + // say which tag it came from - treat those as Hitag 2 and tell the user. if (!strcmp(ctype, "hitag")) { + PrintAndLogEx(WARNING, "`" _YELLOW_("hitag") "` is the legacy dump type and does not record which tag it came from"); + PrintAndLogEx(INFO, "Reading it as " _YELLOW_("Hitag 2") ", re-dump the tag to get a versioned file"); + } + + if (!strcmp(ctype, "hitag") || + !strcmp(ctype, "hitag1") || + !strcmp(ctype, "hitag2") || + !strcmp(ctype, "hitags") || + !strcmp(ctype, "hitagu")) { size_t sptr = 0; for (int i = 0; i < (maxdatalen / 4); i++) { if (sptr + 4 > maxdatalen) { @@ -3929,6 +3981,20 @@ int pm3_save_dump(const char *fn, uint8_t *d, size_t n, JSONFileType jsft) { return PM3_SUCCESS; } +int pm3_save_dump_cb(const char *fn, uint8_t *d, size_t n, JSONFileType jsft, void (*callback)(json_t *)) { + if (fn == NULL || strlen(fn) == 0) { + return PM3_EINVARG; + } + if (d == NULL || n == 0) { + PrintAndLogEx(INFO, "No data to save, skipping..."); + return PM3_EINVARG; + } + + saveFile(fn, ".bin", d, n); + saveFileJSON(fn, jsft, d, n, callback); + return PM3_SUCCESS; +} + int pm3_save_mf_dump(const char *fn, uint8_t *d, size_t n, JSONFileType jsft) { if (fn == NULL || d == NULL || n == 0) { diff --git a/client/src/fileutils.h b/client/src/fileutils.h index 91afa5cd0..6d4ab7e88 100644 --- a/client/src/fileutils.h +++ b/client/src/fileutils.h @@ -50,7 +50,10 @@ typedef enum { jsfMfc_v2, jsfMfc_v3, jsfMfuMemory, - jsfHitag, + jsfHitag1, + jsfHitag2, + jsfHitagS, + jsfHitagU, jsfIclass, jsf14b, jsf14b_v2, @@ -442,6 +445,9 @@ int pm3_load_dump(const char *fn, void **pdump, size_t *dumplen, size_t maxdumpl * @return PM3_SUCCESS if OK */ int pm3_save_dump(const char *fn, uint8_t *d, size_t n, JSONFileType jsft); +// as pm3_save_dump, but lets the caller add fields the raw dump cannot carry, +// such as the Hitag u UID and ICR which arrive separately from the pages +int pm3_save_dump_cb(const char *fn, uint8_t *d, size_t n, JSONFileType jsft, void (*callback)(json_t *)); int pm3_save_dump_json(const char *fn, uint8_t *d, size_t n, JSONFileType jsft); /** STUB