From ee2d55fafb868e115f8a4f64e20a68ee467964fe Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Mon, 14 Sep 2026 19:48:45 +0200 Subject: [PATCH] hf mfdes: put a DESFire card image in emulator memory Adds the on-device card image a DESFire simulation will read, the client side that packs a dump into it, and eload / esave / eview. The layout is in include/desfire_em.h. Tables grow up from the header, file data grows down from the end, and an allocation only has to leave the two frontiers apart -- so a three-file card spends a few hundred bytes rather than a worst case, and the space between them is what the card has left. The PICC is application 000000 and uses the same struct as any other application, so key settings and keys are always stated against an AID. Delete sets a tombstone rather than compacting, which is not a shortcut: a real card does not reclaim on delete either, measured as 2080 bytes free with zero applications before an experiment and 2560 after FormatPICC. Two size limits, and a reader only ever sees the first. cardsize is what the emulated card claims to hold, so GetFreeMem answers from that and CreateFile will refuse with OUT_OF_EEPROM when it runs out. Without it a card impersonating a 2K part would report 7434 bytes free, which no 2K part does. The image size is what emulator memory physically holds, is the harder limit, and is never visible. cardsize is anchored to the free memory the real card reported when the dump was taken -- observed free plus what we reserve for the same content -- so the emulation answers what its original answered. That is also a check on the reservation rule rather than only a convenience: for the bench card it computed 576 bytes spent, and 1984 + 576 is 2560, exactly what that card reports when formatted. Reservation follows what CommitTransaction covers. Backup data, value and record files each get a shadow region because writes to them are staged until commit; a standard data file writes through and does not. Sizes round to a 32 byte granule, which is the granule a real card allocates in. It deliberately does not reproduce NXP's allocator -- that is undocumented and does not fit a simple model, a declared 1024 byte record file costs 1088 on silicon -- so what matters is that the figure is self-consistent and shrinks as the reader writes. No new device command. Emulator memory is one shared region, so CMD_HF_MIFARE_EML_MEMSET and BIG_BUF_EML already reach it, and both inherit the bounds checking those paths gained earlier. Verified with the client only, no simulation yet: packing a dump taken from a DESFire EV2 and walking it back reproduces every field including file contents byte for byte, the same round trip through the device via eload and esave is identical, and an image too large is refused by name rather than truncated -- 'Out of emulator memory laying out AID 112233 file 00: needs 4066 more bytes'. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + client/CMakeLists.txt | 1 + client/Makefile | 3 +- client/experimental_lib/CMakeLists.txt | 1 + client/src/cmdhfmfdes.c | 222 ++++++++ client/src/mifare/desfireem.c | 685 +++++++++++++++++++++++++ client/src/mifare/desfireem.h | 61 +++ doc/commands.json | 45 +- doc/commands.md | 3 + include/desfire_em.h | 284 ++++++++++ 10 files changed, 1303 insertions(+), 3 deletions(-) create mode 100644 client/src/mifare/desfireem.c create mode 100644 client/src/mifare/desfireem.h create mode 100644 include/desfire_em.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 584b2f936..589884e13 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 `hf mfdes eload/esave/eview` - load a DESFire card dump into emulator memory (@iceman1001) - Fixed `PLATFORM=PM3ICOPYX` - builds again, it has no FPGA power switch and the AT91 GPIO layer used the pin unconditionally (@iceman1001) - Fixed `fpga_compress` - PM3ULTIMATE builds again, its bitstreams are an exact multiple of the interleave size (@iceman1001) - Fixed `hf legic eload` - an upload after a command that left a different FPGA bitstream loaded now writes the whole image (@iceman1001) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 19579482e..d19a0b23d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -381,6 +381,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/gen4.c ${PM3_ROOT}/client/src/mifare/lrpcrypto.c ${PM3_ROOT}/client/src/mifare/desfirecrypto.c + ${PM3_ROOT}/client/src/mifare/desfireem.c ${PM3_ROOT}/client/src/mifare/desfiresecurechan.c ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c diff --git a/client/Makefile b/client/Makefile index 7d38fb544..4084332f7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -890,11 +890,12 @@ SRCS = mifare/aiddesfire.c \ lua_bitlib.c \ mifare/lrpcrypto.c \ mifare/desfirecrypto.c \ + mifare/desfireem.c \ mifare/desfirecore.c \ mifare/desfiresecurechan.c \ mifare/desfiretest.c \ mifare/gallaghercore.c \ - mifare/gallaghertest.c \ + mifare/gallaghertest.c \ mifare/mad.c \ mifare/mad_test.c \ mifare/mfkey.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 78a76f9c7..1d04ffb44 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -307,6 +307,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/gen4.c ${PM3_ROOT}/client/src/mifare/lrpcrypto.c ${PM3_ROOT}/client/src/mifare/desfirecrypto.c + ${PM3_ROOT}/client/src/mifare/desfireem.c ${PM3_ROOT}/client/src/mifare/desfiresecurechan.c ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index fe4cb1aa8..230f797d9 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -52,6 +52,7 @@ #include "generator.h" #include "mifare/aiddesfire.h" #include "mifare/prime.h" +#include "mifare/desfireem.h" // emulator memory card image #include "util.h" #include "crypto/originality.h" @@ -9042,6 +9043,224 @@ static void DesfireViewPrintApp(const desfire_dump_app_t *app) { } } +static int CmdHF14ADesELoad(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes eload", + "Load a DESFire card dump into emulator memory.\n" + "The dump is packed into the on-device card image, see doc/mfdes_dump_format.md", + "hf mfdes eload -f hf-mfdes-01020304050607-dump.json"); + + void *argtable[] = { + arg_param_begin, + arg_str1("f", "file", "", "Filename of dump"), + arg_lit0("v", "verbose", "Verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + bool verbose = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + desfire_dump_t *dump = calloc(1, sizeof(desfire_dump_t)); + if (dump == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + return PM3_EMALLOC; + } + + size_t dlen = 0; + int res = loadFileJSON(filename, dump, sizeof(desfire_dump_t), &dlen, NULL); + if (res != PM3_SUCCESS) { + free(dump); + return res; + } + + if (dlen != sizeof(desfire_dump_t)) { + PrintAndLogEx(ERR, "`" _YELLOW_("%s") "` is not a DESFire card dump", filename); + desfire_dump_free(dump); + free(dump); + return PM3_EINVARG; + } + + // the device tells us how much emulator memory it has, so a platform with + // more of it holds a bigger card with no change here + size_t emsize = g_conn.em_size; + if (emsize == 0) { + PrintAndLogEx(ERR, "Device did not report an emulator memory size"); + desfire_dump_free(dump); + free(dump); + return PM3_EDEVNOTSUPP; + } + + uint8_t *img = calloc(emsize, sizeof(uint8_t)); + if (img == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + desfire_dump_free(dump); + free(dump); + return PM3_EMALLOC; + } + + size_t used = 0; + res = desfire_em_pack(dump, img, emsize, &used); + desfire_dump_free(dump); + free(dump); + + if (res != PM3_SUCCESS) { + free(img); + return res; + } + + if (verbose) { + desfire_em_print(img, emsize); + } + + res = desfire_em_upload(img, emsize); + free(img); + + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(SUCCESS, "Done!"); + PrintAndLogEx(HINT, "Hint: try " _YELLOW_("`hf mfdes eview`") " to verify"); + return PM3_SUCCESS; +} + +static int CmdHF14ADesESave(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes esave", + "Save the card image in emulator memory to a dump file.\n" + "Shows what a reader left behind if one has been talking to the simulation", + "hf mfdes esave -f myfile"); + + void *argtable[] = { + arg_param_begin, + arg_str0("f", "file", "", "Filename, if no UID will be used as filename"), + arg_lit0("v", "verbose", "Verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + bool verbose = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + size_t emsize = g_conn.em_size; + if (emsize == 0) { + PrintAndLogEx(ERR, "Device did not report an emulator memory size"); + return PM3_EDEVNOTSUPP; + } + + uint8_t *img = calloc(emsize, sizeof(uint8_t)); + if (img == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + return PM3_EMALLOC; + } + + int res = desfire_em_download(img, emsize); + if (res != PM3_SUCCESS) { + free(img); + return res; + } + + if (verbose) { + desfire_em_print(img, emsize); + } + + desfire_dump_t *dump = calloc(1, sizeof(desfire_dump_t)); + if (dump == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + free(img); + return PM3_EMALLOC; + } + + res = desfire_em_unpack(img, emsize, dump); + free(img); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(HINT, "Hint: emulator memory holds no DESFire card image, try " _YELLOW_("`hf mfdes eload`")); + free(dump); + return res; + } + + if (fnlen < 1) { + if (dump->card_info.uidlen == 0) { + PrintAndLogEx(WARNING, "No UID to build a filename from, use " _YELLOW_("-f ")); + desfire_dump_free(dump); + free(dump); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "Using UID as filename"); + strcat(filename, "hf-mfdes-"); + FillFileNameByUID(filename, dump->card_info.uid, "-dump", dump->card_info.uidlen); + } + + pm3_save_dump_json(filename, (uint8_t *)dump, sizeof(desfire_dump_t), jsfMfDesfire_v1); + + desfire_dump_free(dump); + free(dump); + return PM3_SUCCESS; +} + +static int CmdHF14ADesEView(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes eview", + "Show the DESFire card image currently in emulator memory", + "hf mfdes eview"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("v", "verbose", "Also print every application and file"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool verbose = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + size_t emsize = g_conn.em_size; + if (emsize == 0) { + PrintAndLogEx(ERR, "Device did not report an emulator memory size"); + return PM3_EDEVNOTSUPP; + } + + uint8_t *img = calloc(emsize, sizeof(uint8_t)); + if (img == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + return PM3_EMALLOC; + } + + int res = desfire_em_download(img, emsize); + if (res != PM3_SUCCESS) { + free(img); + return res; + } + + PrintAndLogEx(NORMAL, ""); + desfire_em_print(img, emsize); + + if (verbose) { + desfire_dump_t *dump = calloc(1, sizeof(desfire_dump_t)); + if (dump != NULL) { + if (desfire_em_unpack(img, emsize, dump) == PM3_SUCCESS) { + DesfireViewPrintApp(&dump->picc); + for (uint8_t i = 0; i < dump->appcount && i < DESFIRE_MAX_APP_COUNT; i++) { + DesfireViewPrintApp(&dump->app[i]); + } + } + desfire_dump_free(dump); + free(dump); + } + } + + free(img); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + static int CmdHF14ADesView(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes view", @@ -10796,6 +11015,9 @@ static command_t CommandTable[] = { {"lsfiles", CmdHF14ADesLsFiles, IfPm3Iso14443a, "Show all files list"}, {"dump", CmdHF14ADesDump, IfPm3Iso14443a, "Dump all files"}, {"view", CmdHF14ADesView, AlwaysAvailable, "Display content from tag dump file"}, + {"eload", CmdHF14ADesELoad, IfPm3Iso14443a, "Upload file into emulator memory"}, + {"esave", CmdHF14ADesESave, IfPm3Iso14443a, "Save emulator memory to file"}, + {"eview", CmdHF14ADesEView, IfPm3Iso14443a, "View emulator memory"}, {"createfile", CmdHF14ADesCreateFile, IfPm3Iso14443a, "Create Standard/Backup File"}, {"createvaluefile", CmdHF14ADesCreateValueFile, IfPm3Iso14443a, "Create Value File"}, {"createrecordfile", CmdHF14ADesCreateRecordFile, IfPm3Iso14443a, "Create Linear/Cyclic Record File"}, diff --git a/client/src/mifare/desfireem.c b/client/src/mifare/desfireem.c new file mode 100644 index 000000000..7a71a4b4b --- /dev/null +++ b/client/src/mifare/desfireem.c @@ -0,0 +1,685 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Pack a DESFire card image for emulator memory, and read one back. +// Layout and rationale live in include/desfire_em.h. +//----------------------------------------------------------------------------- + +#include "desfireem.h" + +#include +#include +#include "commonutil.h" +#include "ui.h" +#include "desfirecrypto.h" // desfire_get_key_length +#include "mifarehost.h" // mf_eml_set_mem_xt +#include "comms.h" // g_conn +#include "cmdhw.h" // GetFromDevice + +uint8_t desfire_em_gen_from_version(const uint8_t *versionhw, uint8_t len) { + + if (versionhw == NULL || len < 5) { + return DESFIRE_EM_GEN_UNKNOWN; + } + + // hardware frame: vendor, type, subtype, major, minor, storage, protocol. + // Major and minor are what a reader keys on, see client/src/mifare/prime.c + uint8_t type = versionhw[1]; + uint8_t major = versionhw[3]; + uint8_t minor = versionhw[4]; + + if (minor != 0x00) { + return DESFIRE_EM_GEN_UNKNOWN; + } + + if (type != 0x01) { + return DESFIRE_EM_GEN_UNKNOWN; + } + + switch (major) { + case 0x00: + return DESFIRE_EM_GEN_D40; + case 0x01: + return DESFIRE_EM_GEN_EV1; + case 0x12: + case 0x22: + case 0x42: + return DESFIRE_EM_GEN_EV2; + case 0x30: + return DESFIRE_EM_GEN_LIGHT; + case 0x33: + return DESFIRE_EM_GEN_EV3; + case 0xA0: + return DESFIRE_EM_GEN_DUOX; + default: + return DESFIRE_EM_GEN_UNKNOWN; + } +} + +// NV the GetVersion storage size byte codes: 2^(n>>1) bytes, with the low bit +// meaning "between 2^n and 2^(n+1)". See client/src/mifare/prime.c, and the EV1 +// spec section 9.4.7. +uint32_t desfire_em_nominal_cardsize(const uint8_t *versionhw, uint8_t len) { + + if (versionhw == NULL || len < 6) { + return 0; + } + + uint8_t fsize = versionhw[5]; + if (fsize == 0 || (fsize >> 1) > 24) { + return 0; + } + + return 1UL << (fsize >> 1); +} + +const char *desfire_em_gen_str(uint8_t gen) { + static const char *tbl[] = {"unknown", "D40", "EV1", "EV2", "EV3", "Light", "DuoX"}; + return (gen < ARRAYLEN(tbl)) ? tbl[gen] : "unknown"; +} + +// Bytes the committed region of one file occupies, before the shadow. +static uint32_t desfire_em_file_extent(const desfire_dump_file_t *f) { + + switch (f->type) { + case 0x00: + case 0x01: + return f->size; + case 0x02: + // a value file's payload is the 4 byte value, but it is allocated + // and shadowed like anything else, so give it a whole granule + return DESFIRE_EM_GRANULE; + case 0x03: + case 0x04: + // reserve the declared extent, not the records that happen to + // exist, so WriteRecord never has to grow anything + return f->recordsize * f->maxrecords; + default: + return 0; + } +} + +// True when CommitTransaction covers this file type, ie it needs a shadow. +// EV1 spec 9.6.10 names exactly these four; a standard data file writes through. +static bool desfire_em_file_has_shadow(uint8_t type) { + return (type == 0x01 || type == 0x02 || type == 0x03 || type == 0x04); +} + +uint32_t desfire_em_file_reserve(const desfire_dump_file_t *f) { + + uint32_t extent = DESFIRE_EM_ROUNDUP(desfire_em_file_extent(f)); + if (extent == 0) { + return 0; + } + + return desfire_em_file_has_shadow(f->type) ? (extent * 2) : extent; +} + +// How many bytes of a file's committed region carry content, derived from the +// file's own declared shape rather than stored. A card holds the whole file, and +// a record file holds exactly the records that exist. +// +// Not stored because there is nothing to store: once packed, "we read 16 bytes +// of a 32 byte file" and "we read 32 bytes of which 16 were zero" are the same +// image. A dump that saw less than the whole file is an incomplete observation +// of a card, and desfire_em_pack() says so. +static uint32_t desfire_em_content_len(const desfire_dump_file_t *f) { + + if (f->read_ok == false) { + return 0; + } + + switch (f->type) { + case 0x00: + case 0x01: + return f->size; + case 0x03: + case 0x04: + return f->recordsize * f->currecords; + default: + return 0; // a value file carries its payload in the value field + } +} + +static void desfire_em_fill_app(desfire_em_app_t *dst, const desfire_dump_app_t *src) { + + dst->aid[0] = src->aid & 0xFF; + dst->aid[1] = (src->aid >> 8) & 0xFF; + dst->aid[2] = (src->aid >> 16) & 0xFF; + + dst->keysettings = src->keysettings; + dst->numkeysraw = src->numkeysraw; + dst->isofid = src->isofid; + dst->keytype = src->keytype; + + dst->dfnamelen = MIN(src->dfnamelen, (uint8_t)sizeof(dst->dfname)); + memcpy(dst->dfname, src->dfname, dst->dfnamelen); + + dst->flags = 0; + if (src->auth_ok) { + dst->flags |= DESFIRE_EM_APP_AUTHED; + } + if (src->numkeysraw & 0x20) { + dst->flags |= DESFIRE_EM_APP_ISOFIDS; + } +} + +static void desfire_em_fill_file(desfire_em_file_t *dst, const desfire_dump_file_t *src, uint8_t appidx) { + + dst->app = appidx; + dst->num = src->num; + dst->type = src->type; + dst->isofid = src->isofid; + dst->rights = src->accessrights; + + dst->flags = (src->commmode & DESFIRE_EM_FILE_COMM_MASK); + if (src->read_ok == false) { + dst->flags |= DESFIRE_EM_FILE_UNKNOWN; + } + if (src->limitedcredit) { + dst->flags |= DESFIRE_EM_FILE_LIMCREDIT; + } + + switch (src->type) { + case 0x00: + case 0x01: + dst->u.data.size = src->size; + break; + case 0x02: + dst->u.value.lower = src->lowerlimit; + dst->u.value.upper = src->upperlimit; + dst->u.value.value = src->value; + break; + case 0x03: + case 0x04: + dst->u.record.recordsize = src->recordsize; + dst->u.record.maxrecords = src->maxrecords; + dst->u.record.currecords = src->currecords; + break; + default: + break; + } +} + +static void desfire_em_fill_keys(desfire_em_key_t *tbl, uint8_t *n, const desfire_dump_app_t *app, uint8_t appidx) { + + for (uint8_t k = 0; k < DESFIRE_EM_MAX_KEYS; k++) { + + if (app->keys.present[k] == 0 && app->keys.versionknown[k] == 0) { + continue; + } + + desfire_em_key_t *e = &tbl[*n]; + memset(e, 0, sizeof(desfire_em_key_t)); + e->app = appidx; + e->num = k; + e->ver = app->keys.version[k]; + + if (app->keys.versionknown[k]) { + e->flags |= DESFIRE_EM_KEY_VERKNOWN; + } + + if (app->keys.present[k]) { + e->flags |= DESFIRE_EM_KEY_PRESENT; + memcpy(e->key, app->keys.key[k], desfire_get_key_length(app->keytype)); + } + + (*n)++; + } +} + +int desfire_em_pack(const desfire_dump_t *dump, uint8_t *out, size_t outlen, size_t *used) { + + if (dump == NULL || out == NULL || used == NULL) { + return PM3_EINVARG; + } + + *used = 0; + + if (outlen < sizeof(desfire_em_hdr_t) || outlen > UINT16_MAX) { + PrintAndLogEx(ERR, "Emulator memory of %zu bytes cannot hold a card image", outlen); + return PM3_EINVARG; + } + + if (dump->appcount > DESFIRE_EM_MAX_APPS) { + PrintAndLogEx(ERR, "Card has %u applications, the format holds %u", dump->appcount, DESFIRE_EM_MAX_APPS); + return PM3_EOUTOFBOUND; + } + + memset(out, 0, outlen); + desfire_em_hdr_t *hdr = (desfire_em_hdr_t *)out; + + hdr->magic = DESFIRE_EM_MAGIC; + hdr->layout = DESFIRE_EM_LAYOUT_VERSION; + hdr->size = outlen; + hdr->generation = desfire_em_gen_from_version(dump->versionhw, dump->versionhwlen); + + hdr->uidlen = MIN(dump->card_info.uidlen, (uint8_t)sizeof(hdr->uid)); + memcpy(hdr->uid, dump->card_info.uid, hdr->uidlen); + memcpy(hdr->atqa, dump->card_info.atqa, sizeof(hdr->atqa)); + hdr->sak = dump->card_info.sak; + hdr->atslen = MIN(dump->card_info.ats_len, (uint8_t)sizeof(hdr->ats)); + memcpy(hdr->ats, dump->card_info.ats, hdr->atslen); + + hdr->versionhwlen = MIN(dump->versionhwlen, (uint8_t)sizeof(hdr->versionhw)); + memcpy(hdr->versionhw, dump->versionhw, hdr->versionhwlen); + hdr->versionswlen = MIN(dump->versionswlen, (uint8_t)sizeof(hdr->versionsw)); + memcpy(hdr->versionsw, dump->versionsw, hdr->versionswlen); + hdr->versionprodlen = MIN(dump->versionprodlen, (uint8_t)sizeof(hdr->versionprod)); + memcpy(hdr->versionprod, dump->versionprod, hdr->versionprodlen); + + hdr->signaturelen = MIN(dump->signaturelen, (uint8_t)sizeof(hdr->signature)); + memcpy(hdr->signature, dump->signature, hdr->signaturelen); + + // app[0] is the PICC, so the table is one longer than the card's own count + uint8_t appcount = dump->appcount + 1; + + uint16_t filecount = 0; + for (uint8_t i = 0; i < dump->appcount; i++) { + filecount += MIN(dump->app[i].filecount, (uint8_t)DESFIRE_EM_MAX_FILES); + } + + // keys are sparse, so count them before laying anything out + uint16_t keycount = 0; + for (uint8_t i = 0; i < appcount; i++) { + const desfire_dump_app_t *a = (i == 0) ? &dump->picc : &dump->app[i - 1]; + for (uint8_t k = 0; k < DESFIRE_EM_MAX_KEYS; k++) { + if (a->keys.present[k] || a->keys.versionknown[k]) { + keycount++; + } + } + } + + hdr->app_off = sizeof(desfire_em_hdr_t); + hdr->file_off = hdr->app_off + (appcount * sizeof(desfire_em_app_t)); + hdr->key_off = hdr->file_off + (filecount * sizeof(desfire_em_file_t)); + + uint32_t tables_end = hdr->key_off + (keycount * sizeof(desfire_em_key_t)); + if (tables_end > outlen) { + PrintAndLogEx(ERR, "Card image tables need %u bytes, emulator memory holds %zu", tables_end, outlen); + return PM3_EOUTOFBOUND; + } + hdr->tables_end = tables_end; + + desfire_em_app_t *apps = (desfire_em_app_t *)(out + hdr->app_off); + desfire_em_file_t *files = (desfire_em_file_t *)(out + hdr->file_off); + desfire_em_key_t *keys = (desfire_em_key_t *)(out + hdr->key_off); + + // file data grows down from the end, tables grow up: the two only have to + // not meet, and what is between them is the card's free memory + uint32_t data_start = outlen; + uint32_t reserved = 0; // NV the card has spent, for GetFreeMem + uint8_t nkeys = 0; + uint16_t nfiles = 0; + + for (uint8_t i = 0; i < appcount; i++) { + + const desfire_dump_app_t *a = (i == 0) ? &dump->picc : &dump->app[i - 1]; + + desfire_em_fill_app(&apps[i], a); + if (i == 0) { + // the PICC is AID 000000 whatever the dump happens to say + memset(apps[i].aid, 0, sizeof(apps[i].aid)); + } + + desfire_em_fill_keys(keys, &nkeys, a, i); + + // a card charges for the application itself, not only for its files + reserved += DESFIRE_EM_ROUNDUP(DESFIRE_EM_APP_OVERHEAD + + (a->numkeys * desfire_get_key_length(a->keytype))); + + if (i == 0) { + continue; // the PICC has no files + } + + uint8_t fc = MIN(a->filecount, (uint8_t)DESFIRE_EM_MAX_FILES); + for (uint8_t n = 0; n < fc; n++) { + + const desfire_dump_file_t *sf = &a->files[n]; + desfire_em_file_t *df = &files[nfiles]; + + desfire_em_fill_file(df, sf, i); + + uint32_t reserve = desfire_em_file_reserve(sf); + if (reserve == 0) { + nfiles++; + continue; // a type we do not reserve for, eg transaction MAC + } + + if (data_start < reserve || (data_start - reserve) < tables_end) { + PrintAndLogEx(ERR, "Out of emulator memory laying out AID %06X file %02X: needs %u more bytes" + , a->aid, sf->num, reserve - (data_start - tables_end)); + return PM3_EOUTOFBOUND; + } + + data_start -= reserve; + reserved += reserve; + + uint32_t extent = desfire_em_file_has_shadow(sf->type) ? (reserve / 2) : reserve; + df->dataoff = data_start; + df->datalen = extent; + df->shadow_off = desfire_em_file_has_shadow(sf->type) ? (data_start + extent) : 0; + + uint32_t content = desfire_em_content_len(sf); + if (content && sf->data) { + + if (sf->datalen < content) { + PrintAndLogEx(WARNING, "AID %06X file %02X holds %u of %u bytes, the rest of the image is zero" + , a->aid, sf->num, sf->datalen, content); + content = sf->datalen; + } + + memcpy(out + df->dataoff, sf->data, MIN(content, extent)); + } + + nfiles++; + } + } + + hdr->appcount = appcount; + hdr->filecount = nfiles; + hdr->keycount = nkeys; + hdr->data_start = data_start; + hdr->reserved = reserved; + + // What the card claims to hold. + // + // Anchor it to the free memory the real card reported when the dump was + // taken: cardsize = observed free + what we reserved for the same content. + // The emulated card then answers GetFreeMem with the number its original + // answered, and that figure falls as a reader writes. Much better than + // deriving from the storage size byte, which understates -- a 2K part codes + // 2048 and hands out 2560. + // + // With no observed figure, fall back to the storage size byte as a floor, + // never below what this image already carries. + uint32_t cardsize; + if (dump->freemem_ok) { + cardsize = dump->freemem + reserved; + } else { + cardsize = desfire_em_nominal_cardsize(dump->versionhw, dump->versionhwlen); + if (cardsize < reserved) { + cardsize = reserved; + } + } + + if (cardsize > outlen) { + // a claim larger than emulator memory is one we cannot honour + cardsize = outlen; + } + hdr->cardsize = cardsize; + + if (reserved > hdr->cardsize) { + PrintAndLogEx(ERR, "Card image needs %u bytes but a %u byte card cannot hold it", reserved, hdr->cardsize); + return PM3_EOUTOFBOUND; + } + + *used = outlen; + return PM3_SUCCESS; +} + +int desfire_em_unpack(const uint8_t *img, size_t imglen, desfire_dump_t *dump) { + + if (img == NULL || dump == NULL || imglen < sizeof(desfire_em_hdr_t)) { + return PM3_EINVARG; + } + + const desfire_em_hdr_t *hdr = (const desfire_em_hdr_t *)img; + + if (hdr->magic != DESFIRE_EM_MAGIC) { + PrintAndLogEx(ERR, "Not a DESFire card image, magic is %08X", hdr->magic); + return PM3_EINVARG; + } + + if (hdr->layout != DESFIRE_EM_LAYOUT_VERSION) { + PrintAndLogEx(ERR, "Card image layout v%u, this client speaks v%u", hdr->layout, DESFIRE_EM_LAYOUT_VERSION); + return PM3_EINVARG; + } + + if (hdr->size > imglen || hdr->tables_end > imglen || hdr->data_start > imglen) { + PrintAndLogEx(ERR, "Card image is internally inconsistent with its %zu bytes", imglen); + return PM3_EINVARG; + } + + memset(dump, 0, sizeof(desfire_dump_t)); + + dump->card_info.uidlen = hdr->uidlen; + memcpy(dump->card_info.uid, hdr->uid, MIN(hdr->uidlen, (uint8_t)sizeof(dump->card_info.uid))); + memcpy(dump->card_info.atqa, hdr->atqa, sizeof(hdr->atqa)); + dump->card_info.sak = hdr->sak; + dump->card_info.ats_len = hdr->atslen; + memcpy(dump->card_info.ats, hdr->ats, MIN(hdr->atslen, (uint8_t)sizeof(dump->card_info.ats))); + + dump->versionhwlen = hdr->versionhwlen; + memcpy(dump->versionhw, hdr->versionhw, sizeof(dump->versionhw)); + dump->versionswlen = hdr->versionswlen; + memcpy(dump->versionsw, hdr->versionsw, sizeof(dump->versionsw)); + dump->versionprodlen = hdr->versionprodlen; + memcpy(dump->versionprod, hdr->versionprod, sizeof(dump->versionprod)); + + dump->signaturelen = hdr->signaturelen; + memcpy(dump->signature, hdr->signature, sizeof(dump->signature)); + + // free memory is the card's, not the frontier gap: that is the number a + // reader gets from GetFreeMem, and it has to look like the part we claim + // to be rather than like however much emulator memory happens to be spare + dump->freemem = (hdr->cardsize > hdr->reserved) ? (hdr->cardsize - hdr->reserved) : 0; + dump->freemem_ok = true; + + const desfire_em_app_t *apps = (const desfire_em_app_t *)(img + hdr->app_off); + const desfire_em_file_t *files = (const desfire_em_file_t *)(img + hdr->file_off); + const desfire_em_key_t *keys = (const desfire_em_key_t *)(img + hdr->key_off); + + if (hdr->appcount == 0 || hdr->appcount > (DESFIRE_EM_MAX_APPS + 1)) { + PrintAndLogEx(ERR, "Card image claims %u applications", hdr->appcount); + return PM3_EINVARG; + } + + dump->appcount = hdr->appcount - 1; // app[0] is the PICC + + for (uint8_t i = 0; i < hdr->appcount; i++) { + + desfire_dump_app_t *a = (i == 0) ? &dump->picc : &dump->app[i - 1]; + const desfire_em_app_t *s = &apps[i]; + + a->aid = s->aid[0] | (s->aid[1] << 8) | (s->aid[2] << 16); + a->isofid = s->isofid; + a->keysettings = s->keysettings; + a->numkeysraw = s->numkeysraw; + a->numkeys = s->numkeysraw & 0x1F; + a->keytype = s->keytype; + a->settings_ok = (s->numkeysraw != 0); + a->auth_ok = ((s->flags & DESFIRE_EM_APP_AUTHED) != 0); + a->dfnamelen = MIN(s->dfnamelen, (uint8_t)sizeof(a->dfname)); + memcpy(a->dfname, s->dfname, a->dfnamelen); + } + + for (uint8_t k = 0; k < hdr->keycount; k++) { + + const desfire_em_key_t *s = &keys[k]; + if (s->app >= hdr->appcount || s->num >= DESFIRE_EM_MAX_KEYS) { + continue; + } + + desfire_dump_app_t *a = (s->app == 0) ? &dump->picc : &dump->app[s->app - 1]; + + if (s->flags & DESFIRE_EM_KEY_VERKNOWN) { + a->keys.versionknown[s->num] = 1; + a->keys.version[s->num] = s->ver; + } + + if (s->flags & DESFIRE_EM_KEY_PRESENT) { + a->keys.present[s->num] = 1; + memcpy(a->keys.key[s->num], s->key, DESFIRE_MAX_KEY_SIZE); + } + } + + for (uint16_t n = 0; n < hdr->filecount; n++) { + + const desfire_em_file_t *s = &files[n]; + if (s->app == 0 || s->app >= hdr->appcount) { + continue; + } + + desfire_dump_app_t *a = &dump->app[s->app - 1]; + if (a->filecount >= DESFIRE_MAX_FILE_COUNT) { + continue; + } + + desfire_dump_file_t *f = &a->files[a->filecount]; + + f->num = s->num; + f->type = s->type; + f->isofid = s->isofid; + f->accessrights = s->rights; + f->commmode = s->flags & DESFIRE_EM_FILE_COMM_MASK; + f->limitedcredit = ((s->flags & DESFIRE_EM_FILE_LIMCREDIT) != 0); + f->settings_ok = true; + f->read_ok = ((s->flags & DESFIRE_EM_FILE_UNKNOWN) == 0); + + switch (s->type) { + case 0x00: + case 0x01: + f->size = s->u.data.size; + break; + case 0x02: + f->lowerlimit = s->u.value.lower; + f->upperlimit = s->u.value.upper; + f->value = s->u.value.value; + break; + case 0x03: + case 0x04: + f->recordsize = s->u.record.recordsize; + f->maxrecords = s->u.record.maxrecords; + f->currecords = s->u.record.currecords; + break; + default: + break; + } + + a->filecount++; + + uint32_t content = MIN(desfire_em_content_len(f), (uint32_t)s->datalen); + if (content == 0) { + continue; + } + + if (s->dataoff + content > imglen) { + PrintAndLogEx(WARNING, "AID %06X file %02X reaches past the image, skipping its contents", a->aid, f->num); + continue; + } + + f->data = calloc(content, sizeof(uint8_t)); + if (f->data == NULL) { + return PM3_EMALLOC; + } + + memcpy(f->data, img + s->dataoff, content); + f->datalen = content; + } + + return PM3_SUCCESS; +} + +void desfire_em_print(const uint8_t *img, size_t imglen) { + + if (img == NULL || imglen < sizeof(desfire_em_hdr_t)) { + return; + } + + const desfire_em_hdr_t *hdr = (const desfire_em_hdr_t *)img; + + PrintAndLogEx(INFO, "--- " _CYAN_("DESFire card image") " -----------------------"); + PrintAndLogEx(SUCCESS, "Generation....... %s", desfire_em_gen_str(hdr->generation)); + PrintAndLogEx(SUCCESS, "UID.............. %s", sprint_hex_inrow(hdr->uid, hdr->uidlen)); + PrintAndLogEx(SUCCESS, "Applications..... %u ( incl. the PICC )", hdr->appcount); + PrintAndLogEx(SUCCESS, "Files............ %u", hdr->filecount); + PrintAndLogEx(SUCCESS, "Keys............. %u", hdr->keycount); + PrintAndLogEx(SUCCESS, "Tables end at.... %u", hdr->tables_end); + PrintAndLogEx(SUCCESS, "Data starts at... %u", hdr->data_start); + PrintAndLogEx(SUCCESS, "Card capacity.... %u bytes, %u spent", hdr->cardsize, hdr->reserved); + PrintAndLogEx(SUCCESS, "Free memory...... " _GREEN_("%u") " bytes ( what a reader sees )" + , (hdr->cardsize > hdr->reserved) ? (hdr->cardsize - hdr->reserved) : 0); + PrintAndLogEx(SUCCESS, "Emulator memory.. %u of %u bytes spare" + , (hdr->data_start > hdr->tables_end) ? (hdr->data_start - hdr->tables_end) : 0 + , hdr->size); +} + +//----------------------------------------------------------------------------- +// Moving an image to and from the device. +// +// No DESFire specific command is needed either way. Emulator memory is one +// region shared by every simulation, so the generic MIFARE setter and the bulk +// emulator download already reach it -- CMD_HF_MIFARE_EML_MEMSET writes through +// emlSet(), which bounds-checks, and BIG_BUF_EML reads back with the device +// clamping to the size it reports in capabilities_t. +//----------------------------------------------------------------------------- + +#define DESFIRE_EM_XFER_WIDTH 16 // emlSet offset is blockno * this + +int desfire_em_upload(const uint8_t *img, size_t imglen) { + + if (img == NULL || imglen == 0) { + return PM3_EINVARG; + } + + if ((imglen % DESFIRE_EM_XFER_WIDTH) != 0) { + PrintAndLogEx(ERR, "Card image of %zu bytes is not a multiple of %d", imglen, DESFIRE_EM_XFER_WIDTH); + return PM3_EINVARG; + } + + // one MEMSET carries a header plus data, so size the chunk from what the + // device told us it can take rather than assuming + size_t perframe = (g_conn.max_cmd_data_size - 8) / DESFIRE_EM_XFER_WIDTH; + if (perframe == 0) { + return PM3_EINVARG; + } + if (perframe > 255) { + perframe = 255; // blockcnt is a uint8_t + } + + size_t blocks = imglen / DESFIRE_EM_XFER_WIDTH; + + PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes to emulator memory", imglen); + + for (size_t b = 0; b < blocks; b += perframe) { + + size_t n = MIN(perframe, blocks - b); + + int res = mf_eml_set_mem_xt((uint8_t *)img + (b * DESFIRE_EM_XFER_WIDTH), b, n, DESFIRE_EM_XFER_WIDTH, 0); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Upload failed at block %zu of %zu", b, blocks); + return res; + } + } + + return PM3_SUCCESS; +} + +int desfire_em_download(uint8_t *img, size_t imglen) { + + if (img == NULL || imglen == 0) { + return PM3_EINVARG; + } + + PrintAndLogEx(INFO, "Downloading " _YELLOW_("%zu") " bytes from emulator memory", imglen); + + if (GetFromDevice(BIG_BUF_EML, img, imglen, 0, NULL, 0, NULL, 2500, false) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + + return PM3_SUCCESS; +} diff --git a/client/src/mifare/desfireem.h b/client/src/mifare/desfireem.h new file mode 100644 index 000000000..e1a74cf3a --- /dev/null +++ b/client/src/mifare/desfireem.h @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Pack a DESFire card image for emulator memory, and read one back. +// Layout and rationale live in include/desfire_em.h. +//----------------------------------------------------------------------------- + +#ifndef __DESFIREEM_H +#define __DESFIREEM_H + +#include "common.h" +#include "desfire_em.h" +#include "fileutils.h" // desfire_dump_t + +// Which generation a GetVersion hardware frame describes. Needs at least 5 +// bytes; returns DESFIRE_EM_GEN_UNKNOWN for anything it does not recognise. +uint8_t desfire_em_gen_from_version(const uint8_t *versionhw, uint8_t len); + +// NV the GetVersion storage size byte codes. A floor on the real capacity, not +// the capacity: a 2K part codes 2048 and hands out 2560. 0 if undecodable. +uint32_t desfire_em_nominal_cardsize(const uint8_t *versionhw, uint8_t len); +const char *desfire_em_gen_str(uint8_t gen); + +// Emulator memory a file reserves, committed region and shadow together. +// Rounded to DESFIRE_EM_GRANULE. Backup, value and record files carry a shadow +// because CommitTransaction covers them; standard data files do not. +uint32_t desfire_em_file_reserve(const desfire_dump_file_t *f); + +// Pack `dump` into `out`, which may use at most `outlen` bytes. On success +// *used is the number of bytes the image occupies, which is outlen unless the +// caller wants to trim -- the data pool grows down from the end, so the image +// is not contiguous and cannot be shortened. +// +// Fails with PM3_EOUTOFBOUND, naming the shortfall, rather than truncating. +int desfire_em_pack(const desfire_dump_t *dump, uint8_t *out, size_t outlen, size_t *used); + +// Walk an image back into a desfire_dump_t. The caller owns the per-file heap +// buffers afterwards and must release them with desfire_dump_free(). +int desfire_em_unpack(const uint8_t *img, size_t imglen, desfire_dump_t *dump); + +// Print an image's structure. Does not need a device. +void desfire_em_print(const uint8_t *img, size_t imglen); + +// Move an image to and from emulator memory. imglen must be a multiple of 16 +// for the upload, which is what the generic emulator setter addresses in. +int desfire_em_upload(const uint8_t *img, size_t imglen); +int desfire_em_download(uint8_t *img, size_t imglen); + +#endif // __DESFIREEM_H diff --git a/doc/commands.json b/doc/commands.json index 5b8bb2cdc..0481f9161 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -7887,6 +7887,47 @@ ], "usage": "hf mfdes dump [-hav] [-n ] [-t ] [-k ] [--kdf ] [-i ] [-m ] [-c ] [--schann ] [--aid ] [--isoid ] [--dfname ] [-l ] [--no-auth] [-f ] [--keys ] [--ns]" }, + "hf mfdes eload": { + "command": "hf mfdes eload", + "description": "Load a DESFire card dump into emulator memory. The dump is packed into the on-device card image, see doc/mfdes_dump_format.md", + "notes": [ + "hf mfdes eload -f hf-mfdes-01020304050607-dump.json" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-f, --file Filename of dump", + "-v, --verbose Verbose output" + ], + "usage": "hf mfdes eload [-hv] -f " + }, + "hf mfdes esave": { + "command": "hf mfdes esave", + "description": "Save the card image in emulator memory to a dump file. Shows what a reader left behind if one has been talking to the simulation", + "notes": [ + "hf mfdes esave -f myfile" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-f, --file Filename, if no UID will be used as filename", + "-v, --verbose Verbose output" + ], + "usage": "hf mfdes esave [-hv] [-f ]" + }, + "hf mfdes eview": { + "command": "hf mfdes eview", + "description": "Show the DESFire card image currently in emulator memory", + "notes": [ + "hf mfdes eview" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-v, --verbose Also print every application and file" + ], + "usage": "hf mfdes eview [-hv]" + }, "hf mfdes formatpicc": { "command": "hf mfdes formatpicc", "description": "Format card. Can be done only if enabled in the configuration. Master key needs to be provided.", @@ -16561,8 +16602,8 @@ } }, "metadata": { - "commands_extracted": 921, + "commands_extracted": 924, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-14T06:50:14+00:00" + "extracted_on": "2026-09-14T17:45:11+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 4f2c8c2d8..d92ad86aa 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -818,6 +818,9 @@ Check column "offline" for their availability. |`hf mfdes lsfiles `|N |`Show all files list` |`hf mfdes dump `|N |`Dump all files` |`hf mfdes view `|Y |`Display content from tag dump file` +|`hf mfdes eload `|N |`Upload file into emulator memory` +|`hf mfdes esave `|N |`Save emulator memory to file` +|`hf mfdes eview `|N |`View emulator memory` |`hf mfdes createfile `|N |`Create Standard/Backup File` |`hf mfdes createvaluefile`|N |`Create Value File` |`hf mfdes createrecordfile`|N |`Create Linear/Cyclic Record File` diff --git a/include/desfire_em.h b/include/desfire_em.h new file mode 100644 index 000000000..64d3be604 --- /dev/null +++ b/include/desfire_em.h @@ -0,0 +1,284 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// DESFire card image, as it lives in emulator memory. +// +// The client packs a desfire_dump_t (see client/src/fileutils.h, and +// doc/mfdes_dump_format.md for the json it comes from) into this layout and +// uploads it with eload. The device simulation reads it, and writes back into +// it -- a reader that writes to the simulated card changes this image, so an +// esave afterwards shows what the reader did. That is the difference between +// an emulator and a replay rig. +// +// Layout, inside the CARD_MEMORY_SIZE region: +// +// 0 +---------------------------+ +// | desfire_em_hdr_t | fixed +// +---------------------------+ +// | desfire_em_app_t [] | appcount entries +// | desfire_em_file_t [] | filecount entries grows up +// | desfire_em_key_t [] | keycount entries +// tables_end +// | | +// | ..... free ..... | <- the emulated card's free memory +// | | +// data_start +// | file data regions | grows down +// size +---------------------------+ +// +// Two frontiers rather than fixed-size tables: a card with three files spends a +// few hundred bytes, not the worst case. Tables append as the reader creates +// applications and files; a data region is reserved when a file is created and +// never moves afterwards, so WriteData and WriteRecord are pure writes. The +// only allocation check anywhere is tables_end < data_start, and the space +// between them is what GetFreeMem reports -- so the simulated card fills up and +// refuses with OUT_OF_EEPROM the way a real one does. +// +// Nothing here knows how big emulator memory is. The device reports that to the +// client in capabilities_t, and a platform with more of it holds a bigger card +// with no change to this format. +//----------------------------------------------------------------------------- + +#ifndef __DESFIRE_EM_H +#define __DESFIRE_EM_H + +#include "common.h" +#include "desfire.h" + +#define DESFIRE_EM_MAGIC 0x31534544 // "DES1", little endian +#define DESFIRE_EM_LAYOUT_VERSION 1 + +// Allocation granule. A genuine DESFire rounds NV consumption to 32 bytes: +// measured on a DESFire EV2 2K, a 1 byte file costs 32 and a 33 byte file costs +// 64. Rounding the same way keeps GetFreeMem card-shaped. Note the rounding is +// invisible to a reader otherwise -- GetFileSettings reports the size the file +// was created with, also measured. +#define DESFIRE_EM_GRANULE 32 +#define DESFIRE_EM_ROUNDUP(x) (((x) + (DESFIRE_EM_GRANULE - 1)) & ~(DESFIRE_EM_GRANULE - 1)) + +// NV an application costs before its keys. Measured on a DESFire EV2 2K: an +// application with one key costs 96 bytes, with fourteen keys 352. 64 plus the +// key material reproduces the first exactly and the second about 18% low, which +// is close enough for a free-memory figure that only has to look card-shaped -- +// see the note on not reproducing NXP's allocator, below. +#define DESFIRE_EM_APP_OVERHEAD 64 + +// EV1 hard limits. 28 applications is the PICC's own, reported as COUNT_ERROR +// 0xCE; 32 files and 14 keys follow the file/key number ranges 0x00..0x1F and +// 0x00..0x0D. These bound the tables; what actually fits is decided by the +// frontiers, not by these. +#define DESFIRE_EM_MAX_APPS 28 +#define DESFIRE_EM_MAX_FILES 32 +#define DESFIRE_EM_MAX_KEYS DESFIRE_MAX_KEY_COUNT + +// Which generation the image impersonates. The simulation answers a different +// command set for each, so it is stored rather than re-derived from the version +// bytes at run time. Identification bytes are versionhw[3] and [4]: EV1 is +// 01 00, EV2 12 00 or 42 00, EV2 XL 22 00, EV3 33 00, DuoX A0 00. +typedef enum { + DESFIRE_EM_GEN_UNKNOWN = 0, + DESFIRE_EM_GEN_D40 = 1, + DESFIRE_EM_GEN_EV1 = 2, + DESFIRE_EM_GEN_EV2 = 3, + DESFIRE_EM_GEN_EV3 = 4, + DESFIRE_EM_GEN_LIGHT = 5, + DESFIRE_EM_GEN_DUOX = 6, +} desfire_em_gen_t; + +typedef struct { + uint32_t magic; // DESFIRE_EM_MAGIC + uint8_t layout; // DESFIRE_EM_LAYOUT_VERSION + uint8_t generation; // desfire_em_gen_t + uint8_t flags; // RFU + uint8_t rfu0; + + // card identity, straight from the dump + uint8_t uid[10]; + uint8_t uidlen; + uint8_t atqa[2]; + uint8_t sak; + uint8_t atslen; + uint8_t ats[20]; + + // GetVersion answers over three chained frames and they are not the same + // shape across generations, so each is kept raw + uint8_t versionhw[7]; + uint8_t versionhwlen; + uint8_t versionsw[7]; + uint8_t versionswlen; + uint8_t versionprod[14]; + uint8_t versionprodlen; + + uint8_t signature[56]; // NXP originality signature + uint8_t signaturelen; + uint8_t rfu1; + + // app[0] is the PICC, AID 000000. It holds the PICC master key settings and + // keys and never has files, the same way the json format stores it. + uint8_t appcount; + uint8_t filecount; + uint8_t keycount; + uint8_t rfu2; + + uint16_t app_off; // byte offsets from the start of the image + uint16_t file_off; + uint16_t key_off; + + // the two frontiers. tables grow up to tables_end, file data down from + // data_start, and an allocation only has to leave them apart + uint16_t tables_end; + uint16_t data_start; + uint16_t size; // bytes of emulator memory this image may use + + // Two different limits, and a reader must only ever see the first. + // + // cardsize is the NV the emulated card claims to have, so GetFreeMem + // answers cardsize - reserved and CreateFile refuses with OUT_OF_EEPROM + // once that runs out. Without it a card impersonating a 2K part would + // report several kB free, which no 2K part does. + // + // size is what emulator memory physically holds, and is the harder limit + // of the two. It is never visible to a reader. + uint16_t cardsize; + uint16_t reserved; // NV the card has spent, against cardsize +} PACKED desfire_em_hdr_t; + +// ---------------------------------------------------------------- application + +#define DESFIRE_EM_APP_DELETED (1 << 0) // tombstone, see below +#define DESFIRE_EM_APP_AUTHED (1 << 1) // dump ran authenticated here +#define DESFIRE_EM_APP_ISOFIDS (1 << 2) // application uses ISO file ids + +typedef struct { + uint8_t aid[3]; + uint8_t flags; + uint8_t keysettings; + uint8_t numkeysraw; // key type in bits 6-7, iso-fid flag bit 5, count 0-4 + uint16_t isofid; // 0 = none + uint8_t dfname[16]; + uint8_t dfnamelen; + uint8_t keytype; // DesfireCryptoAlgorithm, one per application +} PACKED desfire_em_app_t; + +// ----------------------------------------------------------------------- file + +// flags bits 0-1 carry the raw 2-bit file communication mode, not a +// DesfireCommunicationMode. Use DesfireFileCommModeToCommMode() to convert. +#define DESFIRE_EM_FILE_COMM_MASK 0x03 +#define DESFIRE_EM_FILE_UNKNOWN (1 << 2) // contents were never read, see below +#define DESFIRE_EM_FILE_LIMCREDIT (1 << 3) // value file, LimitedCredit enabled +#define DESFIRE_EM_FILE_FREEGETVAL (1 << 4) // value file, free GetValue +#define DESFIRE_EM_FILE_DELETED (1 << 5) // tombstone +#define DESFIRE_EM_FILE_DIRTY (1 << 6) // uncommitted writes live in the shadow + +typedef struct { + uint8_t app; // index into the application table + uint8_t num; // file number 0x00..0x1F + uint8_t type; // raw file type byte, 0x00..0x04 for EV1 + uint8_t flags; + uint16_t isofid; // 0 = none + uint16_t rights; // raw access rights word + + uint16_t dataoff; // offset of the committed region + uint16_t datalen; // bytes reserved for it + + // CommitTransaction covers backup data, value and record files; a standard + // data file writes straight through. For the three that are covered, + // shadow_off is a second region of datalen bytes that writes land in until + // CommitTransaction swaps them, or AbortTransaction discards them. Zero + // for standard data files. + uint16_t shadow_off; + + union { + struct { // 0x00 standard, 0x01 backup + uint32_t size; // as created, NOT rounded -- GetFileSettings + } data; // reports this verbatim, measured + struct { // 0x02 value + uint32_t lower; + uint32_t upper; + uint32_t value; + } value; + struct { // 0x03 linear, 0x04 cyclic + uint32_t recordsize; + uint32_t maxrecords; // as created. A cyclic file stores one + uint32_t currecords; // fewer than this, the backup mechanism + } record; // consumes one -- EV1 spec 9.5.9 + } u; +} PACKED desfire_em_file_t; + +// ------------------------------------------------------------------------ key + +// The key algorithm is the application's, so it is not repeated here. A key +// version is readable without knowing the key, so the two are tracked apart: a +// version with no value is the normal shape for a key that was found but never +// recovered, and the simulation must refuse an authentication against it rather +// than authenticate with zeros. +#define DESFIRE_EM_KEY_PRESENT (1 << 0) // we hold the key value +#define DESFIRE_EM_KEY_VERKNOWN (1 << 1) // we read the key version + +typedef struct { + uint8_t app; // index into the application table + uint8_t num; // key number + uint8_t flags; + uint8_t ver; + uint8_t key[DESFIRE_MAX_KEY_SIZE]; +} PACKED desfire_em_key_t; + +//----------------------------------------------------------------------------- +// Notes for whoever implements against this +// +// Tombstones, not compaction. DeleteFile and DeleteApplication set a flag and +// leave the space stranded. That is not a shortcut: a genuine card does not +// give the memory back either. Measured on a DESFire EV2 2K -- 2080 bytes free +// with zero applications before an experiment, 2560 after FormatPICC. Only +// FormatPICC reclaims, and here that is resetting both frontiers. +// +// The unknown flag. A file whose contents could not be read carries +// DESFIRE_EM_FILE_UNKNOWN and its region is reserved but meaningless. The +// simulation must answer an error for it, never zeros. "8 bytes of 00" and "we +// could not read 8 bytes" are different facts and a card image that confuses +// them lies to the reader. +// +// Reservation at CreateFile, in granules: +// standard DESFIRE_EM_ROUNDUP(size) +// backup DESFIRE_EM_ROUNDUP(size) twice, data and shadow +// value one granule twice +// record DESFIRE_EM_ROUNDUP(recordsize * maxrecords) twice +// Reserve the full declared extent, not just the records that exist, so +// WriteRecord never has to grow anything. The json stores only the records +// that exist; eload expands, esave compacts. +// +// This deliberately does not reproduce NXP's own allocator. It is undocumented +// and the numbers do not fit a simple model -- a declared 1024 byte record file +// costs 1088 on real silicon, neither 1x nor 2x. What matters is that +// GetFreeMem is self-consistent and shrinks as the reader writes. +// +// Card capacity. The GetVersion storage size byte codes 2^(n>>1) bytes, but +// that understates what the part actually hands out: a DESFire EV2 2K reports +// storage 0x16, 2048 bytes, and 2560 bytes free when formatted -- measured. So +// the nominal figure is a floor, not the capacity, and cardsize is set to +// whichever is larger of the nominal figure and what the image already holds. +// A dump taken from a real card therefore always fits the card it came from. +// +// Response timing. Everything from SelectApplication onward sits behind the +// ISO14443-4 frame waiting time, which the EV1 ATS sets to FWI 8, 77.33 ms -- +// about 3.7 million ARM cycles at MCK. A software AES block is well under a +// microsecond of that, so responses are computed on demand from this image +// rather than precompiled. The pre-RATS layer is the opposite: ATQA, SAK and +// ATS have no such slack and must be precompiled, as the 14a simulation already +// does. +//----------------------------------------------------------------------------- + +#endif // __DESFIRE_EM_H