From 0e679eaa176cc89022daeba31075162d00ed3da6 Mon Sep 17 00:00:00 2001 From: suut Date: Mon, 4 May 2026 23:16:16 +0200 Subject: [PATCH 01/11] Add the command hf mfdes makemfclicense --- client/src/cmdhfmfdes.c | 303 +++++++++++++++++++++++++++++++- client/src/pm3line_vocabulary.h | 1 + 2 files changed, 299 insertions(+), 5 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index ed9c3ad21..ea3d3e186 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -69,6 +69,10 @@ #define MFDES_PC_MAX_ROUNDS 8U #define MFDES_PC_MAC_LEN 8U +#define MFDES_EV3C_MFC_KILL_KEY ((uint8_t)0x31U) +#define MFDES_EV3C_ALLOWED_DATA_PERMISSIONS ((uint8_t)0x11) +#define MFDES_EV3C_ALLOWED_TRAILER_PERMISSIONS ((uint8_t)0x1F) + // DUOX ISO Internal Authenticate #define DUOX_INTAUTH_CHALLENGE_LEN 16 #define DUOX_INTAUTH_SIG_LEN 64 @@ -93,6 +97,12 @@ static const uint8_t kDuoxVDEDefaultDFName[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; +static uint8_t saved_mfclicense[192]; +static size_t saved_mfclicense_len = 0; +static bool saved_mfclicense_set = false; +static uint8_t saved_mfclicense_mac[8]; +static bool saved_mfclicense_mac_set = false; + typedef struct mfd_app_select { bool dfname_present; uint8_t dfname[16]; @@ -3300,9 +3310,10 @@ static int CmdHF14ADesAuth(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes auth", "Select application on the card. It selects app if it is a valid one or returns an error.", - "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" + "hf mfdes auth -n 49 -t aes -k 00000000000000000000000000000000 --force -> permanently disable Mifare Classic functionality on a DESFire EV3C\n" "hf mfdes auth --aid 123456 -> select application 123456 and authenticate via parameters from `default` command\n" "hf mfdes auth --dfname D2760000850100 -n 0 -t aes -k 00000000000000000000000000000000 -> select DF by name and authenticate"); @@ -3310,6 +3321,7 @@ static int CmdHF14ADesAuth(const char *Cmd) { arg_param_begin, arg_lit0("a", "apdu", "Show APDU requests and responses"), arg_lit0("v", "verbose", "Verbose output"), + arg_lit0("f", "force", "Force irreversible operations"), arg_int0("n", "keyno", "", "Key number"), arg_str0("t", "algo", "", "Crypt algo"), arg_str0("k", "key", "", "Key for authenticate (HEX 8(DES), 16(2TDEA or AES) or 24(3TDEA) bytes)"), @@ -3328,22 +3340,29 @@ static int CmdHF14ADesAuth(const char *Cmd) { bool APDULogging = arg_get_lit(ctx, 1); bool verbose = arg_get_lit(ctx, 2); + bool force = arg_get_lit(ctx, 3); DesfireContext_t dctx = {0}; int securechann = defaultSecureChannel; uint32_t id = 0x000000; DesfireISOSelectWay selectway = ISW6bAID; - int res = CmdDesGetSessionParameters(ctx, &dctx, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, &securechann, DCMPlain, &id, &selectway); + int res = CmdDesGetSessionParameters(ctx, &dctx, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, &securechann, DCMPlain, &id, &selectway); if (res) { CLIParserFree(ctx); return res; } - bool save = arg_get_lit(ctx, 14); + bool save = arg_get_lit(ctx, 15); SetAPDULogging(APDULogging); CLIParserFree(ctx); + if (dctx.keyNum == MFDES_EV3C_MFC_KILL_KEY && !force) { + DropField(); + PrintAndLogEx(FAILED, "This operation would permanently disable the Mifare Classic functionality, use --force to override"); + return PM3_EOPABORTED; + } + res = DesfireSelectAndAuthenticateAppW(&dctx, securechann, selectway, id, false, verbose); if (res != PM3_SUCCESS) { DropField(); @@ -6319,6 +6338,279 @@ static int CmdHF14ADesClearRecordFile(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes makemfclicense", + "Create a Mifare Classic license for DESFire EV3C, as well as a license MAC.\n" + "The MFC license MAC key must have the same value as PICC level key 50.\n" + "\n" + "The permissions are a bitwise-or combination of:\n" + " 0x01: allow mapping block\n" + " 0x02: allow updating key B (only for trailers)\n" + " 0x04: allow updating ACs (only for trailers)\n" + " 0x08: allow updating key A (only for trailers)\n" + " 0x10: allow the DESFire RestrictMFCUpdate command", + "hf mfdes makemfclicense -b 4,5,6,8,9,10 --mfc-keys FFFFFFFFFFFF111111111111222222222222\n" + " -> Create a license for blocks 4, 5, 6, 8, 9, 10 where sector 1 has key A = FFFFFFFFFFFF and key B readable,\n" + " -> and sector 2 has key A = 111111111111, key B = 222222222222 with key B non readable"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("v", "verbose", "Show more output"), + arg_str0("k", "key", "", "Key for computing the MAC, must be HEX 16(AES)"), + arg_str0("b", "blk", "[,[,...]]", "The MFC blocks to map, must be given in ascending order (use the special value 'all' for all blocks)"), + arg_lit0(NULL, "key-a", "Allow updating key A from inside sector trailers mapped to DESFire files"), + arg_lit0(NULL, "key-b", "Allow updating key B from inside sector trailers mapped to DESFire files"), + arg_lit0(NULL, "restrict", "Allow the restriction of data updates by the MFC side"), + arg_lit0(NULL, "map", "Allow mapping the blocks to DESFire files"), + arg_lit0(NULL, "access-conditions", "Allow updating the access conditions from inside sector trailers mapped to DESFire files"), + arg_str0("r", "raw", "", "Raw license in case not all blocks should have the same permissions, of the form num_blocks||block1nr||block1perm||block2nr||block2perm||..."), + arg_str0(NULL, "mfc-keys", "", "The concatenated MFC sector keys, one (if key B is readable) or two per sector"), + arg_lit0("s", "save", "Save the license and license MAC for the next commands in this session"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + logLevel_t loglevel = arg_get_lit(ctx, 1) ? INFO : DEBUG; + bool allow_key_a_update = arg_get_lit(ctx, 4); + bool allow_key_b_update = arg_get_lit(ctx, 5); + bool allow_restrict = arg_get_lit(ctx, 6); + bool allow_map = arg_get_lit(ctx, 7); + bool allow_ac = arg_get_lit(ctx, 8); + bool save = arg_get_lit(ctx, 11); + struct arg_str *key_arg = arg_get_str(ctx, 2); + struct arg_str *blk_str_arg = arg_get_str(ctx, 3); + struct arg_str *raw_arg = arg_get_str(ctx, 9); + struct arg_str *mfc_keys_arg = arg_get_str(ctx, 10); + + bool has_raw = raw_arg->count == 1; + bool use_default_keys; + uint8_t license[192]; + int license_len = 0; + size_t num_sectors; + uint8_t mfc_keys[192]; + int mfc_keys_len = 0; + uint8_t mac_key[16]; + int mac_key_len = 0; + + if (key_arg->count != 1) { + PrintAndLogEx(ERR, "At most one instance of --key is required"); + goto mfclicense_parsing_error; + } + if (CLIParamHexToBuf(key_arg, mac_key, sizeof (mac_key), &mac_key_len) != 0) { + goto mfclicense_parsing_error; + } + if (mac_key_len != 16) { + PrintAndLogEx(ERR, "The MFC License MAC key must be exactly 16 bytes"); + goto mfclicense_parsing_error; + } + + switch (mfc_keys_arg->count) { + case 0: + use_default_keys = true; + break; + case 1: + use_default_keys = false; + break; + default: + PrintAndLogEx(ERR, "At most instance of --mfc-keys is required"); + goto mfclicense_parsing_error; + } + + if (raw_arg->count + blk_str_arg->count != 1) { + PrintAndLogEx(ERR, "Exactly one of --raw or --blk are needed"); + goto mfclicense_parsing_error; + } + + if (has_raw) { + if (CLIParamHexToBuf(raw_arg, license, sizeof (license), &license_len) != 0) { + goto mfclicense_parsing_error; + } + if (allow_key_a_update || allow_key_b_update || allow_restrict || allow_map || allow_ac) { + PrintAndLogEx(WARNING, "Permission flags are not used when --raw is used"); + } + + // Verify the license + size_t num_blocks = license[0]; + if (license_len != 2 * num_blocks + 1) { + PrintAndLogEx(ERR, "Invalid number of blocks in the license"); + goto mfclicense_parsing_error; + } + + uint8_t last_sector = license[1] / 4; + num_sectors = 1; + + for (int i = 0; i < num_blocks; i++) { + uint8_t block_nr = license[1 + 2*i]; + uint8_t permissions = license[1 + 2*i + 1]; + + if (block_nr / 4 != last_sector) { + num_sectors++; + last_sector = block_nr / 4; + } + + if (block_nr >= 64) { + PrintAndLogEx(ERR, "Invalid block number %d", block_nr); + goto mfclicense_parsing_error; + } + + if ( (((block_nr + 1) % 4 == 0) && (permissions & ~MFDES_EV3C_ALLOWED_TRAILER_PERMISSIONS)) || + (((block_nr + 1) % 4 != 0) && (permissions & ~MFDES_EV3C_ALLOWED_DATA_PERMISSIONS))) { + PrintAndLogEx(ERR, "Invalid permissions %02X", permissions); + goto mfclicense_parsing_error; + } + + PrintAndLogEx(loglevel, "[%d] Block %d: permissions %02X", i, block_nr, permissions); + } + } else { + uint8_t blocks[64]; + size_t num_blocks = 0; + + if (strcmp("all", blk_str_arg->sval[0]) == 0) { + PrintAndLogEx(loglevel, "Using all blocks"); + for (int i = 0; i < 64; i++) { + blocks[i] = i; + } + num_blocks = 64; + } else { + char blk_str[512]; + if (strlen(blk_str_arg->sval[0]) + 1 > sizeof (blk_str)) { + PrintAndLogEx(ERR, "Argument too long"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + strcpy(blk_str, blk_str_arg->sval[0]); + + char *ptr = blk_str; + char *saveptr = NULL; + char *token; + int last = -1; + for (;;) { + token = strtok_r(ptr, ",", &saveptr); + ptr = NULL; + + if (!token) { + break; + } + + char *endptr = NULL; + long blk_val = strtol(token, &endptr, 0); + if (endptr == NULL || *token == '\0' || *endptr != '\0' || blk_val < 0 || blk_val >= 64) { + PrintAndLogEx(ERR, "Invalid block number: %s", token); + goto mfclicense_parsing_error; + } + if (blk_val <= last) { + PrintAndLogEx(ERR, "Blocks must be unique and sorted in ascending order"); + goto mfclicense_parsing_error; + } + blocks[num_blocks++] = blk_val; + last = blk_val; + + if (num_blocks > 64) { + PrintAndLogEx(ERR, "Too many blocks"); + goto mfclicense_parsing_error; + } + } + + if (num_blocks == 0) { + PrintAndLogEx(ERR, "At least one block must be given"); + goto mfclicense_parsing_error; + } + } + + uint8_t data_permissions = 0; + uint8_t trailer_permissions = 0; + if (allow_map) { + data_permissions |= 0x01; + trailer_permissions |= 0x01; + } + if (allow_key_b_update) { + trailer_permissions |= 0x02; + } + if (allow_ac) { + trailer_permissions |= 0x04; + } + if (allow_key_a_update) { + trailer_permissions |= 0x08; + } + if (allow_restrict) { + data_permissions |= 0x10; + trailer_permissions |= 0x10; + } + + uint8_t last_sector = blocks[0] / 4; + num_sectors = 1; + license[0] = num_blocks; + for (int i = 0; i < num_blocks; i++) { + if (blocks[i] / 4 != last_sector) { + num_sectors++; + last_sector = blocks[i] / 4; + } + + license[1 + 2*i] = blocks[i]; + if ((blocks[i] + 1) % 4 == 0) { + // mapping a trailer block + license[1 + 2*i + 1] = trailer_permissions; + } else { + // mapping a data block + license[1 + 2*i + 1] = data_permissions; + } + PrintAndLogEx(loglevel, "[%d] Block %d: permissions %02X", i, license[1 + 2*i], license[1 + 2*i + 1]); + } + license_len = 1 + 2*num_blocks; + } + + PrintAndLogEx(loglevel, "Covering %d sectors in total", num_sectors); + + if (use_default_keys) { + // by default: all key A FFFFFFFFFFFF, all key B readable + memset(mfc_keys, '\xFF', 6 * num_sectors); + mfc_keys_len = 6 * num_sectors; + } else { + if (CLIParamHexToBuf(mfc_keys_arg, mfc_keys, sizeof (mfc_keys), &mfc_keys_len) != 0) { + goto mfclicense_parsing_error; + } + if (mfc_keys_len % 6 != 0) { + PrintAndLogEx(ERR, "MFC keys length must be a multiple of 6 bytes"); + goto mfclicense_parsing_error; + } + // Quick sanity checks: do we have more or less the right number of keys for the number of sectors? + if (mfc_keys_len / 6 < num_sectors || (mfc_keys_len / 6) > 2 * num_sectors) { + PrintAndLogEx(ERR, "Not enough or too many keys for the number of sectors (expecting 1 or 2 keys per sector)"); + goto mfclicense_parsing_error; + } + } + + CLIParserFree(ctx); + + // Compute the MAC + uint8_t license_mac_data[384]; + uint8_t mac[8]; + license_mac_data[0] = 0x01; + memcpy(license_mac_data + 1, license, license_len); + memcpy(license_mac_data + 1 + license_len, mfc_keys, mfc_keys_len); + + aes_cmac8(NULL, mac_key, license_mac_data, mac, 1 + license_len + mfc_keys_len); + + PrintAndLogEx(INFO, "License: %s", sprint_hex_inrow(license, license_len)); + PrintAndLogEx(INFO, "MAC: %s", sprint_hex_inrow(mac, 8)); + + if (save) { + memset(saved_mfclicense, license, license_len); + saved_mfclicense_len = license_len; + saved_mfclicense_set = true; + memset(saved_mfclicense_mac, mac, 8); + saved_mfclicense_mac_set = true; + PrintAndLogEx(INFO, "Saved license and license MAC for the next commands in this session"); + } + + return PM3_SUCCESS; + +mfclicense_parsing_error: + CLIParserFree(ctx); + return PM3_EINVARG; +} + static int DesfileReadISOFileAndPrint(DesfireContext_t *dctx, bool select_current_file, uint8_t fnum, uint16_t fisoid, int filetype, @@ -8253,6 +8545,7 @@ static command_t CommandTable[] = { {"write", CmdHF14ADesWriteData, IfPm3Iso14443a, "Write data to standard/backup/record/value file"}, {"value", CmdHF14ADesValueOperations, IfPm3Iso14443a, "Operations with value file (get/credit/limited credit/debit/clear)"}, {"clearrecfile", CmdHF14ADesClearRecordFile, IfPm3Iso14443a, "Clear record File"}, + {"makemfclicense", CmdHF14ADesMakeMFCLicense, AlwaysAvailable, "Generate a Mifare Classic license for DESFire EV3C"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("DUOX") " ------------------------"}, {"intauth", CmdHF14ADesIntAuth, IfPm3Iso14443a, "ISO Internal Authenticate (ECDSA challenge-response)"}, {"vdesign", CmdHF14ADesVdeSign, IfPm3Iso14443a, "VDE ECDSASign (EV charging signature over 32-byte challenge)"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 712dd1774..c70adbdd9 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -526,6 +526,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes write" }, { 0, "hf mfdes value" }, { 0, "hf mfdes clearrecfile" }, + { 1, "hf mfdes makemfclicense" }, { 0, "hf mfdes intauth" }, { 0, "hf mfdes vdesign" }, { 1, "hf mfdes test" }, From 23fbdd2f5278d6ac89e5a4b196302f6acd3f303c Mon Sep 17 00:00:00 2001 From: suut Date: Mon, 4 May 2026 23:23:54 +0200 Subject: [PATCH 02/11] Fix typo, check if raw blocks are given in ascending order --- client/src/cmdhfmfdes.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index ea3d3e186..f128c5492 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -6439,6 +6439,7 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { uint8_t last_sector = license[1] / 4; num_sectors = 1; + int last_block = -1; for (int i = 0; i < num_blocks; i++) { uint8_t block_nr = license[1 + 2*i]; @@ -6449,6 +6450,12 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { last_sector = block_nr / 4; } + if (block_nr <= last_block) { + PrintAndLogEx(ERR, "Blocks must be unique and sorted in ascending order"); + goto mfclicense_parsing_error; + } + last_block = block_nr; + if (block_nr >= 64) { PrintAndLogEx(ERR, "Invalid block number %d", block_nr); goto mfclicense_parsing_error; @@ -6596,10 +6603,10 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { PrintAndLogEx(INFO, "MAC: %s", sprint_hex_inrow(mac, 8)); if (save) { - memset(saved_mfclicense, license, license_len); + memcpy(saved_mfclicense, license, license_len); saved_mfclicense_len = license_len; saved_mfclicense_set = true; - memset(saved_mfclicense_mac, mac, 8); + memcpy(saved_mfclicense_mac, mac, 8); saved_mfclicense_mac_set = true; PrintAndLogEx(INFO, "Saved license and license MAC for the next commands in this session"); } From 106a9e61a3cbe370bdca88390238c2ebb89bcb78 Mon Sep 17 00:00:00 2001 From: suut Date: Tue, 5 May 2026 18:12:59 +0200 Subject: [PATCH 03/11] refactor the MFC block parsing into a function parse_mfc_blocks --- client/src/cmdhfmfdes.c | 122 ++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 42892b87b..68ff857d1 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -6399,6 +6399,82 @@ static int CmdHF14ADesClearRecordFile(const char *Cmd) { return PM3_SUCCESS; } +/** + * Parse MFC blocks given on the command line + * + * If given for making the license, the blocks must be unique and in ascending order. + * + * If given for the CreateMFCMapping command, the blocks must be unique and either all data blocks or all trailer blocks. + */ +static int parse_mfc_blocks(const char *in, uint8_t blocks_out[], size_t *blocks_out_len, bool for_license) { + if (!in || !blocks_out_len) { + return PM3_EINVARG; + } + *blocks_out_len = 0; + char blk_str[512]; + if (strlen(in) + 1 > sizeof (blk_str)) { + PrintAndLogEx(ERR, "Argument too long"); + return PM3_EINVARG; + } + strcpy(blk_str, in); + + char *ptr = blk_str; + char *saveptr = NULL; + char *token; + int last = -1; + uint64_t seen_so_far = 0; + bool has_data = false; + bool has_trailer = false; + for (int i = 0;; i++) { + token = strtok_r(ptr, ",", &saveptr); + ptr = NULL; + + if (!token) { + break; + } + + char *endptr = NULL; + long blk_val = strtol(token, &endptr, 0); + if (endptr == NULL || *token == '\0' || *endptr != '\0' || blk_val < 0 || blk_val >= 64) { + PrintAndLogEx(ERR, "Invalid block number: %s", token); + return PM3_EINVARG; + } + if (for_license) { // check if sorted in ascending order + if (blk_val <= last) { + PrintAndLogEx(ERR, "Blocks must be unique and sorted in ascending order"); + return PM3_EINVARG; + } + } else { // check if unique and all data / all trailer + if (seen_so_far & (1ULL << blk_val)) { + PrintAndLogEx(ERR, "Blocks must be unique"); + return PM3_EINVARG; + } + seen_so_far |= (1ULL << blk_val); + has_data |= !!((blk_val + 1) % 4 != 0); + has_trailer |= !!((blk_val + 1) % 4 == 0); + + if (has_data && has_trailer) { + PrintAndLogEx(ERR, "Either data blocks or trailer blocks must be given, not both"); + return PM3_EINVARG; + } + } + blocks_out[(*blocks_out_len)++] = blk_val; + last = blk_val; + + if (*blocks_out_len > 64) { + PrintAndLogEx(ERR, "Too many blocks"); + return PM3_EINVARG; + } + } + + if (*blocks_out_len == 0) { + PrintAndLogEx(ERR, "At least one block must be given"); + return PM3_EINVARG; + } + + return PM3_SUCCESS; +} + static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes makemfclicense", @@ -6541,47 +6617,7 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { } num_blocks = 64; } else { - char blk_str[512]; - if (strlen(blk_str_arg->sval[0]) + 1 > sizeof (blk_str)) { - PrintAndLogEx(ERR, "Argument too long"); - CLIParserFree(ctx); - return PM3_EINVARG; - } - strcpy(blk_str, blk_str_arg->sval[0]); - - char *ptr = blk_str; - char *saveptr = NULL; - char *token; - int last = -1; - for (;;) { - token = strtok_r(ptr, ",", &saveptr); - ptr = NULL; - - if (!token) { - break; - } - - char *endptr = NULL; - long blk_val = strtol(token, &endptr, 0); - if (endptr == NULL || *token == '\0' || *endptr != '\0' || blk_val < 0 || blk_val >= 64) { - PrintAndLogEx(ERR, "Invalid block number: %s", token); - goto mfclicense_parsing_error; - } - if (blk_val <= last) { - PrintAndLogEx(ERR, "Blocks must be unique and sorted in ascending order"); - goto mfclicense_parsing_error; - } - blocks[num_blocks++] = blk_val; - last = blk_val; - - if (num_blocks > 64) { - PrintAndLogEx(ERR, "Too many blocks"); - goto mfclicense_parsing_error; - } - } - - if (num_blocks == 0) { - PrintAndLogEx(ERR, "At least one block must be given"); + if (parse_mfc_blocks(blk_str_arg->sval[0], blocks, &num_blocks, true) != PM3_SUCCESS) { goto mfclicense_parsing_error; } } @@ -6628,7 +6664,7 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { license_len = 1 + 2*num_blocks; } - PrintAndLogEx(loglevel, "Covering %d sectors in total", num_sectors); + PrintAndLogEx(loglevel, "Covering %d sector%s in total", num_sectors, num_sectors != 1 ? "s" : ""); if (use_default_keys) { // by default: all key A FFFFFFFFFFFF, all key B readable @@ -6642,7 +6678,7 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { PrintAndLogEx(ERR, "MFC keys length must be a multiple of 6 bytes"); goto mfclicense_parsing_error; } - // Quick sanity checks: do we have more or less the right number of keys for the number of sectors? + // Quick sanity check: do we have more or less the right number of keys for the number of sectors? if (mfc_keys_len / 6 < num_sectors || (mfc_keys_len / 6) > 2 * num_sectors) { PrintAndLogEx(ERR, "Not enough or too many keys for the number of sectors (expecting 1 or 2 keys per sector)"); goto mfclicense_parsing_error; From 80b70a37f47c5498c7150b297c359c8590eac97a Mon Sep 17 00:00:00 2001 From: suut Date: Tue, 5 May 2026 18:27:53 +0200 Subject: [PATCH 04/11] remove unused variable to make compiler happy --- client/src/cmdhfmfdes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 68ff857d1..3cf1ec2cc 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -6425,7 +6425,7 @@ static int parse_mfc_blocks(const char *in, uint8_t blocks_out[], size_t *blocks uint64_t seen_so_far = 0; bool has_data = false; bool has_trailer = false; - for (int i = 0;; i++) { + for (;;) { token = strtok_r(ptr, ",", &saveptr); ptr = NULL; From 80751a88c07ae06c99d6d368e06012551677ce4c Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 10 May 2026 20:50:24 +0300 Subject: [PATCH 05/11] Add ISODEP trace annotation helpers --- client/src/cmdhflist.c | 103 ++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/client/src/cmdhflist.c b/client/src/cmdhflist.c index 5534a52ec..2265a3a08 100644 --- a/client/src/cmdhflist.c +++ b/client/src/cmdhflist.c @@ -943,43 +943,83 @@ void annotateIso7816(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool } } +static bool iso14443_4_get_i_block_inf(uint8_t *cmd, uint8_t cmdsize, bool is_response, const uint8_t **inf, size_t *inf_len) { + (void)is_response; + size_t frame_len = cmdsize; + if (frame_len < 1 || (cmd[0] & 0xC0) != 0x00 || (cmd[0] & 0x02) != 0x02) { + return false; + } + + size_t pos = 1; + if ((cmd[0] & 0x08) == 0x08) { + pos++; + } + if ((cmd[0] & 0x04) == 0x04) { + pos++; + } + if (pos >= frame_len) { + return false; + } + + *inf = cmd + pos; + if (inf_len != NULL) { + *inf_len = frame_len - pos; + } + return true; +} + +static bool annotateIso14443_s_r_block(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool is_response, bool show_block_number) { + (void)is_response; + size_t frame_len = cmdsize; + if (frame_len < 1 || frame_len > 4) { + return false; + } + + if ((cmd[0] & 0xC0) == 0xC0) { + switch (cmd[0] & 0x30) { + case 0x00: + snprintf(exp, size, "S-block DESELECT"); + break; + case 0x30: + snprintf(exp, size, "S-block WTX"); + break; + default: + snprintf(exp, size, "S-block"); + break; + } + return true; + } + + if ((cmd[0] & 0xD0) == 0x80) { + if (show_block_number) { + snprintf(exp, size, (cmd[0] & 0x10) ? "R-block NACK(%d)" : "R-block ACK(%d)", cmd[0] & 0x01); + } else { + snprintf(exp, size, (cmd[0] & 0x10) ? "R-block NACK" : "R-block ACK"); + } + return true; + } + + return false; +} + // MIFARE DESFire void annotateMfDesfire(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { // it's basically a ISO14443a tag, so try annotation from there if (applyIso14443a(exp, size, cmd, cmdsize, false) != PM3_SUCCESS) { - // S-block 11xxx010 - if ((cmd[0] & 0xC0) && (cmdsize == 3)) { - switch ((cmd[0] & 0x30)) { - case 0x00: - snprintf(exp, size, "S-block DESELECT"); - break; - case 0x30: - snprintf(exp, size, "S-block WTX"); - break; - default: - snprintf(exp, size, "S-block"); - break; - } - } - // R-block (ack) 101xx01x - else if (((cmd[0] & 0xB0) == 0xA0) && (cmdsize > 2)) { - if ((cmd[0] & 0x10) == 0) - snprintf(exp, size, "R-block ACK(%d)", (cmd[0] & 0x01)); - else - snprintf(exp, size, "R-block NACK(%d)", (cmd[0] & 0x01)); + if (annotateIso14443_s_r_block(exp, size, cmd, cmdsize, false, true)) { + return; } // I-block 000xCN1x else if (((cmd[0] & 0xC0) == 0x00) && (cmdsize > 2)) { - // PCB [CID] [NAD] [INF] CRC CRC - int pos = 1; - if ((cmd[0] & 0x08) == 0x08) // cid byte following - pos++; + const uint8_t *inf = NULL; + if (iso14443_4_get_i_block_inf(cmd, cmdsize, false, &inf, NULL) == false) { + return; + } - if ((cmd[0] & 0x04) == 0x04) // nad byte following - pos++; + int pos = inf - cmd; for (uint8_t i = 0; i < 2; i++, pos++) { bool found_annotation = true; @@ -1370,13 +1410,12 @@ void annotateMfPlus(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { // ok this part is copy paste from annotateMfDesfire, it seems to work for MIFARE Plus also if (((cmd[0] & 0xC0) == 0x00) && (cmdsize > 2)) { - // PCB [CID] [NAD] [INF] CRC CRC - int pos = 1; - if ((cmd[0] & 0x08) == 0x08) // cid byte following - pos++; + const uint8_t *inf = NULL; + if (iso14443_4_get_i_block_inf(cmd, cmdsize, false, &inf, NULL) == false) { + return; + } - if ((cmd[0] & 0x04) == 0x04) // nad byte following - pos++; + int pos = inf - cmd; for (uint8_t i = 0; i < 2; i++, pos++) { bool found_annotation = true; From 9399dfc16e854ff9f2cfe815e8e2d45e3ea1e4c9 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 10 May 2026 20:57:46 +0300 Subject: [PATCH 06/11] Fix ISO7816 trace CRC status handling --- client/src/cmdtrace.c | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/client/src/cmdtrace.c b/client/src/cmdtrace.c index 6653f46f4..e98ca1df1 100644 --- a/client/src/cmdtrace.c +++ b/client/src/cmdtrace.c @@ -39,6 +39,14 @@ static int CmdHelp(const char *Cmd); static uint8_t *gs_trace; static uint16_t gs_traceLen = 0; +typedef enum { + TRACE_CRC_FAIL = 0, + TRACE_CRC_OK = 1, + TRACE_CRC_NONE = 2, + TRACE_CRC_A_OK = 3, + TRACE_CRC_B_OK = 4, +} trace_crc_status_t; + static bool is_last_record(uint16_t tracepos, uint16_t traceLen) { return ((tracepos + TRACELOG_HDR_LEN) >= traceLen); } @@ -542,7 +550,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr } //Check the CRC status - uint8_t crcStatus = 2; + trace_crc_status_t crcStatus = TRACE_CRC_NONE; if (data_len > 2) { switch (protocol) { @@ -569,9 +577,18 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr crcStatus = seos_CRC_check(hdr->isResponse, frame, data_len); break; case ISO_7816_4: - crcStatus = iso14443A_CRC_check(hdr->isResponse, frame, data_len) == 1 ? 3 : 0; - crcStatus = iso14443B_CRC_check(frame, data_len) == 1 ? 4 : crcStatus; + { + uint8_t crcA = iso14443A_CRC_check(hdr->isResponse, frame, data_len); + uint8_t crcB = iso14443B_CRC_check(frame, data_len); + if (crcA == TRACE_CRC_OK) { + crcStatus = TRACE_CRC_A_OK; + } else if (crcB == TRACE_CRC_OK) { + crcStatus = TRACE_CRC_B_OK; + } else { + crcStatus = crcA; + } break; + } case THINFILM: frame[data_len - 1] ^= frame[data_len - 2]; frame[data_len - 2] ^= frame[data_len - 1]; @@ -697,7 +714,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr (*(pos2 + 1)) = '\0'; } else { - if (crcStatus == 0 || crcStatus == 1) { + if (crcStatus == TRACE_CRC_FAIL || crcStatus == TRACE_CRC_OK) { char *pos1 = line[(data_len - 2) / TRACE_MAX_HEX_BYTES]; int delta = (data_len - 2) % TRACE_MAX_HEX_BYTES ? 1 : 0; @@ -708,7 +725,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr char *cb_str = str_dup(pos1 + delta); if (g_session.supports_colors) { - if (crcStatus == 0) { + if (crcStatus == TRACE_CRC_FAIL) { snprintf(pos1, 24, AEND " " _RED_("%s"), cb_str); } else { snprintf(pos1, 24, AEND " " _GREEN_("%s"), cb_str); @@ -726,7 +743,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr cb_str = str_dup(pos1); if (g_session.supports_colors) { - if (crcStatus == 0) { + if (crcStatus == TRACE_CRC_FAIL) { snprintf(pos1, 24, _RED_("%s"), cb_str); } else { snprintf(pos1, 24, _GREEN_("%s"), cb_str); @@ -742,7 +759,13 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr } // Draw the CRC column - const char *crcstrings[] = { _RED_(" !! "), _GREEN_(" ok "), " ", _GREEN_("A ok"), _GREEN_("B ok") }; + const char *crcstrings[] = { + [TRACE_CRC_FAIL] = _RED_(" !! "), + [TRACE_CRC_OK] = _GREEN_(" ok "), + [TRACE_CRC_NONE] = " ", + [TRACE_CRC_A_OK] = _GREEN_("A ok"), + [TRACE_CRC_B_OK] = _GREEN_("B ok"), + }; const char *crc = crcstrings[crcStatus]; // mark short bytes (less than 8 Bit + Parity) From 3da7e52c1a8f844a8df2ff83437cdf1ce7c8dccd Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 10 May 2026 21:17:15 +0300 Subject: [PATCH 07/11] Implement 'hf calypso list' command --- CHANGELOG.md | 1 + client/src/cmdhfcalypso.c | 15 ++++ client/src/cmdhfcalypso.h | 1 + client/src/cmdhflist.c | 120 ++++++++++++++++++++++++++++++++ client/src/cmdhflist.h | 1 + client/src/cmdtrace.c | 11 ++- client/src/pm3line_vocabulary.h | 1 + include/protocols.h | 6 +- 8 files changed, 154 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d73a48b..d3cad5b3a 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 calypso list` command (@kormax) - Added `hf mfd verifycert` command (@kormax) - Added `hf calypso dump` command (@kormax) - Added `pm3trace_edit.py` script for editing of pm3 trace files (@iceman1001) diff --git a/client/src/cmdhfcalypso.c b/client/src/cmdhfcalypso.c index e28985899..704e61d37 100644 --- a/client/src/cmdhfcalypso.c +++ b/client/src/cmdhfcalypso.c @@ -27,6 +27,7 @@ #include "cmdhf14a.h" #include "cmdhf14b.h" #include "cmdparser.h" +#include "cmdtrace.h" #include "commonutil.h" #include "comms.h" #include "emv/emvcore.h" @@ -280,6 +281,15 @@ static const calypso_get_data_probe_t calypso_get_data_probes[] = { {0x5F52, "ATR historical bytes", true}, }; +const char *CalypsoGetDataTagName(uint16_t tag) { + for (size_t i = 0; i < ARRAYLEN(calypso_get_data_probes); i++) { + if (calypso_get_data_probes[i].tag == tag) { + return calypso_get_data_probes[i].name; + } + } + return NULL; +} + static const char *calypso_file_structure_desc(uint8_t subtype) { switch (subtype) { case 0x00: @@ -2898,6 +2908,10 @@ static int CmdHFCalypsoDump(const char *Cmd) { return first_error; } +static int CmdHFCalypsoList(const char *Cmd) { + return CmdTraceListAlias(Cmd, "hf calypso", "calypso"); +} + static int CmdHFCalypsoInfo(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf calypso info", @@ -3006,6 +3020,7 @@ static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"info", CmdHFCalypsoInfo, IfPm3Iso14443, "Tag information"}, {"dump", CmdHFCalypsoDump, IfPm3Iso14443, "Dump readable files and records"}, + {"list", CmdHFCalypsoList, AlwaysAvailable, "List Calypso history"}, {NULL, NULL, NULL, NULL} }; diff --git a/client/src/cmdhfcalypso.h b/client/src/cmdhfcalypso.h index 85b2ce80f..355b77317 100644 --- a/client/src/cmdhfcalypso.h +++ b/client/src/cmdhfcalypso.h @@ -22,5 +22,6 @@ #include "common.h" int CmdHFCalypso(const char *Cmd); +const char *CalypsoGetDataTagName(uint16_t tag); #endif diff --git a/client/src/cmdhflist.c b/client/src/cmdhflist.c index 2265a3a08..7069ae134 100644 --- a/client/src/cmdhflist.c +++ b/client/src/cmdhflist.c @@ -30,6 +30,7 @@ #include "crapto1/crapto1.h" #include "protocols.h" #include "cmdhficlass.h" +#include "cmdhfcalypso.h" #include "mifare/mifaredefault.h" // mifare consts #include "cmdhfseos.h" @@ -1002,6 +1003,125 @@ static bool annotateIso14443_s_r_block(char *exp, size_t size, uint8_t *cmd, uin return false; } +static void calypso_sfi_file_ref(char *out, size_t out_len, uint8_t p2, uint8_t current_mask, uint8_t sfi_mask) { + if (p2 == current_mask) { + snprintf(out, out_len, "current EF"); + } else if ((p2 & 0x07) == sfi_mask && (p2 >> 3) != 0) { + snprintf(out, out_len, "sfi=%u", p2 >> 3); + } else { + snprintf(out, out_len, "p2=%02X", p2); + } +} + +static void calypso_binary_ref(char *out, size_t out_len, uint8_t ins, uint8_t p1, uint8_t p2) { + if (ins == CALYPSO_READ_BINARY) { + if ((p1 & 0x80) == 0x80) { + snprintf(out, out_len, "sfi=%u, off=%u", p1 & 0x1F, p2); + } else { + snprintf(out, out_len, "off=%u", ((p1 & 0x7F) << 8) | p2); + } + } else if ((p2 & 0x80) == 0x80) { + snprintf(out, out_len, "sfi=%u", p2 & 0x1F); + } else if (p2 == 0x00) { + snprintf(out, out_len, "current EF"); + } else { + snprintf(out, out_len, "p2=%02X", p2); + } +} + +static bool annotateCalypsoApdu(char *exp, size_t size, const uint8_t *apdu, size_t apdu_len) { + if (apdu_len < 4) { + return false; + } + + uint8_t ins = apdu[1]; + uint8_t p1 = apdu[2]; + uint8_t p2 = apdu[3]; + + switch (ins) { + case CALYPSO_SELECT: { + if (p1 == 0x04) { + const char *mode = (p2 == 0x02 || p2 == 0x0E) ? "next" : "first"; + const char *fci = (p2 == 0x0C || p2 == 0x0E) ? "none" : "return"; + snprintf(exp, size, "SELECT APPLICATION (mode=%s, fci=%s)", mode, fci); + } else if (p1 == 0x02 && (p2 == 0x00 || p2 == 0x02)) { + snprintf(exp, size, "SELECT FILE"); + } else if (p1 == 0x09 && p2 == 0x00) { + snprintf(exp, size, "SELECT FILE (current DF)"); + } else if (p1 == 0x00) { + snprintf(exp, size, "SELECT FILE (by file id)"); + } else if (p1 == 0x08) { + snprintf(exp, size, "SELECT FILE (by path)"); + } else { + snprintf(exp, size, "SELECT"); + } + return true; + } + case CALYPSO_GET_DATA: { + uint16_t tag = (p1 << 8) | p2; + const char *name = CalypsoGetDataTagName(tag); + if (name) { + snprintf(exp, size, "GET DATA (tag=%04X - %s)", tag, name); + } else { + snprintf(exp, size, "GET DATA (tag=%04X)", tag); + } + return true; + } + case CALYPSO_READ_RECORD: { + char ref[20]; + calypso_sfi_file_ref(ref, sizeof(ref), p2, 0x04, 0x04); + snprintf(exp, size, "READ RECORD (%s, rec=%u)", ref, p1); + return true; + } + case CALYPSO_READ_RECORD_MULTIPLE: { + char ref[20]; + calypso_sfi_file_ref(ref, sizeof(ref), p2, 0x05, 0x05); + snprintf(exp, size, "READ RECORDS (%s, rec=%u)", ref, p1); + return true; + } + case CALYPSO_READ_BINARY: + case CALYPSO_READ_BINARY_EXTENDED: { + char ref[20]; + calypso_binary_ref(ref, sizeof(ref), ins, p1, p2); + snprintf(exp, size, "READ BINARY (%s)", ref); + return true; + } + case CALYPSO_GET_CHALLENGE: + snprintf(exp, size, "GET CHALLENGE"); + return true; + case CALYPSO_GET_RESPONSE: + snprintf(exp, size, "GET RESPONSE"); + return true; + default: + return false; + } +} + +void annotateCalypso(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool is_response) { + if (cmdsize < 1 || is_response) { + return; + } + + if (applyIso14443a(exp, size, cmd, cmdsize, false) == PM3_SUCCESS) { + return; + } + + if (cmd[0] == ISO14443B_REQB || cmd[0] == ISO14443B_ATTRIB || cmd[0] == ISO14443B_HALT) { + annotateIso14443b(exp, size, cmd, cmdsize); + return; + } + + if (annotateIso14443_s_r_block(exp, size, cmd, cmdsize, false, false)) { + return; + } + + const uint8_t *inf = NULL; + size_t inf_len = 0; + if (iso14443_4_get_i_block_inf(cmd, cmdsize, false, &inf, &inf_len)) { + annotateCalypsoApdu(exp, size, inf, inf_len); + } +} + // MIFARE DESFire void annotateMfDesfire(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { diff --git a/client/src/cmdhflist.h b/client/src/cmdhflist.h index 0f8c526b3..55f6a67ae 100644 --- a/client/src/cmdhflist.h +++ b/client/src/cmdhflist.h @@ -56,6 +56,7 @@ void annotateTopaz(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize); void annotateLegic(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize); void annotateFelica(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize); void annotateIso7816(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool is_response); +void annotateCalypso(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool is_response); void annotateIso14443b(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize); void annotateIso14443a(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize, bool is_response); void annotateMfDesfire(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize); diff --git a/client/src/cmdtrace.c b/client/src/cmdtrace.c index e98ca1df1..d565fb2a1 100644 --- a/client/src/cmdtrace.c +++ b/client/src/cmdtrace.c @@ -577,7 +577,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr crcStatus = seos_CRC_check(hdr->isResponse, frame, data_len); break; case ISO_7816_4: - { + case PROTO_CALYPSO: { uint8_t crcA = iso14443A_CRC_check(hdr->isResponse, frame, data_len); uint8_t crcB = iso14443B_CRC_check(frame, data_len); if (crcA == TRACE_CRC_OK) { @@ -644,6 +644,7 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr && protocol != ISO_15693 && protocol != ICLASS && protocol != ISO_7816_4 + && protocol != PROTO_CALYPSO && protocol != PROTO_HITAG1 && protocol != PROTO_HITAG2 && protocol != PROTO_HITAGS @@ -819,6 +820,9 @@ static uint16_t printTraceLine(uint16_t tracepos, uint16_t traceLen, uint8_t *tr case PROTO_FMCOS20: annotateIso14443a(explanation, sizeof(explanation), frame, data_len, hdr->isResponse); break; + case PROTO_CALYPSO: + annotateCalypso(explanation, sizeof(explanation), frame, data_len, hdr->isResponse); + break; case PROTO_MIFARE: case PROTO_MFPLUS: annotateMifare(explanation, sizeof(explanation), frame, data_len, parityBytes, TRACELOG_PARITY_LEN(hdr), hdr->isResponse); @@ -1340,6 +1344,7 @@ int CmdTraceList(const char *Cmd) { "trace list -t 14b -> interpret as " _YELLOW_("ISO14443-B") "\n" "trace list -t 15 -> interpret as " _YELLOW_("ISO15693") "\n" "trace list -t 7816 -> interpret as " _YELLOW_("ISO7816-4") "\n" + "trace list -t calypso -> interpret as " _YELLOW_("Calypso") "\n" "trace list -t cryptorf -> interpret as " _YELLOW_("CryptoRF") "\n" "trace list -t des -> interpret as " _YELLOW_("MIFARE DESFire") "\n" "trace list -t felica -> interpret as " _YELLOW_("ISO18092 / FeliCa") "\n" @@ -1408,6 +1413,7 @@ int CmdTraceList(const char *Cmd) { else if (strcmp(type, "14b") == 0) protocol = ISO_14443B; else if (strcmp(type, "15") == 0) protocol = ISO_15693; else if (strcmp(type, "7816") == 0) protocol = ISO_7816_4; + else if (strcmp(type, "calypso") == 0) protocol = PROTO_CALYPSO; else if (strcmp(type, "cryptorf") == 0) protocol = PROTO_CRYPTORF; else if (strcmp(type, "des") == 0) protocol = MFDES; else if (strcmp(type, "felica") == 0) protocol = FELICA; @@ -1504,6 +1510,9 @@ int CmdTraceList(const char *Cmd) { if (protocol == ISO_7816_4) PrintAndLogEx(INFO, _YELLOW_("ISO7816-4 / Smartcard") " - Timings n/a"); + if (protocol == PROTO_CALYPSO) + PrintAndLogEx(INFO, _YELLOW_("Calypso") " - Timings n/a"); + if (protocol == PROTO_HITAG1 || protocol == PROTO_HITAG2 || protocol == PROTO_HITAGS || protocol == PROTO_HITAGU) { PrintAndLogEx(INFO, _YELLOW_("Hitag 1 / Hitag 2 / Hitag S / Hitag ยต") " - Timings in ETU (8us)"); } diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index b4282c537..2514d1c33 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -222,6 +222,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf calypso help" }, { 0, "hf calypso info" }, { 0, "hf calypso dump" }, + { 1, "hf calypso list" }, { 1, "hf cipurse help" }, { 0, "hf cipurse info" }, { 0, "hf cipurse select" }, diff --git a/include/protocols.h b/include/protocols.h index 58b5041c0..bebb4349f 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -462,7 +462,8 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define PROTO_TEXKOM 19 #define PROTO_XEROX 20 #define PROTO_FMCOS20 21 -#define COUNT_OF_PROTOCOLS 22 +#define PROTO_CALYPSO 22 +#define COUNT_OF_PROTOCOLS 23 // Picopass fuses #define FUSE_FPERS 0x80 @@ -928,6 +929,7 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. // Calypso protocol #define CALYPSO_GET_RESPONSE 0xC0 +#define CALYPSO_GET_DATA 0xCA #define CALYPSO_SELECT 0xA4 #define CALYPSO_INVALIDATE 0x04 #define CALYPSO_REHABILITATE 0x44 @@ -935,7 +937,9 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define CALYPSO_DECREASE 0x30 #define CALYPSO_INCREASE 0x32 #define CALYPSO_READ_BINARY 0xB0 +#define CALYPSO_READ_BINARY_EXTENDED 0xB1 #define CALYPSO_READ_RECORD 0xB2 +#define CALYPSO_READ_RECORD_MULTIPLE 0xB3 #define CALYPSO_UPDATE_BINARY 0xD6 #define CALYPSO_UPDATE_RECORD 0xDC #define CALYPSO_WRITE_RECORD 0xD2 From cfb32e69515a9756859a873ec3b209c8d5910fb1 Mon Sep 17 00:00:00 2001 From: suut Date: Sun, 10 May 2026 23:27:27 +0200 Subject: [PATCH 08/11] Remove superfluous typing of defines --- client/src/cmdhfmfdes.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 3cf1ec2cc..ab20a46bd 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -70,9 +70,9 @@ #define MFDES_PC_MAX_ROUNDS 8U #define MFDES_PC_MAC_LEN 8U -#define MFDES_EV3C_MFC_KILL_KEY ((uint8_t)0x31U) -#define MFDES_EV3C_ALLOWED_DATA_PERMISSIONS ((uint8_t)0x11) -#define MFDES_EV3C_ALLOWED_TRAILER_PERMISSIONS ((uint8_t)0x1F) +#define MFDES_EV3C_MFC_KILL_KEY 0x31U +#define MFDES_EV3C_ALLOWED_DATA_PERMISSIONS 0x11U +#define MFDES_EV3C_ALLOWED_TRAILER_PERMISSIONS 0x1FU // DUOX ISO Internal Authenticate #define DUOX_INTAUTH_CHALLENGE_LEN 16 From b018054bcea1dbb69598901c57e9742e7b95f807 Mon Sep 17 00:00:00 2001 From: suut Date: Sun, 10 May 2026 23:28:27 +0200 Subject: [PATCH 09/11] Fix linter error --- client/src/cmdhfmfdes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index ab20a46bd..2b6f1b38f 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -6664,7 +6664,7 @@ static int CmdHF14ADesMakeMFCLicense(const char *Cmd) { license_len = 1 + 2*num_blocks; } - PrintAndLogEx(loglevel, "Covering %d sector%s in total", num_sectors, num_sectors != 1 ? "s" : ""); + PrintAndLogEx(loglevel, "Covering %lu sector%s in total", num_sectors, num_sectors != 1 ? "s" : ""); if (use_default_keys) { // by default: all key A FFFFFFFFFFFF, all key B readable From 6d458a9f49e9acc9ac07a37f011b2d70e3e2b382 Mon Sep 17 00:00:00 2001 From: suut Date: Sun, 10 May 2026 23:33:37 +0200 Subject: [PATCH 10/11] Remove the --force argument from hf mfdes auth --- client/src/cmdhfmfdes.c | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 2b6f1b38f..cdd2e69d7 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -3371,10 +3371,9 @@ static int CmdHF14ADesAuth(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes auth", "Select application on the card. It selects app if it is a valid one or returns an error.", - "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" - "hf mfdes auth -n 49 -t aes -k 00000000000000000000000000000000 --force -> permanently disable Mifare Classic functionality on a DESFire EV3C\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" "hf mfdes auth --aid 123456 -> select application 123456 and authenticate via parameters from `default` command\n" "hf mfdes auth --dfname D2760000850100 -n 0 -t aes -k 00000000000000000000000000000000 -> select DF by name and authenticate"); @@ -3382,7 +3381,6 @@ static int CmdHF14ADesAuth(const char *Cmd) { arg_param_begin, arg_lit0("a", "apdu", "Show APDU requests and responses"), arg_lit0("v", "verbose", "Verbose output"), - arg_lit0("f", "force", "Force irreversible operations"), arg_int0("n", "keyno", "", "Key number"), arg_str0("t", "algo", "", "Crypt algo"), arg_str0("k", "key", "", "Key for authenticate (HEX 8(DES), 16(2TDEA or AES) or 24(3TDEA) bytes)"), @@ -3401,29 +3399,22 @@ static int CmdHF14ADesAuth(const char *Cmd) { bool APDULogging = arg_get_lit(ctx, 1); bool verbose = arg_get_lit(ctx, 2); - bool force = arg_get_lit(ctx, 3); DesfireContext_t dctx = {0}; int securechann = defaultSecureChannel; uint32_t id = 0x000000; DesfireISOSelectWay selectway = ISW6bAID; - int res = CmdDesGetSessionParameters(ctx, &dctx, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, &securechann, DCMPlain, &id, &selectway); + int res = CmdDesGetSessionParameters(ctx, &dctx, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, &securechann, DCMPlain, &id, &selectway); if (res) { CLIParserFree(ctx); return res; } - bool save = arg_get_lit(ctx, 15); + bool save = arg_get_lit(ctx, 14); SetAPDULogging(APDULogging); CLIParserFree(ctx); - if (dctx.keyNum == MFDES_EV3C_MFC_KILL_KEY && !force) { - DropField(); - PrintAndLogEx(FAILED, "This operation would permanently disable the Mifare Classic functionality, use --force to override"); - return PM3_EOPABORTED; - } - res = DesfireSelectAndAuthenticateAppW(&dctx, securechann, selectway, id, false, verbose); if (res != PM3_SUCCESS) { DropField(); From 6661653316936432c80680196bd610e80449a032 Mon Sep 17 00:00:00 2001 From: suut Date: Sun, 10 May 2026 23:34:41 +0200 Subject: [PATCH 11/11] Remove extra spaces --- client/src/cmdhfmfdes.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index cdd2e69d7..3c51e0dd8 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -3371,9 +3371,9 @@ static int CmdHF14ADesAuth(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes auth", "Select application on the card. It selects app if it is a valid one or returns an error.", - "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" - "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --kdf none -> select PICC level and authenticate with key num=0, key type=des, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t aes -k 00000000000000000000000000000000 -> select PICC level and authenticate with key num=0, key type=aes, key=00..00 and key derivation = none\n" + "hf mfdes auth -n 0 -t des -k 0000000000000000 --save -> select PICC level and authenticate and in case of successful authentication - save channel parameters to defaults\n" "hf mfdes auth --aid 123456 -> select application 123456 and authenticate via parameters from `default` command\n" "hf mfdes auth --dfname D2760000850100 -n 0 -t aes -k 00000000000000000000000000000000 -> select DF by name and authenticate");