From e1152fc98a404b44713576085f79ca314274cd7f Mon Sep 17 00:00:00 2001 From: klks Date: Sun, 24 May 2026 19:12:12 +0800 Subject: [PATCH] Add native fmcos support to pm3 --- client/CMakeLists.txt | 1 + client/Makefile | 1 + client/src/cmdhf.c | 2 + client/src/cmdhffmcos.c | 3117 +++++++++++++++++++++++++++++++++++++++ client/src/cmdhffmcos.h | 26 + doc/fmcos.md | 882 +++++++++++ doc/fmcos_example.md | 697 +++++++++ 7 files changed, 4726 insertions(+) create mode 100644 client/src/cmdhffmcos.c create mode 100644 client/src/cmdhffmcos.h create mode 100644 doc/fmcos.md create mode 100644 doc/fmcos_example.md diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 08140fc54..6b471491e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -397,6 +397,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhfepa.c ${PM3_ROOT}/client/src/cmdhffelica.c ${PM3_ROOT}/client/src/cmdhffido.c + ${PM3_ROOT}/client/src/cmdhffmcos.c ${PM3_ROOT}/client/src/cmdhffudan.c ${PM3_ROOT}/client/src/cmdhfgallagher.c ${PM3_ROOT}/client/src/cmdhfgst.c diff --git a/client/Makefile b/client/Makefile index 0e2aec5da..6b24597c0 100644 --- a/client/Makefile +++ b/client/Makefile @@ -738,6 +738,7 @@ SRCS = mifare/aiddesfire.c \ cmdhfemrtd.c \ cmdhffelica.c \ cmdhffido.c \ + cmdhffmcos.c \ cmdhffudan.c \ cmdhfgallagher.c \ cmdhfgst.c \ diff --git a/client/src/cmdhf.c b/client/src/cmdhf.c index ca39f3f55..eb7ac64dc 100644 --- a/client/src/cmdhf.c +++ b/client/src/cmdhf.c @@ -33,6 +33,7 @@ #include "cmdhfemrtd.h" // eMRTD #include "cmdhffelica.h" // ISO18092 / FeliCa #include "cmdhffido.h" // FIDO authenticators +#include "cmdhffmcos.h" // FMCOS CPU cards #include "cmdhfsecc.h" // iClass SE Config Card #include "cmdhffudan.h" // Fudan cards #include "cmdhfgallagher.h" // Gallagher DESFire cards @@ -588,6 +589,7 @@ static command_t CommandTable[] = { {"emrtd", CmdHFeMRTD, AlwaysAvailable, "{ Machine Readable Travel Document... }"}, {"felica", CmdHFFelica, AlwaysAvailable, "{ ISO18092 / FeliCa RFIDs... }"}, {"fido", CmdHFFido, AlwaysAvailable, "{ FIDO and FIDO2 authenticators... }"}, + {"fmcos", CmdHFFmcos, AlwaysAvailable, "{ FMCOS CPU cards... }"}, {"fudan", CmdHFFudan, AlwaysAvailable, "{ Fudan RFIDs... }"}, {"gallagher", CmdHFGallagher, AlwaysAvailable, "{ Gallagher DESFire RFIDs... }"}, {"gst", CmdHFGST, AlwaysAvailable, "{ Google Smart Tap passes... }"}, diff --git a/client/src/cmdhffmcos.c b/client/src/cmdhffmcos.c new file mode 100644 index 000000000..891e25f6c --- /dev/null +++ b/client/src/cmdhffmcos.c @@ -0,0 +1,3117 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// Commands for FMCOS CPU smart cards (Fudan Microelectronics) +// Reference implementation: client/pyscripts/fmcos/ +//----------------------------------------------------------------------------- + +#include "cmdhffmcos.h" + +#include +#include +#include +#include "comms.h" +#include "cmdmain.h" +#include "util.h" +#include "ui.h" +#include "cliparser.h" +#include "cmdhf14a.h" +#include "iso7816/apduinfo.h" +#include "iso7816/iso7816core.h" +#include "protocols.h" +#include "mbedtls/des.h" +#include + +static int CmdHelp(const char *Cmd); + +// --------------------------------------------------------------------------- +// APDU / SW helpers +// --------------------------------------------------------------------------- + +// True when the RF field is on and a DF was selected with -k by the last command. +// When true, commands skip ISO14443-4 re-activation so the card state is preserved. +static bool g_fmcos_session_active = false; + +// Send a raw APDU over ISO14443-A. +// resp receives the full card response including SW1SW2. +// If g_fmcos_session_active is true and activate is true, the activation step is +// suppressed so the previously selected DF is not reset to MF. +static int fmcos_send_apdu(const uint8_t *apdu, size_t apdu_len, + bool activate, bool leave_on, + uint8_t *resp, int *resp_len) { + bool do_activate = activate && !g_fmcos_session_active; + int res = ExchangeAPDU14a(apdu, (int)apdu_len, do_activate, leave_on, + resp, APDU_RES_LEN, resp_len); + if (res != PM3_SUCCESS) { + g_fmcos_session_active = false; + PrintAndLogEx(ERR, "APDU exchange failed"); + return PM3_ESOFT; + } + g_fmcos_session_active = leave_on; + return PM3_SUCCESS; +} + +// Decode and print a SW1/SW2 status word. +static const char *fmcos_print_sw(uint8_t sw1, uint8_t sw2) { + const char *desc = "Unknown"; + + switch (sw1) { + case 0x62: + if (sw2 >= 0x02 && sw2 <= 0x80) { desc = "Triggering by the card"; break; } + switch (sw2) { + case 0x81: desc = "Part of returned data may be corrupted"; break; + case 0x82: desc = "End of file/record reached before reading Ne bytes"; break; + case 0x83: desc = "Selected file deactivated"; break; + case 0x84: desc = "FCI not formatted"; break; + case 0x85: desc = "Selected file in termination state"; break; + case 0x86: desc = "No input data from sensor"; break; + default: break; + } + break; + + case 0x63: + if (sw2 == 0x81) { desc = "File filled up by last write"; break; } + if ((sw2 & 0xF0) == 0xC0) { desc = "Counter (0-15) encoded in SW2 low nibble"; break; } + break; + + case 0x64: + if (sw2 >= 0x02 && sw2 <= 0x80) { desc = "Triggering by the card"; break; } + if (sw2 == 0x01) { desc = "Immediate response required by card"; break; } + break; + + case 0x65: + if (sw2 == 0x81) { desc = "Memory failure"; break; } + break; + + case 0x67: + if (sw2 == 0x00) { desc = "Invalid length"; break; } + break; + + case 0x68: + switch (sw2) { + case 0x81: desc = "Logical channel not supported"; break; + case 0x82: desc = "Secure messaging not supported"; break; + case 0x83: desc = "Last command of chain expected"; break; + case 0x84: desc = "Command chaining not supported"; break; + default: break; + } + break; + + case 0x69: + switch (sw2) { + case 0x81: desc = "Command incompatible with file structure"; break; + case 0x82: desc = "Security status not satisfied"; break; + case 0x83: desc = "Authentication method blocked"; break; + case 0x84: desc = "Reference data not usable"; break; + case 0x85: desc = "Conditions of use not satisfied"; break; + case 0x86: desc = "Command not allowed (no current EF)"; break; + case 0x87: desc = "Secure messaging data objects missing"; break; + case 0x88: desc = "Incorrect secure messaging data objects"; break; + default: break; + } + break; + + case 0x6A: + switch (sw2) { + case 0x80: desc = "Incorrect parameters in data field"; break; + case 0x81: desc = "Function not supported"; break; + case 0x82: desc = "File or application not found"; break; + case 0x83: desc = "Record not found"; break; + case 0x84: desc = "Not enough memory in file"; break; + case 0x85: desc = "Nc inconsistent with TLV structure"; break; + case 0x86: desc = "Incorrect parameters P1-P2"; break; + case 0x87: desc = "Nc inconsistent with parameters P1-P2"; break; + case 0x88: desc = "Referenced data not found"; break; + case 0x89: desc = "File already exists"; break; + case 0x8A: desc = "DF name already exists"; break; + default: break; + } + break; + + case 0x6D: + if (sw2 == 0x00) { desc = "Invalid INS"; break; } + break; + + case 0x6E: + if (sw2 == 0x00) { desc = "Invalid CLA"; break; } + break; + + case 0x93: + if (sw2 == 0x02) { desc = "Invalid MAC"; break; } + break; + + case 0x94: + switch (sw2) { + case 0x01: desc = "Insufficient balance"; break; + case 0x03: desc = "Key index not supported"; break; + default: break; + } + break; + + case 0x90: + if (sw2 == 0x00) { desc = "Success"; break; } + break; + + default: + break; + } + + if (sw1 == 0x90 && sw2 == 0x00) { + PrintAndLogEx(SUCCESS, "SW: " _GREEN_("%02X%02X") " - %s", sw1, sw2, desc); + } else { + PrintAndLogEx(WARNING, "SW: " _RED_("%02X%02X") " - %s", sw1, sw2, desc); + } + + return desc; +} + +// --------------------------------------------------------------------------- +// Crypto helpers (DES / 3DES ECB via mbedtls) +// --------------------------------------------------------------------------- + +// Encrypt one 8-byte block with DES or 3DES ECB depending on key length. +// key_len 8 -> single DES +// key_len 16 -> 3DES (two-key; if both halves equal, degrades to single DES) +// Returns 0 on success. +static int fmcos_ecb_encrypt(const uint8_t *key, size_t key_len, + const uint8_t *in, uint8_t *out) { + if (key_len == 8) { + mbedtls_des_context ctx; + mbedtls_des_init(&ctx); + mbedtls_des_setkey_enc(&ctx, key); + mbedtls_des_crypt_ecb(&ctx, in, out); + mbedtls_des_free(&ctx); + } else if (key_len == 16) { + // If both halves identical, use single DES with the first half + if (memcmp(key, key + 8, 8) == 0) { + mbedtls_des_context ctx; + mbedtls_des_init(&ctx); + mbedtls_des_setkey_enc(&ctx, key); + mbedtls_des_crypt_ecb(&ctx, in, out); + mbedtls_des_free(&ctx); + } else { + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + mbedtls_des3_set2key_enc(&ctx, key); + mbedtls_des3_crypt_ecb(&ctx, in, out); + mbedtls_des3_free(&ctx); + } + } else { + PrintAndLogEx(ERR, "Unsupported key length %zu (must be 8 or 16)", key_len); + return PM3_EINVARG; + } + return PM3_SUCCESS; +} + +// --------------------------------------------------------------------------- +// Crypto helpers - Phase 3 +// --------------------------------------------------------------------------- + +// Raw single-DES ECB encrypt (always 8-byte key, 8-byte block). +static void fmcos_des8_ecb_enc(const uint8_t key[8], const uint8_t in[8], uint8_t out[8]) { + mbedtls_des_context ctx; + mbedtls_des_init(&ctx); + mbedtls_des_setkey_enc(&ctx, key); + mbedtls_des_crypt_ecb(&ctx, in, out); + mbedtls_des_free(&ctx); +} + +// Raw single-DES ECB decrypt (always 8-byte key, 8-byte block). +static void fmcos_des8_ecb_dec(const uint8_t key[8], const uint8_t in[8], uint8_t out[8]) { + mbedtls_des_context ctx; + mbedtls_des_init(&ctx); + mbedtls_des_setkey_dec(&ctx, key); + mbedtls_des_crypt_ecb(&ctx, in, out); + mbedtls_des_free(&ctx); +} + +// ISO7816 pad: append 0x80 then zeros to reach a multiple of 8. +// out must have room for at least in_len + 8 bytes. Returns padded length. +static size_t fmcos_iso7816_pad(const uint8_t *in, size_t in_len, uint8_t *out) { + memcpy(out, in, in_len); + out[in_len] = 0x80; + size_t padded = in_len + 1; + while (padded % 8 != 0) + out[padded++] = 0x00; + return padded; +} + + +// DES CBC-MAC with ISO7816 padding. key must be exactly 8 bytes. +// iv is 8 bytes (typically the GET CHALLENGE response). +// Writes mac_len bytes (<= 8) into mac_out. +static void fmcos_des_mac(const uint8_t *buf, size_t buf_len, + const uint8_t key[8], + const uint8_t iv[8], + uint8_t *mac_out, size_t mac_len) { + size_t max_padded = buf_len + 8; + uint8_t *padded = calloc(max_padded, 1); + if (padded == NULL) + return; + + size_t padded_len = fmcos_iso7816_pad(buf, buf_len, padded); + + uint8_t val[8]; + memcpy(val, iv, 8); + + for (size_t i = 0; i < padded_len; i += 8) { + uint8_t xored[8]; + for (int j = 0; j < 8; j++) + xored[j] = val[j] ^ padded[i + j]; + fmcos_des8_ecb_enc(key, xored, val); + } + + free(padded); + + if (mac_len > 8) + mac_len = 8; + memcpy(mac_out, val, mac_len); +} + +// 3DES Retail MAC: DES-CBC-MAC with left-key-half (8 bytes), then decrypt +// with right-key-half, then re-encrypt with left-key-half. +// key must be exactly 16 bytes. Writes mac_len bytes (<= 8) into mac_out. +static void fmcos_3des_mac(const uint8_t *buf, size_t buf_len, + const uint8_t key[16], + const uint8_t iv[8], + uint8_t *mac_out, size_t mac_len) { + const uint8_t *key_l = key; + const uint8_t *key_r = key + 8; + + uint8_t val[8]; + fmcos_des_mac(buf, buf_len, key_l, iv, val, 8); + + uint8_t tmp[8]; + fmcos_des8_ecb_dec(key_r, val, tmp); + fmcos_des8_ecb_enc(key_l, tmp, val); + + if (mac_len > 8) + mac_len = 8; + memcpy(mac_out, val, mac_len); +} + + +// Build a 4-byte command MAC over CLA|INS|P1|P2|Lc[|data]. +// iv is the 8-byte GET CHALLENGE response used as the CBC IV. +// key_len 8 -> fmcos_des_mac; key_len 16 -> fmcos_3des_mac. +// Lc encodes payload_len + 4 (reserves space for the MAC itself). +static void fmcos_packet_mac(uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2, + const uint8_t *data, size_t data_len, + const uint8_t *iv, + const uint8_t *key, size_t key_len, + uint8_t *mac_out) { + size_t buf_len = 5 + data_len; + uint8_t *mac_buf = calloc(buf_len, 1); + if (mac_buf == NULL) + return; + + mac_buf[0] = cla; + mac_buf[1] = ins; + mac_buf[2] = p1; + mac_buf[3] = p2; + mac_buf[4] = (uint8_t)((data_len + 4) & 0xFF); + if (data_len > 0) + memcpy(&mac_buf[5], data, data_len); + + if (key_len == 8) { + fmcos_des_mac(mac_buf, buf_len, key, iv, mac_out, 4); + } else { + fmcos_3des_mac(mac_buf, buf_len, key, iv, mac_out, 4); + } + + free(mac_buf); +} + +// --------------------------------------------------------------------------- +// Crypto helpers - Phase 5 (encrypt / decrypt for secure channel) +// --------------------------------------------------------------------------- + +// Remove ISO7816 padding: scan backwards for 0x80 marker. +static int fmcos_iso7816_unpad(const uint8_t *buf, size_t padded_len, size_t *out_len) { + if (padded_len == 0 || padded_len % 8 != 0) + return PM3_ESOFT; + for (size_t i = padded_len; i > 0; i--) { + if (buf[i - 1] == 0x80) { *out_len = i - 1; return PM3_SUCCESS; } + if (buf[i - 1] != 0x00) break; + } + PrintAndLogEx(ERR, "ISO7816 padding marker not found"); + return PM3_ESOFT; +} + +// Pad with ISO7816 and ECB-encrypt (DES or 3DES) the full buffer. +// out must hold at least data_len + 8 bytes. Returns encrypted length. +static size_t fmcos_encrypt(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, uint8_t *out) { + uint8_t padded[512] = {0}; + size_t padded_len = fmcos_iso7816_pad(data, data_len, padded); + for (size_t off = 0; off < padded_len; off += 8) + fmcos_ecb_encrypt(key, key_len, padded + off, out + off); + return padded_len; +} + +// ECB-decrypt (DES or 3DES) then remove ISO7816 padding. +// out must be at least data_len bytes. *out_len receives unpadded length. +static int fmcos_decrypt(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, + uint8_t *out, size_t *out_len) { + if (data_len == 0 || data_len % 8 != 0) { + PrintAndLogEx(ERR, "Encrypted length must be a non-zero multiple of 8"); + return PM3_ESOFT; + } + for (size_t off = 0; off < data_len; off += 8) { + if (key_len == 8) { + fmcos_des8_ecb_dec(key, data + off, out + off); + } else { + mbedtls_des3_context c3; + mbedtls_des3_init(&c3); + mbedtls_des3_set2key_dec(&c3, key); + mbedtls_des3_crypt_ecb(&c3, data + off, out + off); + mbedtls_des3_free(&c3); + } + } + return fmcos_iso7816_unpad(out, data_len, out_len); +} + +// --------------------------------------------------------------------------- +// Card-level primitives +// --------------------------------------------------------------------------- + +// SELECT FILE by 2-byte file ID. +static int fmcos_select(uint16_t file_id, bool activate, bool leave_on, + uint8_t *resp, int *resp_len) { + uint8_t apdu[7]; + apdu[0] = 0x00; + apdu[1] = 0xA4; + apdu[2] = 0x00; + apdu[3] = 0x00; + apdu[4] = 0x02; + apdu[5] = (file_id >> 8) & 0xFF; + apdu[6] = file_id & 0xFF; + return fmcos_send_apdu(apdu, sizeof(apdu), activate, leave_on, resp, resp_len); +} + +// SELECT by AID / DF name. +static int fmcos_select_by_name(const uint8_t *name, size_t name_len, + bool activate, bool leave_on, + uint8_t *resp, int *resp_len) { + if (name_len == 0 || name_len > 16) { + PrintAndLogEx(ERR, "AID must be 1-16 bytes"); + return PM3_EINVARG; + } + uint8_t apdu[21]; + apdu[0] = 0x00; + apdu[1] = 0xA4; + apdu[2] = 0x04; + apdu[3] = 0x00; + apdu[4] = (uint8_t)name_len; + memcpy(&apdu[5], name, name_len); + return fmcos_send_apdu(apdu, 5 + name_len, activate, leave_on, resp, resp_len); +} + +// GET CHALLENGE - request a random nonce from the card. +// chal_len must be 4 or 8. Always writes 8 bytes into chal_out +// (4-byte responses are zero-padded to 8 per FMCOS spec). +// activate: pass true when the RF field is not yet on. +static int fmcos_get_challenge(uint8_t chal_len, bool activate, uint8_t *chal_out) { + if (chal_len != 4 && chal_len != 8) { + PrintAndLogEx(ERR, "Challenge length must be 4 or 8"); + return PM3_EINVARG; + } + + uint8_t apdu[5] = {0x00, 0x84, 0x00, 0x00, chal_len}; + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + int res = fmcos_send_apdu(apdu, sizeof(apdu), activate, true, resp, &resp_len); + if (res != PM3_SUCCESS) + return res; + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Empty response to GET CHALLENGE"); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + return PM3_ESOFT; + } + + int data_len = resp_len - 2; + if (data_len != (int)chal_len) { + PrintAndLogEx(ERR, "Expected %d challenge bytes, got %d", chal_len, data_len); + return PM3_ESOFT; + } + + memset(chal_out, 0x00, 8); + memcpy(chal_out, resp, data_len); + return PM3_SUCCESS; +} + +// Simple TLV walker: find first occurrence of a 1-byte tag. +static const uint8_t *fmcos_tlv_find(const uint8_t *buf, size_t len, + uint8_t tag, size_t *vlen) { + size_t i = 0; + while (i + 1 < len) { + uint8_t t = buf[i++]; + uint8_t l = buf[i++]; + if (i + l > len) + break; + if (t == tag) { + *vlen = l; + return &buf[i]; + } + i += l; + } + return NULL; +} + +// --------------------------------------------------------------------------- +// hf fmcos info (Phase 1) +// --------------------------------------------------------------------------- + +static int CmdHFFmcosInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos info", + "Detect and print information about an FMCOS CPU card.\n" + "Selects the Master File (3F00) and parses the FCI TLV response.", + "hf fmcos info\n" + "hf fmcos info -a"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool apdu_log = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + SetAPDULogging(apdu_log); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + int res = fmcos_select(0x3F00, true, false, resp, &resp_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "No card in field or APDU exchange failed"); + DropField(); + return res; + } + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Card returned empty response"); + DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("FMCOS Card Information") " ---"); + + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + PrintAndLogEx(WARNING, "SELECT MF (3F00) failed - may not be an FMCOS card"); + DropField(); + return PM3_ESOFT; + } + + PrintAndLogEx(SUCCESS, "SELECT MF (3F00) " _GREEN_("OK")); + + int data_len = resp_len - 2; + if (data_len > 0) { + PrintAndLogEx(INFO, "FCI: %s", sprint_hex(resp, (size_t)data_len)); + + size_t outer_len = 0; + const uint8_t *outer = fmcos_tlv_find(resp, (size_t)data_len, 0x6F, &outer_len); + + if (outer != NULL) { + size_t dfname_len = 0; + const uint8_t *dfname = fmcos_tlv_find(outer, outer_len, 0x84, &dfname_len); + if (dfname != NULL) { + PrintAndLogEx(INFO, "DF Name (84): %s", sprint_hex(dfname, dfname_len)); + } + + size_t prop_len = 0; + const uint8_t *prop = fmcos_tlv_find(outer, outer_len, 0xA5, &prop_len); + if (prop != NULL) { + size_t sfi_len = 0; + const uint8_t *sfi = fmcos_tlv_find(prop, prop_len, 0x88, &sfi_len); + if (sfi != NULL && sfi_len >= 1) { + PrintAndLogEx(INFO, "SFI (88): " _YELLOW_("%02X"), sfi[0]); + } + + // 9F0C is a 2-byte tag - walk manually + for (size_t i = 0; i + 3 < prop_len; i++) { + if (prop[i] == 0x9F && prop[i + 1] == 0x0C) { + uint8_t vl = prop[i + 2]; + if (i + 3 + vl <= prop_len) { + PrintAndLogEx(INFO, "Issuer ID (9F0C): %s", + sprint_hex(&prop[i + 3], vl)); + } + break; + } + } + } + } + } + + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf fmcos select --id 3f00") "` to navigate the file system"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf fmcos auth external --id 0 --key ffffffffffffffff") "` to authenticate"); + PrintAndLogEx(NORMAL, ""); + + DropField(); + return PM3_SUCCESS; +} + +// --------------------------------------------------------------------------- +// hf fmcos select (Phase 2) +// --------------------------------------------------------------------------- + +static int CmdHFFmcosSelect(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos select", + "SELECT FILE by 2-byte ID or AID name.\n" + "Activates the RF field, selects the file, then drops the field.", + "hf fmcos select --id 3f00\n" + "hf fmcos select --id 5f00 -v\n" + "hf fmcos select --name 325041592e5359532e4444463031"); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "id", "", "2-byte file ID (4 hex chars)"), + arg_str0(NULL, "name", "", "AID / DF name bytes (up to 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("v", "verbose", "print full FCI TLV response"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[2] = {0}; + int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + + uint8_t name_buf[16] = {0}; + int name_len = 0; + CLIGetHexWithReturn(ctx, 2, name_buf, &name_len); + + bool keep = arg_get_lit(ctx, 3); + bool verbose = arg_get_lit(ctx, 4); + bool apdu_log = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (id_len == 0 && name_len == 0) { + PrintAndLogEx(ERR, "Provide either --id or --name"); + return PM3_EINVARG; + } + if (id_len > 0 && id_len != 2) { + PrintAndLogEx(ERR, "--id must be exactly 2 bytes (4 hex chars)"); + return PM3_EINVARG; + } + + SetAPDULogging(apdu_log); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res; + + if (name_len > 0) { + res = fmcos_select_by_name(name_buf, (size_t)name_len, true, keep, resp, &resp_len); + } else { + uint16_t file_id = ((uint16_t)id_buf[0] << 8) | id_buf[1]; + res = fmcos_select(file_id, true, keep, resp, &resp_len); + } + + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Empty card response"); + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 == 0x90 && sw2 == 0x00 && verbose) { + int data_len = resp_len - 2; + if (data_len > 0) { + PrintAndLogEx(INFO, "FCI: %s", sprint_hex(resp, (size_t)data_len)); + } + } + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// --------------------------------------------------------------------------- +// hf fmcos auth external (Phase 2) +// --------------------------------------------------------------------------- + +static int CmdHFFmcosAuthExternal(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos auth external", + "EXTERNAL AUTHENTICATE.\n" + "Requests a challenge from the card, encrypts it with the provided\n" + "DES (8-byte) or 3DES (16-byte) key, then sends EXTERNAL AUTHENTICATE.", + "hf fmcos auth external --id 0 --key ffffffffffffffff\n" + "hf fmcos auth external --id 1 --key 0102030405060708090a0b0c0d0e0f10"); + + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "key ID (P2 in EXTERNAL AUTHENTICATE)"), + arg_str1(NULL, "key", "", "DES key (8 bytes) or 3DES key (16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int key_id = (int)strtol(arg_get_str(ctx, 1)->sval[0], NULL, 16); + + uint8_t key[16] = {0}; + int key_len = 0; + CLIGetHexWithReturn(ctx, 2, key, &key_len); + + bool keep = arg_get_lit(ctx, 3); + bool apdu_log = arg_get_lit(ctx, 4); + CLIParserFree(ctx); + + if (key_id < 0 || key_id > 0xFF) { + PrintAndLogEx(ERR, "Key ID must be 0-255"); + return PM3_EINVARG; + } + if (key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "Key must be 8 bytes (DES) or 16 bytes (3DES)"); + return PM3_EINVARG; + } + + SetAPDULogging(apdu_log); + + // GET CHALLENGE (8 bytes) - activates the field + uint8_t challenge[8] = {0}; + int res = fmcos_get_challenge(8, true, challenge); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + PrintAndLogEx(INFO, "Challenge: %s", sprint_hex(challenge, 8)); + + // Encrypt challenge with the key (DES or 3DES ECB, single 8-byte block) + uint8_t encrypted[8] = {0}; + res = fmcos_ecb_encrypt(key, (size_t)key_len, challenge, encrypted); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + // EXTERNAL AUTHENTICATE: CLA=00 INS=82 P1=00 P2=key_id Lc=08 Data=encrypted + uint8_t ea_apdu[13]; + ea_apdu[0] = 0x00; + ea_apdu[1] = 0x82; + ea_apdu[2] = 0x00; + ea_apdu[3] = (uint8_t)key_id; + ea_apdu[4] = 0x08; + memcpy(&ea_apdu[5], encrypted, 8); + + uint8_t ea_resp[APDU_RES_LEN] = {0}; + int ea_resp_len = 0; + res = fmcos_send_apdu(ea_apdu, sizeof(ea_apdu), false, keep, ea_resp, &ea_resp_len); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + if (ea_resp_len < 2) { + PrintAndLogEx(ERR, "Empty response to EXTERNAL AUTHENTICATE"); + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = ea_resp[ea_resp_len - 2]; + uint8_t sw2 = ea_resp[ea_resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 == 0x90 && sw2 == 0x00) { + PrintAndLogEx(SUCCESS, "External authentication " _GREEN_("successful")); + } else { + PrintAndLogEx(FAILED, "External authentication " _RED_("failed")); + } + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// --------------------------------------------------------------------------- +// hf fmcos auth internal (Phase 2) +// --------------------------------------------------------------------------- + +static int CmdHFFmcosAuthInternal(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos auth internal", + "INTERNAL AUTHENTICATE.\n" + "Sends a challenge to the card and the card proves it knows the key.", + "hf fmcos auth internal --p1 00 --p2 00 --data 0102030405060708"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "p1", "<0-255>", "P1 parameter (default 0)"), + arg_int0(NULL, "p2", "<0-255>", "P2 parameter / key ID (default 0)"), + arg_str1(NULL, "data", "", "challenge data (8 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int p1 = arg_get_int_def(ctx, 1, 0); + int p2 = arg_get_int_def(ctx, 2, 0); + + uint8_t data[32] = {0}; + int data_len = 0; + CLIGetHexWithReturn(ctx, 3, data, &data_len); + + bool keep = arg_get_lit(ctx, 4); + bool apdu_log = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (data_len == 0) { + PrintAndLogEx(ERR, "Provide challenge --data (8 bytes)"); + return PM3_EINVARG; + } + + SetAPDULogging(apdu_log); + + // INTERNAL AUTHENTICATE: CLA=00 INS=88 + uint8_t apdu[5 + 32]; + apdu[0] = 0x00; + apdu[1] = 0x88; + apdu[2] = (uint8_t)p1; + apdu[3] = (uint8_t)p2; + apdu[4] = (uint8_t)data_len; + memcpy(&apdu[5], data, (size_t)data_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, (size_t)(5 + data_len), true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Empty response to INTERNAL AUTHENTICATE"); + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + int rdata_len = resp_len - 2; + if (rdata_len > 0) { + PrintAndLogEx(INFO, "Response: %s", sprint_hex(resp, (size_t)rdata_len)); + } + + if (sw1 == 0x90 && sw2 == 0x00) { + PrintAndLogEx(SUCCESS, "Internal authentication " _GREEN_("successful")); + } else { + PrintAndLogEx(FAILED, "Internal authentication " _RED_("failed")); + } + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// --------------------------------------------------------------------------- +// hf fmcos auth - sub-dispatcher (Phase 2) +// --------------------------------------------------------------------------- + +static int CmdHFFmcosAuthHelp(const char *Cmd); + +static command_t AuthCommandTable[] = { + {"help", CmdHFFmcosAuthHelp, AlwaysAvailable, "This help"}, + {"external", CmdHFFmcosAuthExternal, IfPm3Iso14443a, "EXTERNAL AUTHENTICATE using DES/3DES key"}, + {"internal", CmdHFFmcosAuthInternal, IfPm3Iso14443a, "INTERNAL AUTHENTICATE (card proves key knowledge)"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHFFmcosAuthHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(AuthCommandTable); + return PM3_SUCCESS; +} + +static int CmdHFFmcosAuth(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(AuthCommandTable, Cmd); +} + +// --------------------------------------------------------------------------- +// Phase 4 - File management +// --------------------------------------------------------------------------- + +// Protection level constants shared by create-file, read, write, write-key. +#define FMCOS_PROT_NONE 0 +#define FMCOS_PROT_MAC 0x80 +#define FMCOS_PROT_ENC 0xC0 + +// Shared inner helper: execute an UPDATE BINARY / UPDATE RECORD / APPEND RECORD +// (or WRITE KEY) APDU with optional MAC or encrypt-then-MAC protection. +// cla: base class byte (0x00 for data commands, 0x80 for proprietary). +// data/data_len: application payload before any protection transforms. +// prot: FMCOS_PROT_NONE, FMCOS_PROT_MAC, or FMCOS_PROT_ENC. +// activate: true to wake the card (passed to GET CHALLENGE if protected, else +// to the write APDU directly). +static int fmcos_write_cmd(uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2, + const uint8_t *data, size_t data_len, + int prot, const uint8_t *key, size_t key_len, + bool activate, bool leave_on, + uint8_t *resp_out, int *resp_len_out) { + uint8_t payload[260] = {0}; + size_t payload_len = 0; + + if (prot == FMCOS_PROT_ENC) { + uint8_t enc_in[256]; + enc_in[0] = (uint8_t)(data_len & 0xFF); + memcpy(enc_in + 1, data, data_len); + payload_len = fmcos_encrypt(key, key_len, enc_in, 1 + data_len, payload); + cla |= 0x04; + } else { + memcpy(payload, data, data_len); + payload_len = data_len; + if (prot == FMCOS_PROT_MAC) + cla |= 0x04; + } + + if (prot != FMCOS_PROT_NONE) { + uint8_t chal[8] = {0}; + int res = fmcos_get_challenge(8, activate, chal); + if (res != PM3_SUCCESS) + return res; + activate = false; + + uint8_t mac[4] = {0}; + fmcos_packet_mac(cla, ins, p1, p2, payload, payload_len, chal, key, key_len, mac); + memcpy(payload + payload_len, mac, 4); + payload_len += 4; + } + + uint8_t apdu[270]; + apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; + apdu[4] = (uint8_t)payload_len; + memcpy(apdu + 5, payload, payload_len); + return fmcos_send_apdu(apdu, 5 + payload_len, activate, leave_on, resp_out, resp_len_out); +} + +// Shared inner helper: execute READ BINARY or READ RECORD with optional +// MAC or encrypt protection. On success, decrypted (or raw) data is written +// to data_out and *data_out_len receives the byte count. +// The response MAC is verified when prot != FMCOS_PROT_NONE. +static int fmcos_read_cmd(uint8_t ins, uint8_t p1, uint8_t p2, uint8_t read_len, + int prot, const uint8_t *key, size_t key_len, + bool activate, bool leave_on, + uint8_t *data_out, int *data_out_len) { + uint8_t cla = 0x00; + uint8_t mac_iv[8] = {0}; + + if (prot != FMCOS_PROT_NONE) { + cla = 0x04; + int res = fmcos_get_challenge(8, activate, mac_iv); + if (res != PM3_SUCCESS) + return res; + activate = false; + } + + uint8_t apdu[11]; + size_t apdu_len; + if (prot != FMCOS_PROT_NONE) { + // Case 4: CLA INS P1 P2 Lc=4 [MAC4] Le + uint8_t mac[4] = {0}; + fmcos_packet_mac(cla, ins, p1, p2, NULL, 0, mac_iv, key, key_len, mac); + apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; + apdu[4] = 0x04; + memcpy(apdu + 5, mac, 4); + apdu[9] = read_len; + apdu_len = 10; + } else { + // Case 2: CLA INS P1 P2 Le + apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; + apdu[4] = read_len; + apdu_len = 5; + } + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, apdu_len, activate, leave_on, resp, &resp_len); + if (res != PM3_SUCCESS) + return res; + if (resp_len < 2) + return PM3_ESOFT; + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 != 0x90 || sw2 != 0x00) + return PM3_ESOFT; + + int data_len = resp_len - 2; + + if (prot == FMCOS_PROT_NONE) { + memcpy(data_out, resp, (size_t)data_len); + *data_out_len = data_len; + return PM3_SUCCESS; + } + + // Protected: response = [msg_bytes][mac4] + if (data_len < 4) { + PrintAndLogEx(ERR, "Protected response too short"); + return PM3_ESOFT; + } + int msg_len = data_len - 4; + uint8_t *ret_mac = resp + msg_len; + + uint8_t calc_mac[4] = {0}; + if (key_len == 8) + fmcos_des_mac(resp, (size_t)msg_len, key, mac_iv, calc_mac, 4); + else + fmcos_3des_mac(resp, (size_t)msg_len, key, mac_iv, calc_mac, 4); + + if (memcmp(calc_mac, ret_mac, 4) != 0) { + PrintAndLogEx(ERR, "Response MAC " _RED_("mismatch")); + return PM3_ESOFT; + } + PrintAndLogEx(DEBUG, "Response MAC " _GREEN_("verified")); + + if (prot == FMCOS_PROT_ENC) { + uint8_t plain[256] = {0}; + size_t plain_len = 0; + res = fmcos_decrypt(key, key_len, resp, (size_t)msg_len, plain, &plain_len); + if (res != PM3_SUCCESS) + return res; + if (plain_len == 0) { + PrintAndLogEx(ERR, "Decrypted length byte missing"); + return PM3_ESOFT; + } + uint8_t actual_len = plain[0]; + memcpy(data_out, plain + 1, actual_len); + *data_out_len = (int)actual_len; + } else { + memcpy(data_out, resp, (size_t)msg_len); + *data_out_len = msg_len; + } + return PM3_SUCCESS; +} + +static int CmdHFFmcosErase(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos erase", + "ERASE DF - erase all EFs in the currently selected DF", + "hf fmcos erase"); + void *argtable[] = { + arg_param_begin, + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool keep = arg_get_lit(ctx, 1); + bool apdu_log = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + SetAPDULogging(apdu_log); + + // CLA=80 INS=0E P1=00 P2=00 (no data, trailing 00 = Le/case-1 sentinel) + uint8_t apdu[5] = {0x80, 0x0E, 0x00, 0x00, 0x00}; + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, sizeof(apdu), true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "DF " _GREEN_("erased")); + else + PrintAndLogEx(FAILED, "Erase " _RED_("failed")); + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosCreateDir(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos create dir", + "CREATE DIRECTORY (DF) inside the currently selected directory", + "hf fmcos create dir --id 3F01 --space 200 --cperm F0 --eperm F0 --appid 95 --name 77616C6C6574546573740A"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "<4hex>", "2-byte file ID"), + arg_str1(NULL, "space", "", "space to allocate for DF (bytes, hex, e.g. 200 = 512)"), + arg_str1(NULL, "cperm", "", "create permission byte"), + arg_str1(NULL, "eperm", "", "erase permission byte"), + arg_str1(NULL, "appid", "", "application ID byte"), + arg_str0(NULL, "name", "", "DF name / AID, 0-16 bytes"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[2] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + int space = (int)strtol(arg_get_str(ctx, 2)->sval[0], NULL, 16); + uint8_t cperm[1] = {0}; int cperm_len = 0; + CLIGetHexWithReturn(ctx, 3, cperm, &cperm_len); + uint8_t eperm[1] = {0}; int eperm_len = 0; + CLIGetHexWithReturn(ctx, 4, eperm, &eperm_len); + uint8_t appid[1] = {0}; int appid_len = 0; + CLIGetHexWithReturn(ctx, 5, appid, &appid_len); + uint8_t name[16] = {0}; int name_len = 0; + CLIGetHexWithReturn(ctx, 6, name, &name_len); + bool keep = arg_get_lit(ctx, 7); + bool apdu_log = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (id_len != 2) { PrintAndLogEx(ERR, "--id must be 2 bytes"); return PM3_EINVARG; } + if (space < 1 || space > 0xFFFF) { PrintAndLogEx(ERR, "--space out of range"); return PM3_EINVARG; } + if (cperm_len != 1 || eperm_len != 1 || appid_len != 1) { + PrintAndLogEx(ERR, "--cperm, --eperm, --appid must each be 1 byte"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + uint8_t data[25] = {0}; + size_t data_len = 0; + data[data_len++] = 0x38; + data[data_len++] = (space >> 8) & 0xFF; + data[data_len++] = space & 0xFF; + data[data_len++] = cperm[0]; + data[data_len++] = eperm[0]; + data[data_len++] = appid[0]; + data[data_len++] = 0xFF; + data[data_len++] = 0xFF; + if (name_len > 0) { memcpy(data + data_len, name, (size_t)name_len); data_len += (size_t)name_len; } + + uint8_t apdu[30]; + apdu[0] = 0x80; apdu[1] = 0xE0; apdu[2] = id_buf[0]; apdu[3] = id_buf[1]; + apdu[4] = (uint8_t)data_len; + memcpy(apdu + 5, data, data_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 5 + data_len, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Directory " _GREEN_("created")); + else + PrintAndLogEx(FAILED, "Create directory " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// Option lists shared by create-file, read, write, write-key +static const CLIParserOption g_fmcos_filetype_opts[] = { + {0x28, "bin"}, + {0x2A, "fix"}, + {0x2C, "var"}, + {0x2E, "loop"}, + {0x2F, "wallet"}, + {0, NULL} +}; + +static const CLIParserOption g_fmcos_prot_opts[] = { + {FMCOS_PROT_NONE, "none"}, + {FMCOS_PROT_MAC, "mac"}, + {FMCOS_PROT_ENC, "enc"}, + {0, NULL} +}; + +static const CLIParserOption g_fmcos_baltype_opts[] = { + {0x01, "passbook"}, + {0x02, "wallet"}, + {0, NULL} +}; + +static int CmdHFFmcosCreateFile(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos create file", + "CREATE EF in the current DF\n" + "Types: bin(0x28) fix(0x2A) var(0x2C) loop(0x2E) wallet(0x2F)\n" + "Prot: none mac(0x80) enc(0xC0) -- ORed into the type byte\n" + "For wallet/passbook type: --rperm=usage rights, --wperm ignored (EDEP write always 0x00),\n" + "--access=loop file link (low byte of the linked loop EF's file ID)", + "hf fmcos create file --id 0101 --type bin --size 32 --rperm FF --wperm FF --access 00\n" + "hf fmcos create file --id 0018 --type loop --size 0517 --rperm F0 --wperm EF --access FF\n" + "hf fmcos create file --id 0002 --type wallet --size 0208 --rperm F0 --wperm 00 --access 18"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "<4hex>", "2-byte file ID"), + arg_str1(NULL, "type", "", "file type: bin fix var loop wallet"), + arg_str1(NULL, "size", "", "file size in bytes (hex, e.g. 0208 = 520)"), + arg_str1(NULL, "rperm", "", "read permission byte"), + arg_str1(NULL, "wperm", "", "write permission byte"), + arg_str1(NULL, "access", "", "access rights byte"), + arg_str0(NULL, "prot", "", "line protection: none(def) mac enc"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[2] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + int ftype = 0; + if (CLIGetOptionList(arg_get_str(ctx, 2), g_fmcos_filetype_opts, &ftype)) { CLIParserFree(ctx); return PM3_EINVARG; } + int size = (int)strtol(arg_get_str(ctx, 3)->sval[0], NULL, 16); + uint8_t rperm[1] = {0}; int rperm_len = 0; + CLIGetHexWithReturn(ctx, 4, rperm, &rperm_len); + uint8_t wperm[1] = {0}; int wperm_len = 0; + CLIGetHexWithReturn(ctx, 5, wperm, &wperm_len); + uint8_t access[1] = {0}; int access_len = 0; + CLIGetHexWithReturn(ctx, 6, access, &access_len); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 7), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + bool keep = arg_get_lit(ctx, 8); + bool apdu_log = arg_get_lit(ctx, 9); + CLIParserFree(ctx); + + if (id_len != 2) { PrintAndLogEx(ERR, "--id must be 2 bytes"); return PM3_EINVARG; } + if (size < 1 || size > 0xFFFF) { PrintAndLogEx(ERR, "--size out of range"); return PM3_EINVARG; } + if (rperm_len != 1 || wperm_len != 1 || access_len != 1) { + PrintAndLogEx(ERR, "--rperm, --wperm, --access must each be 1 byte"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + // For wallet/passbook (0x2F): [type][size_hi][size_lo][usage_rights][0x00][0xFF][loop_file_id] + // byte[4] is always 0x00 -- EDEP write permission is fixed; balance written via financial APDUs only + // byte[6] is the low byte of the linked loop EF's file ID (e.g. 0x18 for loop file 0x0018) + // For all other types: [type|prot][size_hi][size_lo][rperm][wperm][0xFF][access] + uint8_t data[7] = { + (uint8_t)(ftype | prot), + (size >> 8) & 0xFF, size & 0xFF, + rperm[0], + (ftype == 0x2F) ? 0x00 : wperm[0], + 0xFF, + access[0] + }; + uint8_t apdu[12]; + apdu[0] = 0x80; apdu[1] = 0xE0; apdu[2] = id_buf[0]; apdu[3] = id_buf[1]; + apdu[4] = 7; + memcpy(apdu + 5, data, 7); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 12, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "File " _GREEN_("created")); + else + PrintAndLogEx(FAILED, "Create file " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosCreateKeyfile(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos create keyfile", + "CREATE KEYFILE in the current DF", + "hf fmcos create keyfile --id 0000 --space 200 --dfsid 95 --perm F0"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "<4hex>", "2-byte file ID (usually 0000)"), + arg_str1(NULL, "space", "", "space to allocate (bytes, hex, e.g. 200 = 512)"), + arg_str1(NULL, "dfsid", "", "DF SID byte"), + arg_str1(NULL, "perm", "", "key permission byte"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[2] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + int space = (int)strtol(arg_get_str(ctx, 2)->sval[0], NULL, 16); + uint8_t dfsid[1] = {0}; int dfsid_len = 0; + CLIGetHexWithReturn(ctx, 3, dfsid, &dfsid_len); + uint8_t perm[1] = {0}; int perm_len = 0; + CLIGetHexWithReturn(ctx, 4, perm, &perm_len); + bool keep = arg_get_lit(ctx, 5); + bool apdu_log = arg_get_lit(ctx, 6); + CLIParserFree(ctx); + + if (id_len != 2) { PrintAndLogEx(ERR, "--id must be 2 bytes"); return PM3_EINVARG; } + if (space < 1 || space > 0xFFFF) { PrintAndLogEx(ERR, "--space out of range"); return PM3_EINVARG; } + if (dfsid_len != 1 || perm_len != 1) { + PrintAndLogEx(ERR, "--dfsid and --perm must each be 1 byte"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + // data = [0x3F][space_hi][space_lo][dfsid][perm][0xFF][0xFF] + uint8_t data[7] = { + 0x3F, + (space >> 8) & 0xFF, space & 0xFF, + dfsid[0], perm[0], 0xFF, 0xFF + }; + uint8_t apdu[12]; + apdu[0] = 0x80; apdu[1] = 0xE0; apdu[2] = id_buf[0]; apdu[3] = id_buf[1]; + apdu[4] = 7; + memcpy(apdu + 5, data, 7); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 12, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Keyfile " _GREEN_("created")); + else + PrintAndLogEx(FAILED, "Create keyfile " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosCreateHelp(const char *Cmd); + +static command_t CreateCommandTable[] = { + {"help", CmdHFFmcosCreateHelp, AlwaysAvailable, "This help"}, + {"dir", CmdHFFmcosCreateDir, IfPm3Iso14443a, "CREATE DIRECTORY (DF)"}, + {"file", CmdHFFmcosCreateFile, IfPm3Iso14443a, "CREATE EF (binary / fixed / variable / loop / wallet)"}, + {"keyfile", CmdHFFmcosCreateKeyfile, IfPm3Iso14443a, "CREATE KEYFILE"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHFFmcosCreateHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(CreateCommandTable); + return PM3_SUCCESS; +} + +static int CmdHFFmcosCreate(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CreateCommandTable, Cmd); +} + +// --------------------------------------------------------------------------- +// Phase 5 - Data access +// --------------------------------------------------------------------------- + +static int CmdHFFmcosReadBinary(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos read binary", + "READ BINARY from the current transparent EF\n" + "p1/p2 encode the file offset (p1=offset_hi, p2=offset_lo).\n" + "Protection: none(def) mac enc", + "hf fmcos read binary --p1 00 --p2 00 --len 16\n" + "hf fmcos read binary --p1 00 --p2 00 --len 16 --prot mac --key aabbccddeeff0011"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "p1", "", "P1 byte (offset high)"), + arg_str1(NULL, "p2", "", "P2 byte (offset low)"), + arg_int1(NULL, "len", "", "number of bytes to read (0 = read all)"), + arg_str0(NULL, "prot", "","protection: none(def) mac enc"), + arg_str0(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t p1b[1] = {0}; int p1_len = 0; + CLIGetHexWithReturn(ctx, 1, p1b, &p1_len); + uint8_t p2b[1] = {0}; int p2_len = 0; + CLIGetHexWithReturn(ctx, 2, p2b, &p2_len); + int rlen = arg_get_int_def(ctx, 3, 0); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 4), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 5, key, &key_len); + bool keep = arg_get_lit(ctx, 6); + bool apdu_log = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if (p1_len != 1 || p2_len != 1) { PrintAndLogEx(ERR, "--p1 and --p2 must each be 1 byte"); return PM3_EINVARG; } + if (rlen < 0 || rlen > 255) { PrintAndLogEx(ERR, "--len must be 0-255"); return PM3_EINVARG; } + if (prot != FMCOS_PROT_NONE) { + if (key_len != 8 && key_len != 16) { PrintAndLogEx(ERR, "--key must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; } + } + SetAPDULogging(apdu_log); + + uint8_t data_out[256] = {0}; + int data_out_len = 0; + int res = fmcos_read_cmd(0xB0, p1b[0], p2b[0], (uint8_t)rlen, + prot, key, (size_t)key_len, + true, keep, data_out, &data_out_len); + if (res == PM3_SUCCESS) + PrintAndLogEx(SUCCESS, "Data: " _GREEN_("%s"), sprint_hex(data_out, (size_t)data_out_len)); + if (!keep) DropField(); + return res; +} + +static int CmdHFFmcosReadRecord(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos read record", + "READ RECORD from the current record-based EF\n" + "Protection: none(def) mac enc\n" + "Use --tlv for variable-length (VAR) files: requests 2 extra bytes and strips the 00[len] TLV prefix from the response.", + "hf fmcos read record --rec 01 --fid 01 --len 20\n" + "hf fmcos read record --rec 01 --fid 06 --len 16 --tlv\n" + "hf fmcos read record --rec 01 --fid 01 --len 20 --prot mac --key aabbccddeeff0011"); + void *argtable[] = { + arg_param_begin, + arg_int1(NULL, "rec", "", "record number (1-based)"), + arg_str1(NULL, "fid", "", "SFI reference byte (1 byte, 1-30)"), + arg_int1(NULL, "len", "", "record data length in bytes (without TLV overhead)"), + arg_str0(NULL, "prot", "","protection: none(def) mac enc"), + arg_str0(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0(NULL, "tlv", "wrap/unwrap TLV (00[len][data]) for variable-length records"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int rec = arg_get_int_def(ctx, 1, 0); + uint8_t fid_buf[1] = {0}; int fid_len = 0; + CLIGetHexWithReturn(ctx, 2, fid_buf, &fid_len); + int rlen = arg_get_int_def(ctx, 3, 0); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 4), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 5, key, &key_len); + bool use_tlv = arg_get_lit(ctx, 6); + bool keep = arg_get_lit(ctx, 7); + bool apdu_log = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (rec < 1 || rec > 255) { PrintAndLogEx(ERR, "--rec must be 1-255"); return PM3_EINVARG; } + if (fid_len != 1) { PrintAndLogEx(ERR, "--fid must be 1 byte"); return PM3_EINVARG; } + if (rlen < 0 || rlen > 253) { PrintAndLogEx(ERR, "--len must be 0-253"); return PM3_EINVARG; } + if (prot != FMCOS_PROT_NONE) { + if (key_len != 8 && key_len != 16) { PrintAndLogEx(ERR, "--key must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; } + } + SetAPDULogging(apdu_log); + + // For TLV records the card returns 00[len][data], so request 2 extra bytes + int le = use_tlv ? rlen + 2 : rlen; + uint8_t p2 = (uint8_t)(((fid_buf[0] & 0x1F) << 3) | 4); + uint8_t data_out[256] = {0}; + int data_out_len = 0; + int res = fmcos_read_cmd(0xB2, (uint8_t)rec, p2, (uint8_t)le, + prot, key, (size_t)key_len, + true, keep, data_out, &data_out_len); + if (res == PM3_SUCCESS) { + uint8_t *payload = data_out; + int payload_len = data_out_len; + if (use_tlv) { + // Strip 00[len] prefix; verify tag is 00 and length matches + if (data_out_len >= 2 && data_out[0] == 0x00) { + payload = data_out + 2; + payload_len = data_out_len - 2; + } else { + PrintAndLogEx(WARNING, "TLV prefix not found in response (tag=%02X)", data_out[0]); + } + } + PrintAndLogEx(SUCCESS, "Record: " _GREEN_("%s"), sprint_hex(payload, (size_t)payload_len)); + } + if (!keep) DropField(); + return res; +} + +static int CmdHFFmcosReadHelp(const char *Cmd); + +static command_t ReadCommandTable[] = { + {"help", CmdHFFmcosReadHelp, AlwaysAvailable, "This help"}, + {"binary", CmdHFFmcosReadBinary, IfPm3Iso14443a, "READ BINARY from transparent EF"}, + {"record", CmdHFFmcosReadRecord, IfPm3Iso14443a, "READ RECORD from record-based EF"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHFFmcosReadHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(ReadCommandTable); + return PM3_SUCCESS; +} + +static int CmdHFFmcosRead(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(ReadCommandTable, Cmd); +} + +static int CmdHFFmcosWriteBinary(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos write binary", + "UPDATE BINARY in the current transparent EF\n" + "Protection: none(def) mac enc", + "hf fmcos write binary --p1 00 --p2 00 --data 0102030405060708\n" + "hf fmcos write binary --p1 00 --p2 00 --data 01020304 --prot mac --key aabbccddeeff0011"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "p1", "", "P1 byte (offset high)"), + arg_str1(NULL, "p2", "", "P2 byte (offset low)"), + arg_str1(NULL, "data", "", "data to write"), + arg_str0(NULL, "prot", "", "protection: none(def) mac enc"), + arg_str0(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t p1b[1] = {0}; int p1_len = 0; + CLIGetHexWithReturn(ctx, 1, p1b, &p1_len); + uint8_t p2b[1] = {0}; int p2_len = 0; + CLIGetHexWithReturn(ctx, 2, p2b, &p2_len); + uint8_t wdata[245] = {0}; int wdata_len = 0; + CLIGetHexWithReturn(ctx, 3, wdata, &wdata_len); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 4), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 5, key, &key_len); + bool keep = arg_get_lit(ctx, 6); + bool apdu_log = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if (p1_len != 1 || p2_len != 1) { PrintAndLogEx(ERR, "--p1 and --p2 must each be 1 byte"); return PM3_EINVARG; } + if (wdata_len < 1) { PrintAndLogEx(ERR, "--data required"); return PM3_EINVARG; } + if (prot != FMCOS_PROT_NONE && key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "--key must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_write_cmd(0x00, 0xD6, p1b[0], p2b[0], + wdata, (size_t)wdata_len, + prot, key, (size_t)key_len, + true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Binary " _GREEN_("written")); + else + PrintAndLogEx(FAILED, "Write binary " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosWriteRecord(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos write record", + "UPDATE RECORD in the current record-based EF\n" + "Protection: none(def) mac enc\n" + "Use --tlv for variable-length (VAR) files: wraps data as 00[len][data] before sending.", + "hf fmcos write record --rec 01 --fid 01 --data 0102030405060708\n" + "hf fmcos write record --rec 01 --fid 06 --data 0102030405060708 --tlv\n" + "hf fmcos write record --rec 01 --fid 01 --data 01020304 --prot mac --key aabbccddeeff0011"); + void *argtable[] = { + arg_param_begin, + arg_int1(NULL, "rec", "", "record number (1-based)"), + arg_str1(NULL, "fid", "", "SFI reference byte (1 byte, 1-30)"), + arg_str1(NULL, "data", "", "record data to write"), + arg_str0(NULL, "prot", "", "protection: none(def) mac enc"), + arg_str0(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0(NULL, "tlv", "wrap data as 00[len][data] TLV for variable-length records"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int rec = arg_get_int_def(ctx, 1, 0); + uint8_t fid_buf[1] = {0}; int fid_len = 0; + CLIGetHexWithReturn(ctx, 2, fid_buf, &fid_len); + uint8_t wdata[243] = {0}; int wdata_len = 0; + CLIGetHexWithReturn(ctx, 3, wdata, &wdata_len); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 4), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 5, key, &key_len); + bool use_tlv = arg_get_lit(ctx, 6); + bool keep = arg_get_lit(ctx, 7); + bool apdu_log = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (rec < 1 || rec > 255) { PrintAndLogEx(ERR, "--rec must be 1-255"); return PM3_EINVARG; } + if (fid_len != 1) { PrintAndLogEx(ERR, "--fid must be 1 byte"); return PM3_EINVARG; } + if (wdata_len < 1) { PrintAndLogEx(ERR, "--data required"); return PM3_EINVARG; } + if (use_tlv && wdata_len > 243) { PrintAndLogEx(ERR, "--data too long for TLV write (max 243 bytes)"); return PM3_EINVARG; } + if (prot != FMCOS_PROT_NONE && key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "--key must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + // For VAR records: prepend 00[len] TLV wrapper + uint8_t send_buf[245] = {0}; + size_t send_len; + if (use_tlv) { + send_buf[0] = 0x00; + send_buf[1] = (uint8_t)wdata_len; + memcpy(send_buf + 2, wdata, (size_t)wdata_len); + send_len = (size_t)wdata_len + 2; + } else { + memcpy(send_buf, wdata, (size_t)wdata_len); + send_len = (size_t)wdata_len; + } + + uint8_t p2 = (uint8_t)(((fid_buf[0] & 0x1F) << 3) | 4); + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_write_cmd(0x00, 0xDC, (uint8_t)rec, p2, + send_buf, send_len, + prot, key, (size_t)key_len, + true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Record " _GREEN_("written")); + else + PrintAndLogEx(FAILED, "Write record " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosWriteHelp(const char *Cmd); + +static command_t WriteCommandTable[] = { + {"help", CmdHFFmcosWriteHelp, AlwaysAvailable, "This help"}, + {"binary", CmdHFFmcosWriteBinary, IfPm3Iso14443a, "UPDATE BINARY in transparent EF"}, + {"record", CmdHFFmcosWriteRecord, IfPm3Iso14443a, "UPDATE RECORD in record-based EF"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHFFmcosWriteHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(WriteCommandTable); + return PM3_SUCCESS; +} + +static int CmdHFFmcosWrite(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(WriteCommandTable, Cmd); +} + +static int CmdHFFmcosAppend(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos append", + "APPEND RECORD to a cyclic or linear EF\n" + "Protection: none(def) mac enc", + "hf fmcos append --fid 01 --data 0102030405060708\n" + "hf fmcos append --fid 01 --data 01020304 --prot mac --key aabbccddeeff0011"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "fid", "", "SFI reference byte (1 byte, 1-30)"), + arg_str1(NULL, "data", "", "record data to append"), + arg_str0(NULL, "prot", "", "protection: none(def) mac enc"), + arg_str0(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t fid_buf[1] = {0}; int fid_len = 0; + CLIGetHexWithReturn(ctx, 1, fid_buf, &fid_len); + uint8_t wdata[245] = {0}; int wdata_len = 0; + CLIGetHexWithReturn(ctx, 2, wdata, &wdata_len); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 3), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 4, key, &key_len); + bool keep = arg_get_lit(ctx, 5); + bool apdu_log = arg_get_lit(ctx, 6); + CLIParserFree(ctx); + + if (fid_len != 1) { PrintAndLogEx(ERR, "--fid must be 1 byte"); return PM3_EINVARG; } + if (wdata_len < 1) { PrintAndLogEx(ERR, "--data required"); return PM3_EINVARG; } + if (prot != FMCOS_PROT_NONE && key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "--key must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; + } + SetAPDULogging(apdu_log); + + // P1=0, P2 = ((fid & 0x1F) << 3) | 4 + uint8_t p2 = (uint8_t)(((fid_buf[0] & 0x1F) << 3) | 4); + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_write_cmd(0x00, 0xE2, 0x00, p2, + wdata, (size_t)wdata_len, + prot, key, (size_t)key_len, + true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Record " _GREEN_("appended")); + else + PrintAndLogEx(FAILED, "Append record " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// Key type option list for WRITE KEY +static const CLIParserOption g_fmcos_keytype_opts[] = { + {0x30, "desenc"}, + {0x31, "desdec"}, + {0x32, "desmac"}, + {0x34, "internal"}, + {0x36, "lineprotect"}, + {0x37, "unlockpin"}, + {0x38, "changepin"}, + {0x39, "extauth"}, + {0x3A, "pin"}, + {0x3C, "overdraft"}, + {0x3D, "debit"}, + {0x3E, "purchase"}, + {0x3F, "credit"}, + {0, NULL} +}; + +static int CmdHFFmcosWriteKey(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos key", + "WRITE KEY to the currently selected keyfile (INS D4)\n" + "Key types - Group A (need --change --version --algo):\n" + " desenc desdec desmac internal overdraft debit purchase credit\n" + "Group B (need --followup --errcount; extauth also needs --change):\n" + " extauth pin\n" + "Group C (need --change --errcount):\n" + " lineprotect unlockpin changepin\n" + "Use --authkey with --prot mac|enc to protect the command.", + "hf fmcos key --op 01 --id 00 --type internal --usage F0 --change 02 --version 00 --algo 01 --key 3434343434343434343434343434343\n" + "hf fmcos key --op 01 --id 00 --type pin --usage F0 --followup 01 --errcount 33 --key 123456"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "op", "", "authorization key slot (P1), e.g. 01"), + arg_str1(NULL, "id", "", "key slot to write (P2), e.g. 00"), + arg_str1(NULL, "type", "", "key type (see above)"), + arg_str1(NULL, "usage", "", "usage rights byte"), + arg_str0(NULL, "change", "", "change rights byte (Group A/B-extauth/C)"), + arg_str0(NULL, "version", "", "key version byte (Group A)"), + arg_str0(NULL, "algo", "", "algorithm ID byte (Group A)"), + arg_str0(NULL, "followup", "", "follow-up status byte (Group B)"), + arg_str0(NULL, "errcount", "", "error counter byte (Group B/C)"), + arg_str1(NULL, "key", "", "key value (8 or 16 bytes; 2-6 bytes for pin type)"), + arg_str0(NULL, "authkey", "", "line-protect key for MAC/enc (8 or 16 bytes)"), + arg_str0(NULL, "prot", "", "protection: none(def) mac enc"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t op_buf[1] = {0}; int op_len = 0; + CLIGetHexWithReturn(ctx, 1, op_buf, &op_len); + uint8_t id_buf[1] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 2, id_buf, &id_len); + int ktype = 0; + if (CLIGetOptionList(arg_get_str(ctx, 3), g_fmcos_keytype_opts, &ktype)) { CLIParserFree(ctx); return PM3_EINVARG; } + uint8_t usage_b[1] = {0}; int usage_len = 0; + CLIGetHexWithReturn(ctx, 4, usage_b, &usage_len); + uint8_t change_b[1] = {0}; int change_len = 0; + CLIGetHexWithReturn(ctx, 5, change_b, &change_len); + uint8_t version_b[1] = {0}; int version_len = 0; + CLIGetHexWithReturn(ctx, 6, version_b, &version_len); + uint8_t algo_b[1] = {0}; int algo_len = 0; + CLIGetHexWithReturn(ctx, 7, algo_b, &algo_len); + uint8_t followup_b[1] = {0};int followup_len = 0; + CLIGetHexWithReturn(ctx, 8, followup_b, &followup_len); + uint8_t errcnt_b[1] = {0}; int errcnt_len = 0; + CLIGetHexWithReturn(ctx, 9, errcnt_b, &errcnt_len); + uint8_t kval[16] = {0}; int kval_len = 0; + CLIGetHexWithReturn(ctx, 10, kval, &kval_len); + uint8_t authkey[16] = {0}; int authkey_len = 0; + CLIGetHexWithReturn(ctx, 11, authkey, &authkey_len); + int prot = FMCOS_PROT_NONE; + if (CLIGetOptionList(arg_get_str(ctx, 12), g_fmcos_prot_opts, &prot)) { CLIParserFree(ctx); return PM3_EINVARG; } + bool keep = arg_get_lit(ctx, 13); + bool apdu_log = arg_get_lit(ctx, 14); + CLIParserFree(ctx); + + if (op_len != 1 || id_len != 1) { PrintAndLogEx(ERR, "--op and --id must each be 1 byte"); return PM3_EINVARG; } + if (usage_len != 1) { PrintAndLogEx(ERR, "--usage must be 1 byte"); return PM3_EINVARG; } + if (ktype == 0x3A) { + if (kval_len < 2 || kval_len > 6) { PrintAndLogEx(ERR, "--key for pin type must be 2-6 bytes (PIN value)"); return PM3_EINVARG; } + } else { + if (kval_len != 8 && kval_len != 16) { PrintAndLogEx(ERR, "--key must be 8 or 16 bytes"); return PM3_EINVARG; } + } + if (prot != FMCOS_PROT_NONE && authkey_len != 8 && authkey_len != 16) { + PrintAndLogEx(ERR, "--authkey must be 8 or 16 bytes when --prot is set"); return PM3_EINVARG; + } + + // Build metadata according to key type group. + // The first byte ORs in the protection level per FMCOS spec. + uint8_t data[64] = {0}; + size_t data_len = 0; + data[data_len++] = (uint8_t)(ktype | prot); + data[data_len++] = usage_b[0]; + + // Group A: desenc desdec desmac internal overdraft debit purchase credit + if (ktype == 0x30 || ktype == 0x31 || ktype == 0x32 || ktype == 0x34 || + ktype == 0x3C || ktype == 0x3D || ktype == 0x3E || ktype == 0x3F) { + if (change_len != 1 || version_len != 1 || algo_len != 1) { + PrintAndLogEx(ERR, "This key type needs --change --version --algo"); + return PM3_EINVARG; + } + data[data_len++] = change_b[0]; + data[data_len++] = version_b[0]; + data[data_len++] = algo_b[0]; + } + // Group B-extauth: extauth + else if (ktype == 0x39) { + if (change_len != 1 || followup_len != 1 || errcnt_len != 1) { + PrintAndLogEx(ERR, "extauth key type needs --change --followup --errcount"); + return PM3_EINVARG; + } + data[data_len++] = change_b[0]; + data[data_len++] = followup_b[0]; + data[data_len++] = errcnt_b[0]; + } + // Group B-pin: pin + else if (ktype == 0x3A) { + if (followup_len != 1 || errcnt_len != 1) { + PrintAndLogEx(ERR, "pin key type needs --followup --errcount"); + return PM3_EINVARG; + } + data[data_len++] = 0xEF; + data[data_len++] = followup_b[0]; + data[data_len++] = errcnt_b[0]; + } + // Group C: lineprotect unlockpin changepin + else if (ktype == 0x36 || ktype == 0x37 || ktype == 0x38) { + if (change_len != 1 || errcnt_len != 1) { + PrintAndLogEx(ERR, "This key type needs --change --errcount"); + return PM3_EINVARG; + } + data[data_len++] = change_b[0]; + data[data_len++] = 0xFF; + data[data_len++] = errcnt_b[0]; + } else { + PrintAndLogEx(ERR, "Unknown key type 0x%02X", ktype); + return PM3_EINVARG; + } + + // Append key value + memcpy(data + data_len, kval, (size_t)kval_len); + data_len += (size_t)kval_len; + + SetAPDULogging(apdu_log); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_write_cmd(0x80, 0xD4, op_buf[0], id_buf[0], + data, data_len, + prot, authkey, (size_t)authkey_len, + true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "Key " _GREEN_("written")); + else + PrintAndLogEx(FAILED, "Write key " _RED_("failed")); + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// --------------------------------------------------------------------------- +// Phase 6 - PIN management +// --------------------------------------------------------------------------- + +// VERIFY PIN (INS=20): present the PIN to the card. +static int CmdHFFmcosPinVerify(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos pin verify", + "VERIFY PIN (INS 20) - present PIN to the card", + "hf fmcos pin verify --id 00 --pin 123456\n" + "hf fmcos pin verify --id 00 --pin 1234"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "key slot (P2), 1 byte"), + arg_str1(NULL, "pin", "", "PIN bytes (2-6 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[1] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + uint8_t pin[8] = {0}; int pin_len = 0; + CLIGetHexWithReturn(ctx, 2, pin, &pin_len); + bool keep = arg_get_lit(ctx, 3); + bool apdu_log = arg_get_lit(ctx, 4); + CLIParserFree(ctx); + + if (id_len != 1) { PrintAndLogEx(ERR, "--id must be 1 byte"); return PM3_EINVARG; } + if (pin_len < 2 || pin_len > 6) { PrintAndLogEx(ERR, "--pin must be 2-6 bytes"); return PM3_EINVARG; } + SetAPDULogging(apdu_log); + + // CLA=00 INS=20 P1=00 P2=key_id Lc=pin_len Data=pin + uint8_t apdu[11]; + apdu[0] = 0x00; apdu[1] = 0x20; apdu[2] = 0x00; apdu[3] = id_buf[0]; + apdu[4] = (uint8_t)pin_len; + memcpy(apdu + 5, pin, (size_t)pin_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 5 + (size_t)pin_len, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "PIN " _GREEN_("verified")); + else if (sw1 == 0x63) + PrintAndLogEx(FAILED, "Wrong PIN, retries remaining: %d", sw2 & 0x0F); + else + PrintAndLogEx(FAILED, "PIN verify " _RED_("failed")); + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// CHANGE PIN (INS=5E P1=01): present old PIN and new PIN. +static int CmdHFFmcosPinChange(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos pin change", + "CHANGE PIN (INS 5E P1=01) - change PIN with old PIN authorization\n" + "Data sent: old_pin + 0xFF + new_pin", + "hf fmcos pin change --id 00 --old 123456 --new 13371337"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "key slot (P2), 1 byte"), + arg_str1(NULL, "old", "", "old PIN bytes (2-6 bytes)"), + arg_str1(NULL, "new", "", "new PIN bytes (2-6 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[1] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + uint8_t old_pin[8] = {0}; int old_len = 0; + CLIGetHexWithReturn(ctx, 2, old_pin, &old_len); + uint8_t new_pin[8] = {0}; int new_len = 0; + CLIGetHexWithReturn(ctx, 3, new_pin, &new_len); + bool keep = arg_get_lit(ctx, 4); + bool apdu_log = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (id_len != 1) { PrintAndLogEx(ERR, "--id must be 1 byte"); return PM3_EINVARG; } + if (old_len < 2 || old_len > 6) { PrintAndLogEx(ERR, "--old must be 2-6 bytes"); return PM3_EINVARG; } + if (new_len < 2 || new_len > 6) { PrintAndLogEx(ERR, "--new must be 2-6 bytes"); return PM3_EINVARG; } + SetAPDULogging(apdu_log); + + // Data = old_pin + 0xFF + new_pin + uint8_t data[13]; + size_t data_len = 0; + memcpy(data, old_pin, (size_t)old_len); data_len += (size_t)old_len; + data[data_len++] = 0xFF; + memcpy(data + data_len, new_pin, (size_t)new_len); data_len += (size_t)new_len; + + uint8_t apdu[22]; + apdu[0] = 0x80; apdu[1] = 0x5E; apdu[2] = 0x01; apdu[3] = id_buf[0]; + apdu[4] = (uint8_t)data_len; + memcpy(apdu + 5, data, data_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 5 + data_len, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "PIN " _GREEN_("changed")); + else + PrintAndLogEx(FAILED, "PIN change " _RED_("failed")); + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// RESET PIN (INS=5E P1=00): set new PIN authorized by a change-PIN key. +// MAC = DES-MAC(new_pin, XOR(key_left, key_right)) +static int CmdHFFmcosPinReset(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos pin reset", + "RESET PIN (INS 5E P1=00) - set new PIN using change-PIN key\n" + "Appends DES-MAC(new_pin, key_left XOR key_right) to the data.", + "hf fmcos pin reset --id 00 --pin 123456 --key aabbccddeeff001122334455667788aa"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "key slot (P2), 1 byte"), + arg_str1(NULL, "pin", "", "new PIN bytes (2-6 bytes)"), + arg_str1(NULL, "key", "", "change-PIN key (16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[1] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + uint8_t pin[8] = {0}; int pin_len = 0; + CLIGetHexWithReturn(ctx, 2, pin, &pin_len); + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 3, key, &key_len); + bool keep = arg_get_lit(ctx, 4); + bool apdu_log = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (id_len != 1) { PrintAndLogEx(ERR, "--id must be 1 byte"); return PM3_EINVARG; } + if (pin_len < 2 || pin_len > 6) { PrintAndLogEx(ERR, "--pin must be 2-6 bytes"); return PM3_EINVARG; } + if (key_len != 16) { PrintAndLogEx(ERR, "--key must be 16 bytes (change-PIN key)"); return PM3_EINVARG; } + SetAPDULogging(apdu_log); + + // mac_key = key_left XOR key_right + uint8_t mac_key[8]; + for (int i = 0; i < 8; i++) + mac_key[i] = key[i] ^ key[i + 8]; + + // MAC = DES-CBC-MAC(new_pin, mac_key, iv=0) + uint8_t zero_iv[8] = {0}; + uint8_t mac[4] = {0}; + fmcos_des_mac(pin, (size_t)pin_len, mac_key, zero_iv, mac, 4); + + // Data = new_pin + mac[4] + uint8_t data[10]; + memcpy(data, pin, (size_t)pin_len); + memcpy(data + pin_len, mac, 4); + size_t data_len = (size_t)pin_len + 4; + + uint8_t apdu[15]; + apdu[0] = 0x80; apdu[1] = 0x5E; apdu[2] = 0x00; apdu[3] = id_buf[0]; + apdu[4] = (uint8_t)data_len; + memcpy(apdu + 5, data, data_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + int res = fmcos_send_apdu(apdu, 5 + data_len, true, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "PIN " _GREEN_("reset")); + else + PrintAndLogEx(FAILED, "PIN reset " _RED_("failed")); + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// UNBLOCK PIN (INS=24): present encrypted new PIN + packet MAC, authorized by unlock-PIN key. +// Data = encrypt([len|new_pin], unlock_key) + packet_mac(cla, ins, p1, p2, enc_data, chal_iv, unlock_key) +static int CmdHFFmcosPinUnblock(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos pin unblock", + "UNBLOCK PIN (INS 24) - unblock a locked PIN using the unlock-PIN key\n" + "Data = encrypt([len|new_pin], unlock_key) + packet_MAC", + "hf fmcos pin unblock --id 00 --pin 123456 --key aabbccddeeff001122334455667788aa"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "PIN key slot (P1), 1 byte"), + arg_str1(NULL, "pin", "", "new PIN bytes (2-6 bytes)"), + arg_str1(NULL, "key", "", "unlock-PIN key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t id_buf[1] = {0}; int id_len = 0; + CLIGetHexWithReturn(ctx, 1, id_buf, &id_len); + uint8_t pin[8] = {0}; int pin_len = 0; + CLIGetHexWithReturn(ctx, 2, pin, &pin_len); + uint8_t key[16] = {0}; int key_len = 0; + CLIGetHexWithReturn(ctx, 3, key, &key_len); + bool keep = arg_get_lit(ctx, 4); + bool apdu_log = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (id_len != 1) { PrintAndLogEx(ERR, "--id must be 1 byte"); return PM3_EINVARG; } + if (pin_len < 2 || pin_len > 6) { PrintAndLogEx(ERR, "--pin must be 2-6 bytes"); return PM3_EINVARG; } + if (key_len != 8 && key_len != 16) { PrintAndLogEx(ERR, "--key must be 8 or 16 bytes"); return PM3_EINVARG; } + SetAPDULogging(apdu_log); + + // Encrypt [len_byte | pin] with the unlock key + uint8_t plain[7]; + plain[0] = (uint8_t)pin_len; + memcpy(plain + 1, pin, (size_t)pin_len); + uint8_t enc_data[16] = {0}; + size_t enc_len = fmcos_encrypt(key, (size_t)key_len, plain, 1 + (size_t)pin_len, enc_data); + + // GET CHALLENGE for packet MAC IV - CLA=84, INS=24 + uint8_t chal[8] = {0}; + int res = fmcos_get_challenge(8, true, chal); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + + uint8_t mac[4] = {0}; + fmcos_packet_mac(0x84, 0x24, id_buf[0], 0x00, + enc_data, enc_len, chal, key, (size_t)key_len, mac); + + // Build data = enc_data + mac + uint8_t data[20]; + memcpy(data, enc_data, enc_len); + memcpy(data + enc_len, mac, 4); + size_t data_len = enc_len + 4; + + uint8_t apdu[25]; + apdu[0] = 0x84; apdu[1] = 0x24; apdu[2] = id_buf[0]; apdu[3] = 0x00; + apdu[4] = (uint8_t)data_len; + memcpy(apdu + 5, data, data_len); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + res = fmcos_send_apdu(apdu, 5 + data_len, false, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { if (!keep) DropField(); return res; } + if (resp_len < 2) { if (!keep) DropField(); return PM3_ESOFT; } + + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + if (sw1 == 0x90 && sw2 == 0x00) + PrintAndLogEx(SUCCESS, "PIN " _GREEN_("unblocked")); + else + PrintAndLogEx(FAILED, "PIN unblock " _RED_("failed")); + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosPinHelp(const char *Cmd); + +static command_t PinCommandTable[] = { + {"help", CmdHFFmcosPinHelp, AlwaysAvailable, "This help"}, + {"verify", CmdHFFmcosPinVerify, IfPm3Iso14443a, "VERIFY PIN (present PIN to card)"}, + {"change", CmdHFFmcosPinChange, IfPm3Iso14443a, "CHANGE PIN (old + new, requires old PIN)"}, + {"reset", CmdHFFmcosPinReset, IfPm3Iso14443a, "RESET PIN (new PIN + change-PIN key MAC)"}, + {"unblock", CmdHFFmcosPinUnblock, IfPm3Iso14443a, "UNBLOCK PIN (encrypted new PIN + MAC)"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHFFmcosPinHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(PinCommandTable); + return PM3_SUCCESS; +} + +static int CmdHFFmcosPin(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(PinCommandTable, Cmd); +} + +// --------------------------------------------------------------------------- +// Phase 7 helpers +// --------------------------------------------------------------------------- + +static void fmcos_get_datetime_bcd(uint8_t date_out[4], uint8_t time_out[3]) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + char ds[9], ts[7]; + strftime(ds, sizeof(ds), "%Y%m%d", t); + strftime(ts, sizeof(ts), "%H%M%S", t); + for (int i = 0; i < 4; i++) + date_out[i] = (uint8_t)((((ds[i * 2] - '0') & 0xF) << 4) | ((ds[i * 2 + 1] - '0') & 0xF)); + for (int i = 0; i < 3; i++) + time_out[i] = (uint8_t)((((ts[i * 2] - '0') & 0xF) << 4) | ((ts[i * 2 + 1] - '0') & 0xF)); +} + +// --------------------------------------------------------------------------- +// Financial operations +// --------------------------------------------------------------------------- + +static int CmdHFFmcosBalance(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos balance", + "GET BALANCE - read wallet or passbook balance", + "hf fmcos balance --type wallet\n" + "hf fmcos balance --type passbook"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "type", "", "balance type: wallet, passbook"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int bal_type = 0; + int res = CLIGetOptionList(arg_get_str(ctx, 1), g_fmcos_baltype_opts, &bal_type); + bool keep = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + if (res != PM3_SUCCESS) return PM3_EINVARG; + + uint8_t apdu[5] = {0x80, 0x5C, 0x00, (uint8_t)bal_type, 0x04}; + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + if (fmcos_send_apdu(apdu, sizeof(apdu), true, keep, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Short response"); + return PM3_ESOFT; + } + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 == 0x90 && sw2 == 0x00 && resp_len >= 6) { + uint32_t balance = ((uint32_t)resp[0] << 24) | ((uint32_t)resp[1] << 16) | + ((uint32_t)resp[2] << 8) | resp[3]; + PrintAndLogEx(SUCCESS, "Balance (%s): " _GREEN_("%u") " (0x%08X)", + bal_type == 0x01 ? "passbook" : "wallet", balance, balance); + } + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosCredit(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos credit", + "ADD CREDIT to wallet or passbook (two-phase with MAC/TAC verification).\n" + "Phase 1: card returns old balance, serial, RNG; derive process key, verify MAC1.\n" + "Phase 2: send date/time and MAC2; card returns TAC which is verified.", + "hf fmcos credit --type wallet --id 01 --amount 1000\n" + " --terminal 010203040506\n" + " --key 00112233445566778899aabbccddeeff\n" + " --ikey aabbccddeeff00112233445566778899"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "type", "", "balance type: wallet, passbook"), + arg_str1(NULL, "id", "", "credit key file ID (1 byte)"), + arg_int1(NULL, "amount", "", "credit amount"), + arg_str1(NULL, "terminal", "", "terminal ID (6 bytes)"), + arg_str1(NULL, "key", "", "credit key (16 bytes)"), + arg_str1(NULL, "ikey", "", "internal key (16 bytes, for TAC verification)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int bal_type = 0; + int res = CLIGetOptionList(arg_get_str(ctx, 1), g_fmcos_baltype_opts, &bal_type); + int key_id = (int)strtol(arg_get_str(ctx, 2)->sval[0], NULL, 16); + int amount_i = arg_get_int(ctx, 3); + + uint8_t terminal[6] = {0}; + int terminal_len = 0; + CLIGetHexWithReturn(ctx, 4, terminal, &terminal_len); + + uint8_t crde_key[16] = {0}; + int crde_key_len = 0; + CLIGetHexWithReturn(ctx, 5, crde_key, &crde_key_len); + + uint8_t ikey[16] = {0}; + int ikey_len = 0; + CLIGetHexWithReturn(ctx, 6, ikey, &ikey_len); + + bool keep = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if (res != PM3_SUCCESS) return PM3_EINVARG; + if (key_id < 0 || key_id > 0xFF) { PrintAndLogEx(ERR, "Key ID must be 0-255"); return PM3_EINVARG; } + if (amount_i <= 0) { PrintAndLogEx(ERR, "Amount must be positive"); return PM3_EINVARG; } + if (terminal_len != 6) { PrintAndLogEx(ERR, "Terminal ID must be 6 bytes"); return PM3_EINVARG; } + if (crde_key_len != 16) { PrintAndLogEx(ERR, "Credit key must be 16 bytes"); return PM3_EINVARG; } + if (ikey_len != 16) { PrintAndLogEx(ERR, "Internal key must be 16 bytes"); return PM3_EINVARG; } + + uint32_t amount = (uint32_t)amount_i; + uint8_t tx_type = (uint8_t)bal_type; // transaction_type == balance_type for credit + + // ---- Phase 1: INITIALIZE FOR CREDIT (INS 50, P1 00) ---- + // APDU: CLA INS P1 P2 Lc[=11] key_id amount[4] terminal[6] Le[=16] + uint8_t ph1[17]; + ph1[0] = 0x80; ph1[1] = 0x50; ph1[2] = 0x00; ph1[3] = (uint8_t)bal_type; ph1[4] = 0x0B; + ph1[5] = (uint8_t)key_id; + ph1[6] = (amount >> 24) & 0xFF; ph1[7] = (amount >> 16) & 0xFF; + ph1[8] = (amount >> 8) & 0xFF; ph1[9] = amount & 0xFF; + memcpy(&ph1[10], terminal, 6); + ph1[16] = 0x10; + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + if (fmcos_send_apdu(ph1, sizeof(ph1), true, true, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + // Response: old_balance[4] online_serial[2] key_ver[1] algo[1] random_1[4] mac_1[4] SW[2] + if (resp_len < 18) { + if (resp_len >= 2) fmcos_print_sw(resp[resp_len - 2], resp[resp_len - 1]); + PrintAndLogEx(ERR, "Phase 1 short response (%d bytes) -- DF selected?", resp_len); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint32_t old_balance = ((uint32_t)resp[0] << 24) | ((uint32_t)resp[1] << 16) | + ((uint32_t)resp[2] << 8) | resp[3]; + uint8_t online_serial[2]; + memcpy(online_serial, resp + 4, 2); + + // Process key: fmcos_encrypt(random_1[4] | online_serial[2], crde_key) -> first 8 bytes + uint8_t pk_buf[6]; + memcpy(pk_buf, resp + 8, 4); // random_1 + memcpy(pk_buf + 4, resp + 4, 2); // online_serial + uint8_t pk_enc[24] = {0}; + fmcos_encrypt(crde_key, (size_t)crde_key_len, pk_buf, 6, pk_enc); + uint8_t process_key[8]; + memcpy(process_key, pk_enc, 8); + + // Verify MAC1: DES-CBC-MAC(old_bal[4] | amount[4] | tx_type[1] | terminal[6], process_key) + uint8_t mac1_buf[15]; + memcpy(mac1_buf, resp, 4); + mac1_buf[4] = (amount >> 24) & 0xFF; mac1_buf[5] = (amount >> 16) & 0xFF; + mac1_buf[6] = (amount >> 8) & 0xFF; mac1_buf[7] = amount & 0xFF; + mac1_buf[8] = tx_type; + memcpy(mac1_buf + 9, terminal, 6); + + uint8_t zero_iv[8] = {0}; + uint8_t mac1_calc[4] = {0}; + fmcos_des_mac(mac1_buf, 15, process_key, zero_iv, mac1_calc, 4); + + if (memcmp(mac1_calc, resp + 12, 4) != 0) { + PrintAndLogEx(ERR, "MAC1 mismatch - card response invalid"); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "MAC1 OK old balance %u", old_balance); + + // Compute MAC2: DES-CBC-MAC(amount[4] | tx_type[1] | terminal[6] | date[4] | time[3], process_key) + uint8_t date[4], ttime[3]; + fmcos_get_datetime_bcd(date, ttime); + + uint8_t mac2_buf[18]; + mac2_buf[0] = (amount >> 24) & 0xFF; mac2_buf[1] = (amount >> 16) & 0xFF; + mac2_buf[2] = (amount >> 8) & 0xFF; mac2_buf[3] = amount & 0xFF; + mac2_buf[4] = tx_type; + memcpy(mac2_buf + 5, terminal, 6); + memcpy(mac2_buf + 11, date, 4); + memcpy(mac2_buf + 15, ttime, 3); + + uint8_t mac2[4] = {0}; + fmcos_des_mac(mac2_buf, 18, process_key, zero_iv, mac2, 4); + + // ---- Phase 2: CREDIT (INS 52) ---- + // APDU: CLA INS P1 P2 Lc[=11] date[4] time[3] mac2[4] Le[=4] + uint8_t ph2[17]; + ph2[0] = 0x80; ph2[1] = 0x52; ph2[2] = 0x00; ph2[3] = 0x00; ph2[4] = 0x0B; + memcpy(&ph2[5], date, 4); + memcpy(&ph2[9], ttime, 3); + memcpy(&ph2[12], mac2, 4); + ph2[16] = 0x04; + + memset(resp, 0, sizeof(resp)); + resp_len = 0; + if (fmcos_send_apdu(ph2, sizeof(ph2), false, keep, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + // Response: TAC[4] SW[2] + if (resp_len < 6) { + PrintAndLogEx(ERR, "Phase 2 short response (%d bytes)", resp_len); + return PM3_ESOFT; + } + sw1 = resp[resp_len - 2]; sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + return PM3_ESOFT; + } + + // Verify TAC: DES-CBC-MAC(new_bal[4] | online_serial[2] | mac2_buf[18], tac_key) + // tac_key = XOR of the two 8-byte halves of the internal key + uint32_t new_balance = old_balance + amount; + uint8_t tac_key[8]; + for (int i = 0; i < 8; i++) tac_key[i] = ikey[i] ^ ikey[i + 8]; + + uint8_t tac_buf[24]; + tac_buf[0] = (new_balance >> 24) & 0xFF; tac_buf[1] = (new_balance >> 16) & 0xFF; + tac_buf[2] = (new_balance >> 8) & 0xFF; tac_buf[3] = new_balance & 0xFF; + memcpy(tac_buf + 4, online_serial, 2); + memcpy(tac_buf + 6, mac2_buf, 18); + + uint8_t tac_calc[4] = {0}; + fmcos_des_mac(tac_buf, 24, tac_key, zero_iv, tac_calc, 4); + + if (memcmp(tac_calc, resp, 4) != 0) { + PrintAndLogEx(WARNING, "TAC mismatch - new balance %u may be incorrect", new_balance); + } else { + PrintAndLogEx(SUCCESS, "TAC OK new balance " _GREEN_("%u"), new_balance); + } + fmcos_print_sw(sw1, sw2); + return PM3_SUCCESS; +} + +static int CmdHFFmcosPurchase(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos purchase", + "PURCHASE from wallet or passbook (two-phase with process key/TAC verification).\n" + "Phase 1: card returns balance, serial, RNG; derive process key, compute MAC1.\n" + "Phase 2: send tx serial, date/time, MAC1; card returns TAC which is verified.", + "hf fmcos purchase --type wallet --id 02 --amount 100\n" + " --terminal 010203040506\n" + " --key 00112233445566778899aabbccddeeff\n" + " --ikey aabbccddeeff00112233445566778899\n" + " --serial 01020304"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "type", "", "balance type: wallet, passbook"), + arg_str1(NULL, "id", "", "purchase key file ID (1 byte)"), + arg_int1(NULL, "amount", "", "purchase amount"), + arg_str1(NULL, "terminal", "", "terminal ID (6 bytes)"), + arg_str1(NULL, "key", "", "purchase key (16 bytes)"), + arg_str1(NULL, "ikey", "", "internal key (16 bytes, for TAC verification)"), + arg_str0(NULL, "serial", "", "transaction serial (4 bytes, default 00000001)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int bal_type = 0; + int res = CLIGetOptionList(arg_get_str(ctx, 1), g_fmcos_baltype_opts, &bal_type); + int key_id = (int)strtol(arg_get_str(ctx, 2)->sval[0], NULL, 16); + int amount_i = arg_get_int(ctx, 3); + + uint8_t terminal[6] = {0}; + int terminal_len = 0; + CLIGetHexWithReturn(ctx, 4, terminal, &terminal_len); + + uint8_t purch_key[16] = {0}; + int purch_key_len = 0; + CLIGetHexWithReturn(ctx, 5, purch_key, &purch_key_len); + + uint8_t ikey[16] = {0}; + int ikey_len = 0; + CLIGetHexWithReturn(ctx, 6, ikey, &ikey_len); + + uint8_t tx_serial[4] = {0x00, 0x00, 0x00, 0x01}; + int serial_len = 0; + CLIGetHexWithReturn(ctx, 7, tx_serial, &serial_len); + + bool keep = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (res != PM3_SUCCESS) return PM3_EINVARG; + if (key_id < 0 || key_id > 0xFF) { PrintAndLogEx(ERR, "Key ID must be 0-255"); return PM3_EINVARG; } + if (amount_i <= 0) { PrintAndLogEx(ERR, "Amount must be positive"); return PM3_EINVARG; } + if (terminal_len != 6) { PrintAndLogEx(ERR, "Terminal ID must be 6 bytes"); return PM3_EINVARG; } + if (purch_key_len != 16) { PrintAndLogEx(ERR, "Purchase key must be 16 bytes"); return PM3_EINVARG; } + if (ikey_len != 16) { PrintAndLogEx(ERR, "Internal key must be 16 bytes"); return PM3_EINVARG; } + if (serial_len != 0 && serial_len != 4) { PrintAndLogEx(ERR, "Serial must be 4 bytes"); return PM3_EINVARG; } + + uint32_t amount = (uint32_t)amount_i; + // transaction_type: 0x05 passbook purchase, 0x06 wallet purchase + uint8_t tx_type = (bal_type == 0x01) ? 0x05 : 0x06; + + // ---- Phase 1: INITIALIZE FOR PURCHASE (INS 50, P1 01) ---- + // APDU: CLA INS P1 P2 Lc[=11] key_id amount[4] terminal[6] Le[=15] + uint8_t ph1[17]; + ph1[0] = 0x80; ph1[1] = 0x50; ph1[2] = 0x01; ph1[3] = (uint8_t)bal_type; ph1[4] = 0x0B; + ph1[5] = (uint8_t)key_id; + ph1[6] = (amount >> 24) & 0xFF; ph1[7] = (amount >> 16) & 0xFF; + ph1[8] = (amount >> 8) & 0xFF; ph1[9] = amount & 0xFF; + memcpy(&ph1[10], terminal, 6); + ph1[16] = 0x0F; + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + if (fmcos_send_apdu(ph1, sizeof(ph1), true, true, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + // Response: old_balance[4] offline_serial[2] overdraft_lim[3] key_ver[1] algo[1] random_1[4] SW[2] + if (resp_len < 17) { + if (resp_len >= 2) fmcos_print_sw(resp[resp_len - 2], resp[resp_len - 1]); + PrintAndLogEx(ERR, "Phase 1 short response (%d bytes) -- DF selected?", resp_len); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint32_t old_balance = ((uint32_t)resp[0] << 24) | ((uint32_t)resp[1] << 16) | + ((uint32_t)resp[2] << 8) | resp[3]; + PrintAndLogEx(INFO, "Old balance: %u", old_balance); + + // Process key: fmcos_encrypt(random_1[4] | offline_serial[2] | tx_serial[2], purchase_key) -> first 8 bytes + uint8_t pk_buf[8]; + memcpy(pk_buf, resp + 11, 4); // random_1 + memcpy(pk_buf + 4, resp + 4, 2); // offline_serial + pk_buf[6] = tx_serial[2]; + pk_buf[7] = tx_serial[3]; + uint8_t pk_enc[24] = {0}; + fmcos_encrypt(purch_key, (size_t)purch_key_len, pk_buf, 8, pk_enc); + uint8_t process_key[8]; + memcpy(process_key, pk_enc, 8); + + // Compute MAC1: DES-CBC-MAC(amount[4] | tx_type[1] | terminal[6] | date[4] | time[3], process_key) + uint8_t date[4], ttime[3]; + fmcos_get_datetime_bcd(date, ttime); + + uint8_t mac1_buf[18]; + mac1_buf[0] = (amount >> 24) & 0xFF; mac1_buf[1] = (amount >> 16) & 0xFF; + mac1_buf[2] = (amount >> 8) & 0xFF; mac1_buf[3] = amount & 0xFF; + mac1_buf[4] = tx_type; + memcpy(mac1_buf + 5, terminal, 6); + memcpy(mac1_buf + 11, date, 4); + memcpy(mac1_buf + 15, ttime, 3); + + uint8_t zero_iv[8] = {0}; + uint8_t mac1[4] = {0}; + fmcos_des_mac(mac1_buf, 18, process_key, zero_iv, mac1, 4); + + // ---- Phase 2: DEBIT (INS 54, P1 01, P2 00) ---- + // APDU: CLA INS P1 P2 Lc[=15] tx_serial[4] date[4] time[3] mac1[4] Le[=8] + uint8_t ph2[21]; + ph2[0] = 0x80; ph2[1] = 0x54; ph2[2] = 0x01; ph2[3] = 0x00; ph2[4] = 0x0F; + memcpy(&ph2[5], tx_serial, 4); + memcpy(&ph2[9], date, 4); + memcpy(&ph2[13], ttime, 3); + memcpy(&ph2[16], mac1, 4); + ph2[20] = 0x08; + + memset(resp, 0, sizeof(resp)); + resp_len = 0; + if (fmcos_send_apdu(ph2, sizeof(ph2), false, keep, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + // Response: TAC[4] mac2_card[4] SW[2] + if (resp_len < 10) { + PrintAndLogEx(ERR, "Phase 2 short response (%d bytes)", resp_len); + return PM3_ESOFT; + } + sw1 = resp[resp_len - 2]; sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + return PM3_ESOFT; + } + + // Verify TAC: DES-CBC-MAC(amount[4] | tx_type[1] | terminal[6] | tx_serial[4] | date[4] | time[3], tac_key) + // tac_key = XOR of the two 8-byte halves of the internal key + uint32_t new_balance = old_balance - amount; + uint8_t tac_key[8]; + for (int i = 0; i < 8; i++) tac_key[i] = ikey[i] ^ ikey[i + 8]; + + uint8_t tac_buf[22]; + tac_buf[0] = (amount >> 24) & 0xFF; tac_buf[1] = (amount >> 16) & 0xFF; + tac_buf[2] = (amount >> 8) & 0xFF; tac_buf[3] = amount & 0xFF; + tac_buf[4] = tx_type; + memcpy(tac_buf + 5, terminal, 6); + memcpy(tac_buf + 11, tx_serial, 4); + memcpy(tac_buf + 15, date, 4); + memcpy(tac_buf + 19, ttime, 3); + + uint8_t tac_calc[4] = {0}; + fmcos_des_mac(tac_buf, 22, tac_key, zero_iv, tac_calc, 4); + + if (memcmp(tac_calc, resp, 4) != 0) { + PrintAndLogEx(WARNING, "TAC mismatch - new balance %u may be incorrect", new_balance); + } else { + PrintAndLogEx(SUCCESS, "TAC OK new balance " _GREEN_("%u"), new_balance); + } + fmcos_print_sw(sw1, sw2); + return PM3_SUCCESS; +} + +static int CmdHFFmcosOverdraft(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos overdraft", + "UPDATE OVERDRAFT LIMIT on passbook (two-phase with MAC1/MAC2 verification).\n" + "Phase 1: card returns balance, serial, old limit, RNG, MAC1; verify MAC1.\n" + "Phase 2: send new limit, date/time, MAC2; card returns TAC (4 bytes).\n" + "Provide --ikey (internal key DTK) to verify the TAC returned by the card.", + "hf fmcos overdraft --id 01 --limit 5000 --terminal 010203040506\n" + " --key 00112233445566778899aabbccddeeff\n" + "hf fmcos overdraft --id 01 --limit 5000 --terminal 010203040506\n" + " --key 00112233445566778899aabbccddeeff --ikey aabbccddeeff00112233445566778899"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "overdraft key file ID (1 byte)"), + arg_int1(NULL, "limit", "", "new overdraft limit (24-bit max 16777215)"), + arg_str1(NULL, "terminal", "", "terminal ID (6 bytes)"), + arg_str1(NULL, "key", "", "overdraft key (16 bytes)"), + arg_str0(NULL, "ikey", "", "internal key DTK (16 bytes) for TAC verification (optional)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int key_id = (int)strtol(arg_get_str(ctx, 1)->sval[0], NULL, 16); + int limit_i = arg_get_int(ctx, 2); + + uint8_t terminal[6] = {0}; + int terminal_len = 0; + CLIGetHexWithReturn(ctx, 3, terminal, &terminal_len); + + uint8_t od_key[16] = {0}; + int od_key_len = 0; + CLIGetHexWithReturn(ctx, 4, od_key, &od_key_len); + + uint8_t ikey[16] = {0}; + int ikey_len = 0; + CLIGetHexWithReturn(ctx, 5, ikey, &ikey_len); + + bool keep = arg_get_lit(ctx, 6); + CLIParserFree(ctx); + + if (key_id < 0 || key_id > 0xFF) { PrintAndLogEx(ERR, "Key ID must be 0-255"); return PM3_EINVARG; } + if (limit_i < 0 || limit_i > 0xFFFFFF) { PrintAndLogEx(ERR, "Limit must be 0-16777215"); return PM3_EINVARG; } + if (terminal_len != 6) { PrintAndLogEx(ERR, "Terminal ID must be 6 bytes"); return PM3_EINVARG; } + if (od_key_len != 16) { PrintAndLogEx(ERR, "Overdraft key must be 16 bytes"); return PM3_EINVARG; } + if (ikey_len != 0 && ikey_len != 16) { PrintAndLogEx(ERR, "Internal key must be 16 bytes"); return PM3_EINVARG; } + + bool verify_tac = (ikey_len == 16); + uint32_t new_limit = (uint32_t)limit_i; + + // ---- Phase 1: INITIALIZE FOR OVERDRAFT (INS 50, P1 04, P2 01=passbook) ---- + // APDU: CLA INS P1 P2 Lc[=7] key_id terminal[6] Le[=19] + uint8_t ph1[13]; + ph1[0] = 0x80; ph1[1] = 0x50; ph1[2] = 0x04; ph1[3] = 0x01; ph1[4] = 0x07; + ph1[5] = (uint8_t)key_id; + memcpy(&ph1[6], terminal, 6); + ph1[12] = 0x13; + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + if (fmcos_send_apdu(ph1, sizeof(ph1), true, true, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + // Response: old_balance[4] online_serial[2] old_od_limit[3] key_ver[1] algo[1] random_1[4] card_mac1[4] SW[2] + if (resp_len < 21) { + if (resp_len >= 2) fmcos_print_sw(resp[resp_len - 2], resp[resp_len - 1]); + PrintAndLogEx(ERR, "Phase 1 short response (%d bytes) -- DF selected?", resp_len); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + fmcos_print_sw(sw1, sw2); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint32_t old_balance = ((uint32_t)resp[0] << 24) | ((uint32_t)resp[1] << 16) | + ((uint32_t)resp[2] << 8) | resp[3]; + uint8_t online_serial[2]; + memcpy(online_serial, resp + 4, 2); + uint32_t old_od_limit = ((uint32_t)resp[6] << 16) | ((uint32_t)resp[7] << 8) | resp[8]; + PrintAndLogEx(INFO, "Old balance: %u old overdraft limit: %u", old_balance, old_od_limit); + + // Process key: fmcos_encrypt(random_1[4] | online_serial[2], od_key) -> first 8 bytes + uint8_t pk_buf[6]; + memcpy(pk_buf, resp + 11, 4); + memcpy(pk_buf + 4, resp + 4, 2); + uint8_t pk_enc[24] = {0}; + fmcos_encrypt(od_key, (size_t)od_key_len, pk_buf, 6, pk_enc); + uint8_t process_key[8]; + memcpy(process_key, pk_enc, 8); + + // Verify MAC1: DES-CBC-MAC(old_bal[4] | old_od_limit[3] | 0x07[1] | terminal[6], process_key) + uint8_t mac1_buf[14]; + memcpy(mac1_buf, resp, 4); // old_balance + memcpy(mac1_buf + 4, resp + 6, 3); // old_od_limit (3 bytes at resp[6..8]) + mac1_buf[7] = 0x07; // transaction_type = Overdraft + memcpy(mac1_buf + 8, terminal, 6); + + uint8_t zero_iv[8] = {0}; + uint8_t mac1_calc[4] = {0}; + fmcos_des_mac(mac1_buf, 14, process_key, zero_iv, mac1_calc, 4); + + if (memcmp(mac1_calc, resp + 15, 4) != 0) { + PrintAndLogEx(ERR, "MAC1 mismatch - card response invalid"); + g_fmcos_session_active = keep; + if (!keep) DropField(); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "MAC1 OK"); + + // Compute MAC2: DES-CBC-MAC(new_limit[3] | 0x07[1] | terminal[6] | date[4] | time[3], process_key) + uint8_t date[4], ttime[3]; + fmcos_get_datetime_bcd(date, ttime); + + uint8_t mac2_buf[17]; + mac2_buf[0] = (new_limit >> 16) & 0xFF; + mac2_buf[1] = (new_limit >> 8) & 0xFF; + mac2_buf[2] = new_limit & 0xFF; + mac2_buf[3] = 0x07; + memcpy(mac2_buf + 4, terminal, 6); + memcpy(mac2_buf + 10, date, 4); + memcpy(mac2_buf + 14, ttime, 3); + + uint8_t mac2[4] = {0}; + fmcos_des_mac(mac2_buf, 17, process_key, zero_iv, mac2, 4); + + // ---- Phase 2: UPDATE OVERDRAFT (INS 58, P1 00, P2 00) ---- + // APDU: CLA INS P1 P2 Lc[=14] new_limit[3] date[4] time[3] mac2[4] Le[=4] + uint8_t ph2[20]; + ph2[0] = 0x80; ph2[1] = 0x58; ph2[2] = 0x00; ph2[3] = 0x00; ph2[4] = 0x0E; + ph2[5] = (new_limit >> 16) & 0xFF; + ph2[6] = (new_limit >> 8) & 0xFF; + ph2[7] = new_limit & 0xFF; + memcpy(&ph2[8], date, 4); + memcpy(&ph2[12], ttime, 3); + memcpy(&ph2[15], mac2, 4); + ph2[19] = 0x04; + + memset(resp, 0, sizeof(resp)); + resp_len = 0; + if (fmcos_send_apdu(ph2, sizeof(ph2), false, keep, resp, &resp_len) != PM3_SUCCESS) + return PM3_ESOFT; + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Phase 2 short response"); + return PM3_ESOFT; + } + sw1 = resp[resp_len - 2]; sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 != 0x90 || sw2 != 0x00) + return PM3_ESOFT; + + PrintAndLogEx(SUCCESS, "Overdraft limit updated to " _GREEN_("%u"), new_limit); + + if (verify_tac) { + if (resp_len < 6) { + PrintAndLogEx(WARNING, "TAC not present in Phase 2 response (got %d bytes)", resp_len); + } else { + // TAC key: XOR of the two 8-byte halves of the internal key (same as credit/purchase) + uint8_t tac_key[8]; + for (int i = 0; i < 8; i++) tac_key[i] = ikey[i] ^ ikey[i + 8]; + + // TAC buffer: tac_bal[4]|serial[2]|new_limit[3]|0x07[1]|terminal[6]|date[4]|time[3] + // The card stores (actual_funds + od_limit) as its balance field. When the limit + // changes, the new stored balance = old_balance + new_limit - old_od_limit. + uint32_t tac_balance = old_balance + new_limit - old_od_limit; + uint8_t tac_buf[23]; + tac_buf[0] = (tac_balance >> 24) & 0xFF; + tac_buf[1] = (tac_balance >> 16) & 0xFF; + tac_buf[2] = (tac_balance >> 8) & 0xFF; + tac_buf[3] = tac_balance & 0xFF; + memcpy(tac_buf + 4, online_serial, 2); + tac_buf[6] = (new_limit >> 16) & 0xFF; + tac_buf[7] = (new_limit >> 8) & 0xFF; + tac_buf[8] = new_limit & 0xFF; + tac_buf[9] = 0x07; + memcpy(tac_buf + 10, terminal, 6); + memcpy(tac_buf + 16, date, 4); + memcpy(tac_buf + 20, ttime, 3); + + uint8_t tac_calc[4] = {0}; + fmcos_des_mac(tac_buf, 23, tac_key, zero_iv, tac_calc, 4); + + if (memcmp(tac_calc, resp, 4) != 0) { + PrintAndLogEx(WARNING, "TAC mismatch - overdraft limit update may be unverified"); + PrintAndLogEx(INFO, "Verify --ikey is the type-0x34 internal key for this card"); + } else { + PrintAndLogEx(SUCCESS, "TAC OK " _GREEN_("%s"), sprint_hex(resp, 4)); + } + } + } else if (resp_len >= 6) { + PrintAndLogEx(INFO, "TAC (unverified): %s", sprint_hex(resp, 4)); + } + + return PM3_SUCCESS; +} + +static int CmdHFFmcosBlock(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos block", + "BLOCK the card or an application.\n" + "Uses the line-protection key to generate a MAC. One of --card or --app is required.\n" + "Application block type: --perm (permanent) or --temp (temporary, default).", + "hf fmcos block --card --key aabbccddeeff0011\n" + "hf fmcos block --app --perm --key aabbccddeeff001122334455667788aa"); + + void *argtable[] = { + arg_param_begin, + arg_lit0(NULL, "card", "block the entire card (CARD BLOCK, INS 16)"), + arg_lit0(NULL, "app", "block the current application (APP BLOCK, INS 1E)"), + arg_lit0(NULL, "perm", "permanent application block (default is temporary)"), + arg_str1(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + bool block_card = arg_get_lit(ctx, 1); + bool block_app = arg_get_lit(ctx, 2); + bool permanent = arg_get_lit(ctx, 3); + + uint8_t key[16] = {0}; + int key_len = 0; + CLIGetHexWithReturn(ctx, 4, key, &key_len); + + bool keep = arg_get_lit(ctx, 5); + bool apdu_log = arg_get_lit(ctx, 6); + CLIParserFree(ctx); + + if (!block_card && !block_app) { + PrintAndLogEx(ERR, "Specify --card or --app"); + return PM3_EINVARG; + } + if (block_card && block_app) { + PrintAndLogEx(ERR, "Specify only one of --card or --app"); + return PM3_EINVARG; + } + if (key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "Key must be 8 bytes (DES) or 16 bytes (3DES)"); + return PM3_EINVARG; + } + + SetAPDULogging(apdu_log); + + // GET CHALLENGE (8 bytes) as MAC IV - activates the field + uint8_t chal[8] = {0}; + int res = fmcos_get_challenge(8, true, chal); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + uint8_t cla, ins, p2; + if (block_card) { + cla = 0x84; ins = 0x16; p2 = 0x00; + } else { + // APP BLOCK: P2=0x00 temporary, P2=0x01 permanent + cla = 0x84; ins = 0x1E; p2 = permanent ? 0x01 : 0x00; + } + uint8_t p1 = 0x00; + + uint8_t mac[4] = {0}; + fmcos_packet_mac(cla, ins, p1, p2, NULL, 0, chal, key, (size_t)key_len, mac); + + uint8_t apdu[9]; + apdu[0] = cla; + apdu[1] = ins; + apdu[2] = p1; + apdu[3] = p2; + apdu[4] = 0x04; + memcpy(&apdu[5], mac, 4); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + res = fmcos_send_apdu(apdu, sizeof(apdu), false, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Empty response"); + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 == 0x90 && sw2 == 0x00) { + if (block_card) { + PrintAndLogEx(SUCCESS, "Card " _GREEN_("blocked")); + } else { + PrintAndLogEx(SUCCESS, "Application " _GREEN_("blocked") " (%s)", + permanent ? "permanent" : "temporary"); + } + } else { + PrintAndLogEx(FAILED, "Block command " _RED_("failed")); + } + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int CmdHFFmcosUnblock(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos unblock", + "UNBLOCK the current application (APP UNBLOCK, INS 18).\n" + "Uses the line-protection key to generate a MAC.", + "hf fmcos unblock --key aabbccddeeff0011\n" + "hf fmcos unblock --key aabbccddeeff001122334455667788aa"); + + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "key", "", "line-protection key (8 or 16 bytes)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t key[16] = {0}; + int key_len = 0; + CLIGetHexWithReturn(ctx, 1, key, &key_len); + + bool keep = arg_get_lit(ctx, 2); + bool apdu_log = arg_get_lit(ctx, 3); + CLIParserFree(ctx); + + if (key_len != 8 && key_len != 16) { + PrintAndLogEx(ERR, "Key must be 8 bytes (DES) or 16 bytes (3DES)"); + return PM3_EINVARG; + } + + SetAPDULogging(apdu_log); + + uint8_t chal[8] = {0}; + int res = fmcos_get_challenge(8, true, chal); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + // APP UNBLOCK: CLA=84 INS=18 P1=00 P2=00 + uint8_t mac[4] = {0}; + fmcos_packet_mac(0x84, 0x18, 0x00, 0x00, NULL, 0, chal, key, (size_t)key_len, mac); + + uint8_t apdu[9]; + apdu[0] = 0x84; + apdu[1] = 0x18; + apdu[2] = 0x00; + apdu[3] = 0x00; + apdu[4] = 0x04; + memcpy(&apdu[5], mac, 4); + + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + res = fmcos_send_apdu(apdu, sizeof(apdu), false, keep, resp, &resp_len); + if (res != PM3_SUCCESS) { + if (!keep) DropField(); + return res; + } + + if (resp_len < 2) { + PrintAndLogEx(ERR, "Empty response"); + if (!keep) DropField(); + return PM3_ESOFT; + } + + uint8_t sw1 = resp[resp_len - 2]; + uint8_t sw2 = resp[resp_len - 1]; + fmcos_print_sw(sw1, sw2); + + if (sw1 == 0x90 && sw2 == 0x00) { + PrintAndLogEx(SUCCESS, "Application " _GREEN_("unblocked")); + } else { + PrintAndLogEx(FAILED, "Unblock command " _RED_("failed")); + } + + if (!keep) DropField(); + return (sw1 == 0x90 && sw2 == 0x00) ? PM3_SUCCESS : PM3_ESOFT; +} + +// --------------------------------------------------------------------------- +// Transaction history +// --------------------------------------------------------------------------- + +static int CmdHFFmcosHistory(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf fmcos history", + "Read transaction history records from a loop (cyclic) EF.\n" + "Each record is 23 bytes: serial[2] | od_limit[3] | amount[4] | type[1] | terminal[6] | date[4] | time[3]\n" + "Record 1 is the most recent. Reading stops when the card returns a non-9000 SW.", + "hf fmcos history --fid 18\n" + "hf fmcos history --fid 19 --count 20"); + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "fid", "", "loop file SFI (1 byte, e.g. 18 for wallet, 19 for passbook)"), + arg_int0(NULL, "count", "", "max records to read (default 10, 0=read all up to 255)"), + arg_lit0("k", "keep", "keep field ON after command"), + arg_lit0("a", "apdu", "show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t fid_buf[1] = {0}; int fid_len = 0; + CLIGetHexWithReturn(ctx, 1, fid_buf, &fid_len); + int count = arg_get_int_def(ctx, 2, 10); + bool keep = arg_get_lit(ctx, 3); + bool apdu_log = arg_get_lit(ctx, 4); + CLIParserFree(ctx); + + if (fid_len != 1) { PrintAndLogEx(ERR, "--fid must be 1 byte"); return PM3_EINVARG; } + if (count < 0 || count > 255) { PrintAndLogEx(ERR, "--count must be 0-255"); return PM3_EINVARG; } + if (count == 0) count = 255; + + SetAPDULogging(apdu_log); + + uint8_t sfi = fid_buf[0] & 0x1F; + uint8_t p2 = (sfi << 3) | 0x04; + + static const struct { uint8_t code; const char *name; } tx_types[] = { + {0x04, "PB cash W/D "}, + {0x05, "PB purchase "}, + {0x06, "WL purchase "}, + {0x07, "OD limit upd"}, + {0x09, "Compound pur"}, + }; + const size_t ntypes = sizeof(tx_types) / sizeof(tx_types[0]); + + PrintAndLogEx(INFO, " # | Date | Time | Type | Amount | OD Limit | Serial | Terminal"); + PrintAndLogEx(INFO, "---+------------+----------+--------------+------------+----------+--------+-------------------"); + + int found = 0; + for (int rec = 1; rec <= count; rec++) { + uint8_t apdu[5] = {0x00, 0xB2, (uint8_t)rec, p2, 23}; + uint8_t resp[APDU_RES_LEN] = {0}; + int resp_len = 0; + + if (fmcos_send_apdu(apdu, sizeof(apdu), true, true, resp, &resp_len) != PM3_SUCCESS) + break; + + if (resp_len < 2) break; + uint8_t sw1 = resp[resp_len - 2], sw2 = resp[resp_len - 1]; + if (sw1 != 0x90 || sw2 != 0x00) { + if (rec == 1) fmcos_print_sw(sw1, sw2); + break; + } + if (resp_len < 25) break; + + uint8_t *r = resp; + uint16_t serial = ((uint16_t)r[0] << 8) | r[1]; + uint32_t od_limit = ((uint32_t)r[2] << 16) | ((uint32_t)r[3] << 8) | r[4]; + uint32_t amount = ((uint32_t)r[5] << 24) | ((uint32_t)r[6] << 16) | + ((uint32_t)r[7] << 8) | r[8]; + uint8_t tx_type = r[9]; + uint8_t *terminal = r + 10; + uint8_t *date = r + 16; + uint8_t *ttime = r + 20; + + // BCD date YYYYMMDD → "YYYY-MM-DD" + char date_str[11]; + snprintf(date_str, sizeof(date_str), "%02X%02X-%02X-%02X", + date[0], date[1], date[2], date[3]); + // BCD time HHMMSS → "HH:MM:SS" + char time_str[9]; + snprintf(time_str, sizeof(time_str), "%02X:%02X:%02X", + ttime[0], ttime[1], ttime[2]); + + char type_hex_buf[13]; + snprintf(type_hex_buf, sizeof(type_hex_buf), "0x%02X ", tx_type); + const char *type_name = type_hex_buf; + for (size_t i = 0; i < ntypes; i++) { + if (tx_types[i].code == tx_type) { type_name = tx_types[i].name; break; } + } + + PrintAndLogEx(INFO, "%2d | %s | %s | %s | %10u | %8u | %06X | %s", + rec, date_str, time_str, type_name, + amount, od_limit, serial, + sprint_hex(terminal, 6)); + found++; + } + + if (!keep) DropField(); + + if (found == 0) + PrintAndLogEx(INFO, "(no records found)"); + else + PrintAndLogEx(SUCCESS, "%d record%s", found, found == 1 ? "" : "s"); + + return PM3_SUCCESS; +} + +// --------------------------------------------------------------------------- +// Top-level command table +// --------------------------------------------------------------------------- + +static command_t CommandTable[] = { + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("Card information") " ---------"}, + {"info", CmdHFFmcosInfo, IfPm3Iso14443a, "Detect card and print file-system info"}, + {"select", CmdHFFmcosSelect, IfPm3Iso14443a, "SELECT FILE by 2-byte ID or AID name"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("File management") " ----------"}, + {"erase", CmdHFFmcosErase, IfPm3Iso14443a, "ERASE DF contents"}, + {"create", CmdHFFmcosCreate, IfPm3Iso14443a, "{ Create directory / EF / keyfile... }"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("Data access") " --------------"}, + {"read", CmdHFFmcosRead, IfPm3Iso14443a, "{ Read binary / record... }"}, + {"write", CmdHFFmcosWrite, IfPm3Iso14443a, "{ Write binary / record... }"}, + {"append", CmdHFFmcosAppend, IfPm3Iso14443a, "APPEND RECORD to cyclic / linear EF"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("Authentication") " -----------"}, + {"auth", CmdHFFmcosAuth, IfPm3Iso14443a, "{ External / internal authenticate... }"}, + {"key", CmdHFFmcosWriteKey, IfPm3Iso14443a, "WRITE KEY to keyfile"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("PIN management") " -----------"}, + {"pin", CmdHFFmcosPin, IfPm3Iso14443a, "{ Verify / change / reset / unblock PIN }"}, + {"--------", CmdHelp, AlwaysAvailable, "--------- " _CYAN_("Financial") " ----------------"}, + {"balance", CmdHFFmcosBalance, IfPm3Iso14443a, "GET BALANCE (wallet or passbook)"}, + {"credit", CmdHFFmcosCredit, IfPm3Iso14443a, "ADD CREDIT to wallet or passbook"}, + {"purchase", CmdHFFmcosPurchase, IfPm3Iso14443a, "PURCHASE from wallet or passbook"}, + {"overdraft", CmdHFFmcosOverdraft,IfPm3Iso14443a, "UPDATE OVERDRAFT LIMIT"}, + {"history", CmdHFFmcosHistory, IfPm3Iso14443a, "READ transaction history from loop EF"}, + {"block", CmdHFFmcosBlock, IfPm3Iso14443a, "BLOCK card or application"}, + {"unblock", CmdHFFmcosUnblock, IfPm3Iso14443a, "UNBLOCK application"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} + +int CmdHFFmcos(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} diff --git a/client/src/cmdhffmcos.h b/client/src/cmdhffmcos.h new file mode 100644 index 000000000..9076a8914 --- /dev/null +++ b/client/src/cmdhffmcos.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// Commands for FMCOS CPU smart cards (Fudan Microelectronics) +//----------------------------------------------------------------------------- + +#ifndef CMDHFFMCOS_H__ +#define CMDHFFMCOS_H__ + +#include "common.h" + +int CmdHFFmcos(const char *Cmd); + +#endif // CMDHFFMCOS_H__ diff --git a/doc/fmcos.md b/doc/fmcos.md new file mode 100644 index 000000000..98b34eee2 --- /dev/null +++ b/doc/fmcos.md @@ -0,0 +1,882 @@ +# FMCOS CPU Smart Card Commands + + +FMCOS (Fudan Microelectronics CPU OS) is an ISO14443-A CPU card operating system used in +Chinese transit and e-wallet cards (PBOC compliant). Common hardware: FM1208-09, FM1216. + +All commands in this family are reachable via `hf fmcos `. + +--- + +# Table of Contents + +- [Card Information](#card-information) + - [info](#info) + - [select](#select) +- [File Management](#file-management) + - [erase](#erase) + - [create dir](#create-dir) + - [create file](#create-file) + - [create keyfile](#create-keyfile) +- [Data Access](#data-access) + - [read binary](#read-binary) + - [read record](#read-record) + - [write binary](#write-binary) + - [write record](#write-record) + - [append](#append) + - [key (write key)](#key-write-key) +- [Authentication](#authentication) + - [auth external](#auth-external) + - [auth internal](#auth-internal) +- [PIN Management](#pin-management) + - [pin verify](#pin-verify) + - [pin change](#pin-change) + - [pin reset](#pin-reset) + - [pin unblock](#pin-unblock) +- [Financial Operations](#financial-operations) + - [balance](#balance) + - [credit](#credit) + - [purchase](#purchase) + - [overdraft](#overdraft) + - [history](#history) +- [Card Lifecycle](#card-lifecycle) + - [block](#block) + - [unblock](#unblock) +- [File Access Reference](#file-access-reference) +- [Key Types Reference](#key-types-reference) +- [File Protection Modes](#file-protection-modes) +- [Access Rights Byte](#access-rights-byte) +- [Complete Wallet Session Walkthrough](#complete-wallet-session-walkthrough) + +--- + +## Card Information + +### info + +Detect a FMCOS card and dump its file-system layout: MF, DFs, and EFs with their file +identifiers, types, sizes, and access attributes. + +``` +hf fmcos info +``` + +### select + +SELECT a file or application directory by 2-byte file ID (hex) or by DF name (ASCII string). +After selection subsequent commands operate within that context. + +``` +hf fmcos select --id 3f00 +hf fmcos select --id 3f01 +hf fmcos select --name 77616C6C657454657374 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | 2-byte file ID (e.g. `3f00` for MF, `3f01` for an ADF) | +| `--name ` | DF name bytes as hex (up to 16 bytes, e.g. `77616C6C657454657374` = `walletTest`) | +| `-k` / `--keep` | Keep the RF field on after the command | + +--- + +## File Management + +### erase + +ERASE DF -- delete all EFs and sub-DFs from the currently selected DF, but keep the DF +itself and its keyfile. Requires the MF or relevant DF to be selected first. + +``` +hf fmcos select --id 3f00 +hf fmcos erase +``` + +### create dir + +CREATE DF (directory / application directory). + +``` +hf fmcos create dir --id 3f01 --space 1500 --cperm f0 --eperm f0 --appid 95 --name 77616C6C657454657374 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | 2-byte file ID for the new DF | +| `--space ` | Total byte space to reserve (hex, e.g. `1500` = 5376 bytes) | +| `--cperm ` | Create-permission byte (e.g. `f0` = always allowed) | +| `--eperm ` | Erase-permission byte | +| `--appid ` | 1-byte application SID / AID tag | +| `--name ` | Optional DF name bytes as hex (up to 16 bytes, enables select-by-name) | + +### create file + +CREATE EF (elementary file) in the currently selected DF. + +``` +# Unprotected binary file +hf fmcos create file --id 0002 --type bin --size 50 --rperm f0 --wperm f0 --access ff + +# Variable-length record file with MAC-only line protection +hf fmcos create file --id 0006 --type var --size 50 --rperm f0 --wperm f0 --access 7f --prot mac + +# Loop (cyclic) file with MAC+encryption +hf fmcos create file --id 000a --type loop --size 210 --rperm f0 --wperm f0 --access 7f --prot enc + +# Wallet/passbook balance file (EDEP) linked to loop file 0x0018 +hf fmcos create file --id 0002 --type wallet --size 0208 --rperm f0 --wperm 00 --access 18 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | 2-byte file ID | +| `--type ` | `bin` (0x28), `fix` (0x2A), `var` (0x2C), `loop` (0x2E), `wallet` (0x2F) | +| `--size ` | File size in bytes (hex, e.g. `0208` = 520) | +| `--rperm ` | Read/usage-rights byte; for wallet type this is the single usage rights byte | +| `--wperm ` | Write permission byte; ignored for wallet type (always sent as 0x00) | +| `--access ` | Access-rights byte; for wallet type this is the low byte of the linked loop EF's file ID | +| `--prot ` | Line-protection mode: `none` (default), `mac`, `enc` (MAC+encrypt) | + +**File type encodings:** + +| Name | Code | Description | +|------|------|-------------| +| `bin` | 0x28 | Binary transparent file | +| `fix` | 0x2A | Fixed-length record file | +| `var` | 0x2C | Variable-length record file | +| `loop` | 0x2E | Cyclic (loop) file -- used for transaction logs | +| `wallet` | 0x2F | E-purse wallet / passbook balance file | + +### create keyfile + +CREATE KEYFILE in the currently selected DF. A DF must have a keyfile before any keys +can be written to it. + +``` +hf fmcos create keyfile --id 0000 --space 200 --dfsid 95 --perm f0 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | 2-byte file ID for the keyfile (commonly `0000`) | +| `--space ` | Space to reserve for key storage (bytes, hex, e.g. `200` = 512) | +| `--dfsid ` | Parent DF SID (must match the DF's `--appid` value) | +| `--perm ` | Key access permission byte | + +--- + +## Data Access + +### read binary + +READ BINARY from the currently selected transparent (bin) EF. + +``` +# Plain read +hf fmcos read binary --p1 00 --p2 00 --len 10 + +# With MAC line-protection (verifies response MAC) +hf fmcos read binary --p1 00 --p2 00 --len 10 --prot mac --key 36363636363636363636363636363636 + +# With MAC+encryption (decrypts response) +hf fmcos read binary --p1 00 --p2 00 --len 10 --prot enc --key 36363636363636363636363636363636 +``` + +| Flag | Description | +|------|-------------| +| `--p1 ` | P1 byte (high byte of file offset) | +| `--p2 ` | P2 byte (low byte of file offset) | +| `--len ` | Number of bytes to read (Le) | +| `--prot ` | `none`, `mac`, or `enc` | +| `--key ` | Line-protection key (8 or 16 bytes, required when `--prot` is mac/enc) | + +### read record + +READ RECORD from the currently selected record or cyclic EF. + +``` +# Read record 1 from var file 0x06 (plain) +hf fmcos read record --rec 01 --fid 06 --len 10 + +# Read with MAC verification +hf fmcos read record --rec 01 --fid 07 --len 10 --prot mac --key 36363636363636363636363636363636 + +# Read with decryption +hf fmcos read record --rec 01 --fid 08 --len 10 --prot enc --key 36363636363636363636363636363636 +``` + +| Flag | Description | +|------|-------------| +| `--rec ` | Record number (P1); `00` = current record | +| `--fid ` | File ID in P2 (upper 5 bits); `00` = current file | +| `--len ` | Number of bytes to read | +| `--prot ` | `none`, `mac`, or `enc` | +| `--key ` | Line-protection key when prot is mac/enc | + +### write binary + +UPDATE BINARY -- write data to the currently selected transparent EF. + +``` +# Plain write +hf fmcos write binary --p1 00 --p2 00 --data 11121314151617181910 + +# Write with MAC +hf fmcos write binary --p1 00 --p2 00 --data 21222324252627282920 \ + --prot mac --key 36363636363636363636363636363636 + +# Write with MAC+encryption (data is encrypted before sending) +hf fmcos write binary --p1 00 --p2 00 --data 31323334353637383930 \ + --prot enc --key 36363636363636363636363636363636 +``` + +| Flag | Description | +|------|-------------| +| `--p1 ` | P1 byte (high offset byte) | +| `--p2 ` | P2 byte (low offset byte) | +| `--data ` | Data bytes to write | +| `--prot ` | `none`, `mac`, or `enc` | +| `--key ` | Line-protection key | + +### write record + +UPDATE RECORD -- write a record into the currently selected EF. + +``` +# Plain record write (P1=record number, P2=file-id<<3|04) +hf fmcos write record --rec 01 --fid 06 --data 5152535455565758595a + +# With MAC +hf fmcos write record --rec 01 --fid 07 --data 6162636465666768696a \ + --prot mac --key 36363636363636363636363636363636 + +# With MAC+encryption +hf fmcos write record --rec 01 --fid 08 --data 7172737475767778797a \ + --prot enc --key 36363636363636363636363636363636 +``` + +| Flag | Description | +|------|-------------| +| `--rec ` | Record number (P1) | +| `--fid ` | File ID for P2 encoding | +| `--data ` | Record data bytes | +| `--prot ` | `none`, `mac`, or `enc` | +| `--key ` | Line-protection key | + +### append + +APPEND RECORD -- append a new record to a cyclic (loop) EF. + +``` +# Plain append +hf fmcos append --fid 0a --data 9192939495969798999a + +# With MAC +hf fmcos append --fid 0b --data a1a2a3a4a5a6a7a8a9a0 \ + --prot mac --key 36363636363636363636363636363636 + +# With MAC+encryption +hf fmcos append --fid 0c --data b1b2b3b4b5b6b7b8b9b0 \ + --prot enc --key 36363636363636363636363636363636 +``` + +| Flag | Description | +|------|-------------| +| `--fid ` | File ID of the loop EF | +| `--data ` | Record data bytes | +| `--prot ` | `none`, `mac`, or `enc` | +| `--key ` | Line-protection key | + +### key (write key) + +WRITE KEY -- write a key entry into the currently selected keyfile. Use `--op 01` to add +a new key, `--op 02` to update an existing one. + +**Group A keys** (DES/3DES data keys with version + algorithm fields): + +``` +# Add InternalKey (0x34) at slot 0, always-free usage +hf fmcos key --op 01 --id 00 --type internal \ + --usage f0 --change 02 --version 00 --algo 01 \ + --key 2b8a438742c851566f02d881b09d58c0 + +# Add CreditKey (0x3F) at slot 1 +hf fmcos key --op 01 --id 01 --type credit \ + --usage f0 --change 02 --version 00 --algo 01 \ + --key a9e6e145f5df09500a58eef8575d49db + +# Add PurchaseKey (0x3E) at slot 1 +hf fmcos key --op 01 --id 01 --type purchase \ + --usage f0 --change 02 --version 00 --algo 01 \ + --key eb18ce6986c820970e876219052ce0cf +``` + +**Group B keys** (PIN and external-auth keys with error counter): + +``` +# Add PIN key (0x3A) at slot 0 -- pin value is 2-6 raw bytes +hf fmcos key --op 01 --id 00 --type pin \ + --usage f0 --followup 01 --errcount 33 \ + --key 123456 + +# Add ExternalAuth key (0x39) at slot 0 +hf fmcos key --op 01 --id 00 --type extauth \ + --usage f0 --change 02 --followup 44 --errcount 33 \ + --key f49dc1ba1b4deb5264718bc559106c0d +``` + +**Group C keys** (line-protection, unlock-PIN, change-PIN): + +``` +# Add line-protection key (0x36) at slot 0 +hf fmcos key --op 01 --id 00 --type lineprotect \ + --usage f0 --change 02 --errcount ff \ + --key 8a021972bfec9d152ca9eb82d7d12c09 + +# Add unlock-PIN key (0x37) +hf fmcos key --op 01 --id 00 --type unlockpin \ + --usage f0 --change 02 --errcount 33 \ + --key d8f60fa2d791f3a658d27c0545824300 + +# Add change-PIN key (0x38) +hf fmcos key --op 01 --id 00 --type changepin \ + --usage f0 --change 02 --errcount 33 \ + --key fb487a6d1b7cbf1bf84c666b8338376e +``` + +**Key types:** + +| Name | Code | Group | Description | +|------|------|-------|-------------| +| `desenc` | 0x30 | A | DES encrypt key | +| `desdec` | 0x31 | A | DES decrypt key | +| `desmac` | 0x32 | A | DES MAC key | +| `internal` | 0x34 | A | Internal-auth / TAC key | +| `overdraft` | 0x3C | A | Overdraft-limit key | +| `debit` | 0x3D | A | Debit (online transfer) key | +| `purchase` | 0x3E | A | Purchase / debit key | +| `credit` | 0x3F | A | Credit key | +| `extauth` | 0x39 | B | External-authentication key | +| `pin` | 0x3A | B | PIN code key | +| `lineprotect` | 0x36 | C | Line-protection key | +| `unlockpin` | 0x37 | C | Unlock-PIN key | +| `changepin` | 0x38 | C | Change-PIN key | + +--- + +## Authentication + +### auth external + +EXTERNAL AUTHENTICATE -- authenticate the reader to the card. The card issues a challenge, +the reader encrypts it with the external-auth key, and sends the response back. + +``` +hf fmcos auth external --id 00 --key f49dc1ba1b4deb5264718bc559106c0d +``` + +| Flag | Description | +|------|-------------| +| `--id ` | Key slot ID | +| `--key ` | External-auth key (8 or 16 bytes) | + +### auth internal + +INTERNAL AUTHENTICATE -- authenticate the card to the reader. The reader sends an 8-byte +challenge (`--data`); the card responds with a DES-encrypted value that the reader verifies +offline. + +``` +hf fmcos auth internal --p1 00 --p2 00 --data 0102030405060708 +``` + +| Flag | Description | +|------|-------------| +| `--p1 ` | P1 byte (typically `00`) | +| `--p2 ` | P2 byte (typically `00`) | +| `--data ` | 8-byte challenge sent to the card | + +--- + +## PIN Management + +### pin verify + +VERIFY PIN -- present the PIN code to the card to unlock PIN-gated operations. +PIN is 2-6 raw bytes. + +``` +hf fmcos pin verify --id 00 --pin 123456 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | PIN key slot ID | +| `--pin ` | PIN bytes (2-6 bytes) | + +### pin change + +CHANGE PIN -- change the PIN using the current (old) PIN for authorization. + +``` +hf fmcos pin change --id 00 --old 123456 --new 13371337 +``` + +| Flag | Description | +|------|-------------| +| `--id ` | PIN key slot ID | +| `--old ` | Current PIN (2-6 bytes) | +| `--new ` | New PIN (2-6 bytes) | + +### pin reset + +RESET PIN -- set a new PIN using the change-PIN key MAC for authorization (no old PIN needed). +The command computes a MAC over the new PIN using the change-PIN key and sends it to the card. + +``` +hf fmcos pin reset --id 00 --pin 13371337 \ + --key fb487a6d1b7cbf1bf84c666b8338376e +``` + +| Flag | Description | +|------|-------------| +| `--id ` | PIN key slot ID | +| `--pin ` | New PIN (2-6 bytes) | +| `--key ` | Change-PIN key (16 bytes); MAC = DES-MAC(new_pin, XOR_halves(key)) | + +### pin unblock + +UNBLOCK PIN -- clear the PIN blocked state and set a new PIN. +The new PIN is encrypted with the unlock-PIN key and a GET CHALLENGE IV. + +``` +hf fmcos pin unblock --id 00 --pin 123456 \ + --key d8f60fa2d791f3a658d27c054582430e +``` + +| Flag | Description | +|------|-------------| +| `--id ` | PIN key slot ID | +| `--pin ` | New PIN (2-6 bytes) | +| `--key ` | Unlock-PIN key (16 bytes) | + +--- + +## Financial Operations + +FMCOS implements a two-phase PBOC e-wallet protocol. Phase 1 initializes the transaction +and returns card-computed data (old balance, serial number, random seed, MAC1). Phase 2 +commits the transaction with a terminal-computed MAC2 and receives a Transaction +Authentication Code (TAC) from the card which the terminal verifies. + +**Keys involved:** + +| Key | Role | +|-----|------| +| Credit key (16 B) | Derive the session process key for credit operations | +| Purchase key (16 B) | Derive the session process key for purchase/debit | +| Overdraft key (16 B) | Derive the session process key for overdraft-limit updates | +| Internal key / DTK (16 B) | Verify TAC: all financial commands use `tac_key = XOR(ikey[0:8], ikey[8:16])` → DES-CBC-MAC | + +**Terminal ID:** 6 bytes identifying the terminal. Use any fixed value for testing (e.g. `666666666666`). + +### balance + +GET BALANCE -- read the current balance from the wallet or passbook balance file. + +``` +hf fmcos balance --type wallet +hf fmcos balance --type passbook +``` + +| Flag | Description | +|------|-------------| +| `--type ` | `wallet` (0x02) or `passbook` (0x01) | +| `-k` / `--keep` | Keep field on | + +**APDU:** `80 5C 00 04` +**Response:** 4-byte big-endian balance. + +Example output: + +``` +[=] Balance (wallet): 1000 (0x000003E8) +``` + +### credit + +ADD CREDIT -- two-phase credit transaction. + +**Phase 1** (`INS 50`, P1=00): send key ID, amount, terminal ID. Card returns old balance, +transaction serial, key version, algo ID, random seed, and MAC1. The implementation: +- Derives the process key: `encrypt(random[4] | serial[2], credit_key)` -> first 8 bytes +- Verifies MAC1: `DES-CBC-MAC(old_bal[4] | amount[4] | type[1] | terminal[6], process_key)` + +**Phase 2** (`INS 52`, P1=00): send date, time, MAC2. Card returns TAC. The implementation: +- Computes MAC2: `DES-CBC-MAC(amount[4] | type[1] | terminal[6] | date[4] | time[3], process_key)` +- Verifies TAC: `DES-CBC-MAC(new_bal[4] | serial[2] | MAC2_buf[18], tac_key)` + +``` +hf fmcos credit --type wallet --id 01 --amount 1000 \ + --terminal 666666666666 \ + --key a9e6e145f5df09500a58eef8575d49db \ + --ikey 2b8a438742c851566f02d881b09d58c0 + +hf fmcos credit --type passbook --id 01 --amount 2000 \ + --terminal 666666666666 \ + --key a9e6e145f5df09500a58eef8575d49db \ + --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +| Flag | Description | +|------|-------------| +| `--type ` | `wallet` or `passbook` | +| `--id ` | Credit key slot ID (1 byte decimal) | +| `--amount ` | Credit amount (integer, units match card configuration) | +| `--terminal ` | Terminal ID (6 bytes) | +| `--key ` | Credit key (16 bytes) | +| `--ikey ` | Internal key (16 bytes) for TAC verification | +| `-k` / `--keep` | Keep field on after command | + +> **Note**: FMCOS resets the card's security status after each completed financial transaction. +> Re-verify PIN before each credit or purchase operation (SW:6985 indicates the security status was cleared). + +Example output: + +``` +[=] MAC1 OK old balance 0 +[+] TAC OK new balance 1000 +[+] SW: 9000 - Success +``` + +### purchase + +PURCHASE -- two-phase debit transaction from wallet or passbook. + +**Phase 1** (`INS 50`, P1=01): send key ID, amount, terminal. Card returns old balance, +offline serial, overdraft limit, key version, algo, random seed (15 bytes total). +- Derives process key: `encrypt(random[4] | offline_serial[2] | tx_serial[2], purchase_key)[:8]` +- Computes MAC1: `DES-CBC-MAC(amount[4] | tx_type[1] | terminal[6] | date[4] | time[3], process_key)` + +**Phase 2** (`INS 54`, P1=01, P2=00): send tx serial, date, time, MAC1. Card returns TAC[4]+MAC2[4]. +- Verifies TAC: `DES-CBC-MAC(amount[4] | tx_type[1] | terminal[6] | serial[4] | date[4] | time[3], tac_key)` + +Transaction type byte: 0x05 for passbook purchase, 0x06 for wallet purchase. + +``` +# Wallet purchase of 50 units +hf fmcos purchase --type wallet --id 01 --amount 50 \ + --terminal 666666666666 \ + --key eb18ce6986c820970e876219052ce0cf \ + --ikey 2b8a438742c851566f02d881b09d58c0 \ + --serial 01020304 + +# Passbook purchase (no explicit serial -- defaults to 00000001) +hf fmcos purchase --type passbook --id 01 --amount 50 \ + --terminal 666666666666 \ + --key eb18ce6986c820970e876219052ce0cf \ + --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +| Flag | Description | +|------|-------------| +| `--type ` | `wallet` or `passbook` | +| `--id ` | Purchase key slot ID | +| `--amount ` | Debit amount | +| `--terminal ` | Terminal ID (6 bytes) | +| `--key ` | Purchase key (16 bytes) | +| `--ikey ` | Internal key (16 bytes) for TAC verification | +| `--serial ` | 4-byte transaction serial (optional, default `00000001`) | +| `-k` / `--keep` | Keep field on | + +Example output: + +``` +[=] Old balance: 1000 +[+] TAC OK new balance 950 +[+] SW: 9000 - Success +``` + +### overdraft + +UPDATE OVERDRAFT LIMIT -- two-phase overdraft-limit update on the passbook. + +**Phase 1** (`INS 50`, P1=04, P2=01): send key ID and terminal. Card returns old balance, +online serial, old limit, key version, algo, random seed, and MAC1 (19 bytes total). +- Derives process key: `encrypt(random[4] | serial[2], overdraft_key)[:8]` +- Verifies MAC1: `DES-CBC-MAC(old_bal[4] | old_limit[3] | 0x07[1] | terminal[6], process_key)` + +**Phase 2** (`INS 58`, P1=00, P2=00): send new limit (3 bytes), date, time, MAC2. +- Computes MAC2: `DES-CBC-MAC(new_limit[3] | 0x07[1] | terminal[6] | date[4] | time[3], process_key)` +- Card returns TAC[4]. When `--ikey` is provided the TAC is verified: + `DES-CBC-MAC(XOR(ikey[0:8], ikey[8:16]), tac_bal[4] | online_serial[2] | new_limit[3] | 0x07[1] | terminal[6] | date[4] | time[3])` + where `tac_bal = old_balance + new_limit - old_od_limit`. The card stores + `actual_funds + overdraft_limit` as its balance field, so when the limit changes the new + stored balance shifts by the limit delta. + +``` +hf fmcos overdraft --id 01 --limit 1000 \ + --terminal 666666666666 \ + --key 94f63c4fae5e4977d749928ad12bc128 \ + --ikey 659a500f0f1fce35b6884bdff966576a +``` + +| Flag | Description | +|------|-------------| +| `--id ` | Overdraft key slot ID | +| `--limit ` | New overdraft limit (24-bit integer, max 16777215) | +| `--terminal ` | Terminal ID (6 bytes) | +| `--key ` | Overdraft key (16 bytes) | +| `--ikey ` | Internal key DTK (16 bytes) for TAC verification (optional) | +| `-k` / `--keep` | Keep field on | + +Example output: + +``` +[=] Old balance: 1000 old overdraft limit: 0 +[=] MAC1 OK +[+] Overdraft limit updated to 1000 +[+] TAC OK aabbccdd +[+] SW: 9000 - Success +``` + +### history + +READ TRANSACTION HISTORY -- decode all records in the loop (cyclic) EF used as a transaction +log. The card appends a 23-byte record to the loop file after every financial operation. + +``` +# Wallet transaction log (loop file SFI 0x18 in the example setup) +hf fmcos history --fid 18 + +# Passbook transaction log, read up to 20 records +hf fmcos history --fid 19 --count 20 +``` + +| Flag | Description | +|------|-------------| +| `--fid ` | Loop file SFI byte (1 byte, e.g. `18` for wallet loop, `19` for passbook loop) | +| `--count ` | Max records to read (default 10; `0` = read all, up to 255) | +| `-k` / `--keep` | Keep field on after command | +| `-a` / `--apdu` | Show raw APDU traffic | + +**Record layout (23 bytes):** + +| Offset | Length | Field | Notes | +|--------|--------|-------|-------| +| 0 | 2 | Serial | Transaction serial number (big-endian) | +| 2 | 3 | OD limit | Overdraft limit at time of transaction | +| 5 | 4 | Amount | Transaction amount (big-endian) | +| 9 | 1 | Type | Transaction type byte | +| 10 | 6 | Terminal | Terminal ID | +| 16 | 4 | Date | BCD date `YYYYMMDD` | +| 20 | 3 | Time | BCD time `HHMMSS` | + +**Transaction type codes:** + +| Code | Description | +|------|-------------| +| `0x04` | Passbook cash withdrawal | +| `0x05` | Passbook purchase | +| `0x06` | Wallet purchase | +| `0x07` | Overdraft limit update | +| `0x09` | Compound purchase | + +Example output: + +``` + # | Date | Time | Type | Amount | OD Limit | Serial | Terminal +---+------------+----------+--------------+------------+----------+--------+------------------- + 1 | 2026-05-24 | 14:30:22 | WL purchase | 50 | 0 | 000002 | 66 66 66 66 66 66 + 2 | 2026-05-24 | 14:28:05 | WL purchase | 1000 | 0 | 000001 | 66 66 66 66 66 66 +[+] 2 records +``` + +> **Note**: Record 1 is always the most recently written entry. Reading stops automatically +> when the card returns a non-9000 SW (record number exceeds log capacity). + +--- + +## Card Lifecycle + +### block + +BLOCK the entire card (CARD BLOCK, `INS 16`) or the currently selected application +(APP BLOCK, `INS 1E`). Uses the line-protection key to compute a packet MAC over the +command header via GET CHALLENGE. + +``` +# Block the card permanently +hf fmcos block --card --key 8a021972bfec9d152ca9eb82d7d12c09 + +# Block application temporarily (default) +hf fmcos block --app --key 8a021972bfec9d152ca9eb82d7d12c09 + +# Block application permanently +hf fmcos block --app --perm --key 8a021972bfec9d152ca9eb82d7d12c09 +``` + +| Flag | Description | +|------|-------------| +| `--card` | Block the whole card | +| `--app` | Block the current application | +| `--perm` | Permanent block (default is temporary for `--app`) | +| `--key ` | Line-protection key (8 or 16 bytes) | + +### unblock + +UNBLOCK the currently selected application (APP UNBLOCK, `INS 18`). +Same MAC pattern as block. + +``` +hf fmcos unblock --key 8a021972bfec9d152ca9eb82d7d12c09 +``` + +| Flag | Description | +|------|-------------| +| `--key ` | Line-protection key (8 or 16 bytes) | + +--- + +## File Access Reference + +### MF (Master File) + +- Automatically selected on card reset. +- Can be selected at any DF level using FID `3F00` or the MF name. +- Default name assigned at creation: `1PAY.SYS.DDF01`. + +### DF (Directory File) + +- Selected by file identifier (FID) or directory name (DF name). + +### Binary EF (type `0x28`) + +- Read with READ BINARY when the read condition is satisfied. +- Updated with UPDATE BINARY when the write condition is satisfied. + +### Fixed-Length Record EF (type `0x2A`) + +- Read a specific record with READ RECORD when the read condition is satisfied. +- Update a specific record with UPDATE RECORD when the write condition is satisfied. +- Append a record at the end with APPEND RECORD when the append condition is satisfied. + +### Cyclic (Loop) EF (type `0x2E`) + +- Read a specific record with READ RECORD when the read condition is satisfied. +- Prepend a new record at the front with APPEND RECORD when the append condition is satisfied. +- When the file is full, the oldest record is automatically overwritten. +- The most recently written record always has record number 1; the prior record is number 2; and so on. + +### Wallet/Purse EF (EDEP/EP, type `0x2F`) + +- GET BALANCE reads the current balance. +- Under key control: CREDIT FOR LOAD (圈存), DEBIT FOR PURCHASE / CASH WITHDRAW (消费/取现), + DEBIT FOR UNLOAD (圈提), and UPDATE OVERDRAFT LIMIT (修改透支限额). + +### Variable-Length Record EF (type `0x2C`) + +- Read a specific record with READ RECORD when the read condition is satisfied. +- Update an existing record with UPDATE RECORD; append a new record with APPEND RECORD. +- **TLV format:** each record is `Tag (1 byte) | Length (1 byte) | Value (Length bytes)`. + Tag `0x00` is used by FMCOS for the standard record wrapper. +- UPDATE RECORD requires the new record's total TLV length to equal the original; otherwise the + command fails (SW `6A83`). + +### KEY File (type `0x3F`) + +- Only one KEY file is allowed per DF/MF; it **must be created before any other file** in that directory. +- Key data can **never be read out** from the card. +- While a DF/MF has no KEY file (and no other files), any file can be created and accessed without + access-rights restrictions. Once you leave and re-enter that directory, access rights are enforced. +- Each key is stored as a variable-length record: `key_data + 8 header bytes`. + - Triple-DES (16-byte) key record: **24 bytes** total. + - Single-DES (8-byte) key record: **16 bytes** total. +- WRITE KEY adds a new key (when the "add key" permission is satisfied) or updates key data + (when that specific key's "change" permission is satisfied). +- Key data can only be used when the key's "use" permission is satisfied. + +### Key Independence + +Each key is bound to exactly one function (encrypt, decrypt, MAC, etc.) and cannot be used +for any other function — including keys that generate, derive, or transport other card keys. + +### PIN Key + +- VERIFY checks the PIN; PIN CHANGE / UNBLOCK changes and optionally unlocks it. +- On a successful VERIFY, the security-status register is updated to the post-condition value + stored in the PIN key record. +- An error counter decrements on every failed VERIFY; when it reaches 0 the PIN key is locked. + +### Unlock-PIN Key + +- UNBLOCK verifies the unlock password and simultaneously unlocks a PIN key that was blocked + by repeated wrong attempts, while also setting a new PIN. +- Once the unlock-PIN key's own error counter reaches 0, it is permanently locked with no recovery. + +### External Authentication Key + +- EXTERNAL AUTHENTICATE can be executed when the key's use condition is satisfied. +- WRITE KEY updates the key when the change condition is satisfied. +- Once locked by exhausting its error counter, it **cannot be unlocked**. + +--- + +## Key Types Reference + +FMCOS keys are stored in a keyfile EF. Each key entry begins with a type byte that +encodes both the functional role (high nibble = 0x3x) and the line-protection mode +OR-ed in by `--prot` when writing the key itself. + +| Type name | Byte | Role | +|-----------|------|------| +| `desenc` | 0x30 | 3DES ECB encryption | +| `desdec` | 0x31 | 3DES ECB decryption | +| `desmac` | 0x32 | DES MAC generation | +| `internal` | 0x34 | Internal-authenticate / TAC key | +| `lineprotect` | 0x36 | Line-protection key (MAC-only or MAC+enc mode) | +| `unlockpin` | 0x37 | Authorize PIN unblock | +| `changepin` | 0x38 | Authorize PIN reset | +| `extauth` | 0x39 | External-authenticate key | +| `pin` | 0x3A | PIN code key | +| `overdraft` | 0x3C | Overdraft-limit session key | +| `debit` | 0x3D | Online-transfer (debit) session key | +| `purchase` | 0x3E | Purchase / offline-debit session key | +| `credit` | 0x3F | Credit session key | + +--- + +## File Protection Modes + +When creating a file or writing with protection, the `--prot` flag selects the mode: + +| Mode | Value | Description | +|------|-------|-------------| +| `none` | 0x00 | No line protection | +| `mac` | 0x80 | MAC-only; command includes 4-byte packet MAC, response includes MAC | +| `enc` | 0xC0 | MAC + encryption; data encrypted, 4-byte MAC appended | + +MAC is computed by `fmcos_packet_mac`: DES(8-byte key) or 3DES-Retail-MAC(16-byte key) +over `CLA|INS|P1|P2|Lc[|data]` with a GET CHALLENGE response as the CBC IV. + +--- + +## Access Rights Byte + +The access-rights byte passed to `create file` controls whether line protection is needed +and which key slot guards read / write access. + +``` +Bit 7 (MSB): 1 = protection NOT required, 0 = protection required +Bit 6-5: reserved +Bits 4-3: read key index (11=key0, 10=key1, 01=key2, 00=key3) +Bits 2-1 (LSB): write key index (11=key0, 10=key1, 01=key2, 00=key3) +``` + +Common values: + +| Value | Meaning | +|-------|---------| +| `ff` | No protection required, key0 for both read/write | +| `7f` | Protection required, key0 for both read/write | +| `f0` | No protection required (permission byte for directories/keys) | diff --git a/doc/fmcos_example.md b/doc/fmcos_example.md new file mode 100644 index 000000000..9dda57ff5 --- /dev/null +++ b/doc/fmcos_example.md @@ -0,0 +1,697 @@ +# FMCOS Wallet Walkthrough + +When commands are chained in a session the RF field must stay on between them. +Add `-k` (keep field on) to every command in a chain except the last one. + +## Keys used throughout + + +| Variable | Hex | +|---|---| +| `external_auth_key` | `f49dc1ba1b4deb52647186bc59106c0d` | +| `internal_key` | `2b8a438742c851566f02d881b09d58c0` | +| `line_protection_key` | `8a021972bfec9d152ca9eb82d7d12c09` | +| `unlock_pin_key` | `d8f60fa2d791f3a658d27c05458243ed` | +| `change_pin_key` | `fb487a6d1b7cbf1bf84c666b8338376e` | +| `purchase_key` | `eb18ce6986c820970e876219052ce0cf` | +| `credit_key` | `a9e6e145f5df09500a58eef8575d49db` | +| `debit_key` | `97fb4eda4b5237035946ee62d325d909` | +| `overdraw_limit_key` | `94f63c4fae5e4977d749928ad12bc128` | +| `int_enc` | `c4608b786af1992343e91a076670ae7c` | +| `int_dec` | `b8d4190c76856901fc686f36ab9b1ce0` | +| `int_mac` | `46a3ea8b254ee2749cc681050fd0dbcc` | +| PIN (`\x12\x34\x56`) | `123456` (3 bytes, raw BCD) | +| New PIN (`\x13\x37\x13\x37`) | `13371337` (4 bytes, raw BCD) | +| Terminal ID | `666666666666` (6 bytes) | + +--- + +## 1. reset — select MF and erase DF + +Select the Master File then erase the application directory from a previous run: + +``` +hf fmcos select --id 3f00 -k +hf fmcos erase +``` + +`hf fmcos erase` sends INS=0x0E to delete the currently-selected DF and all +its children. Run it only when the card already has a DF selected (the MF +itself cannot be erased this way). + +--- + +## 2. setup — create directory, keyfile, keys, loop files, balance files + +### 2a. Create the application directory (ADF) + +> **Note**: `--space` and `--size` arguments are parsed as **hexadecimal**. +> `--space 1500` = 0x1500 = 5376 bytes, `--size 0208` = 0x0208 = 520 bytes. + +`77616C6C657454657374` is the hex encoding of ASCII `walletTest`. + +``` +hf fmcos select --id 3f00 -k +hf fmcos create dir --id 3F01 --space 1500 --cperm F0 --eperm F0 --appid 95 --name 77616C6C657454657374 -k +``` + +### 2b. Select the new ADF by name + +``` +hf fmcos select --name 77616C6C657454657374 -k +``` + +All subsequent setup commands assume this DF remains selected (field stays on). + +### 2c. Create the keyfile + +``` +hf fmcos create keyfile --id 0000 --space 200 --dfsid 95 --perm F0 -k +``` + +### 2d. Write keys + +- `--op 01` = P1, authorization operation code (0x01 = add/update) +- `--id 00` = P2, key slot to write (0x00 = auto-assign next slot) +- `--usage F0` = usage rights byte + +All key writes continue in the same session so every command carries `-k`. + +**Key 0 — DES Encrypt (int_enc)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type desenc --change F4 --version 05 --algo 98 --key c4608b786af1992343e91a076670ae7c -k +``` + +**Key 1 — DES Decrypt (int_dec)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type desdec --change F4 --version 05 --algo 98 --key b8d4190c76856901fc686f36ab9b1ce0 -k +``` + +**Key 2 — DES MAC (int_mac)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type desmac --change F4 --version 05 --algo 98 --key 46a3ea8b254ee2749cc681050fd0dbcc -k +``` + +**Key 3 — Internal Key (internal_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type internal --change 02 --version 00 --algo 01 --key 2b8a438742c851566f02d881b09d58c0 -k +``` + +**Key 4 — File Line Protection Key (line_protection_key)** + +Group C type — uses `--change` and `--errcount`. +``` +hf fmcos key --op 01 --id 00 --usage F0 --type lineprotect --change 02 --errcount 33 --key 8a021972bfec9d152ca9eb82d7d12c09 -k +``` + +**Key 5 — Unlock PIN Key (unlock_pin_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type unlockpin --change 02 --errcount 33 --key d8f60fa2d791f3a658d27c05458243ed -k +``` + +**Key 6 — Change PIN Key (change_pin_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type changepin --change 02 --errcount 33 --key fb487a6d1b7cbf1bf84c666b8338376e -k +``` + +**Key 7 — External Authentication Key (external_auth_key)** + +Group B type with `--change` — uses `--change`, `--followup`, and `--errcount`. +``` +hf fmcos key --op 01 --id 00 --usage F0 --type extauth --change 02 --followup 44 --errcount 33 --key f49dc1ba1b4deb52647186bc59106c0d -k +``` + +**Key 8 — Purchase Key (purchase_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type purchase --change 02 --version 00 --algo 01 --key eb18ce6986c820970e876219052ce0cf -k +``` + +**Key 9 — Credit Key (credit_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type credit --change 02 --version 00 --algo 01 --key a9e6e145f5df09500a58eef8575d49db -k +``` + +**Key 10 — Debit Key (debit_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type debit --change 02 --version 00 --algo 01 --key 97fb4eda4b5237035946ee62d325d909 -k +``` + +**Key 11 — Overdraw Limit Key (overdraw_limit_key)** +``` +hf fmcos key --op 01 --id 00 --usage F0 --type overdraft --change 02 --version 00 --algo 01 --key 94f63c4fae5e4977d749928ad12bc128 -k +``` + +**Key 12 — PIN Key (pin_code)** + +The PIN `\x12\x34\x56` is 3 raw BCD bytes. +Group B type — uses `--followup` and `--errcount`. +``` +hf fmcos key --op 01 --id 00 --usage F0 --type pin --followup 01 --errcount 33 --key 123456 -k +``` + +### 2e. Create loop files for transaction logging + +Loop file 0x0018 (to be linked to the wallet balance file): +``` +hf fmcos create file --id 0018 --type loop --size 0517 --rperm F0 --wperm EF --access FF -k +``` + +Loop file 0x0019 (to be linked to the passbook balance file): +``` +hf fmcos create file --id 0019 --type loop --size 0517 --rperm F0 --wperm EF --access FF -k +``` + +### 2f. Create wallet and passbook balance files + +Wallet balance file (EF 0x0002, linked to loop file 0x0018): +``` +hf fmcos create file --id 0002 --type wallet --size 0208 --rperm F0 --wperm 00 --access 18 -k +``` + +Passbook balance file (EF 0x0001, linked to loop file 0x0019): +``` +hf fmcos create file --id 0001 --type wallet --size 0208 --rperm F0 --wperm 00 --access 19 +``` + +The last command drops the field to end the setup session. + +--- + +## 3. verify_pin — verify the PIN + +The PIN `\x12\x34\x56` is 3 raw BCD bytes. + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 +``` + +--- + +## 4. get_balance — read wallet and passbook balances + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos balance --type wallet -k +hf fmcos balance --type passbook +``` + +The command prints the 4-byte big-endian balance in decimal and hex. + +--- + +## 5. add_money — credit (load funds) + +Credit key index is 9 (written ninth in step 2d, 0-based index = 9 = 0x09). + +Credit 1000 units to the wallet, then 2000 to the passbook in one session: +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos credit --type wallet --id 09 --amount 1000 --terminal 666666666666 --key a9e6e145f5df09500a58eef8575d49db --ikey 2b8a438742c851566f02d881b09d58c0 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos credit --type passbook --id 09 --amount 2000 --terminal 666666666666 --key a9e6e145f5df09500a58eef8575d49db --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +> **Note**: FMCOS resets the card's internal security status after each completed +> financial transaction (Phase 1 + Phase 2). PIN verification must be repeated +> before each credit or purchase operation, even within the same RF session. + +`--key` is the credit_key (16-byte 3DES key used to derive the process key). +`--ikey` is the internal_key used for TAC verification. + +--- + +## 6. spend_wallet / spend_passbook — purchase (deduct funds) + +Purchase key index is 8 (0-based index = 8 = 0x08). + +Purchase (deduct) 50 units from the wallet: +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos purchase --type wallet --id 08 --amount 50 --terminal 666666666666 --key eb18ce6986c820970e876219052ce0cf --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +Purchase 50 units from the passbook: +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos purchase --type passbook --id 08 --amount 50 --terminal 666666666666 --key eb18ce6986c820970e876219052ce0cf --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +`--serial` (optional) sets the 4-byte transaction serial number; defaults to +`00000001` when omitted. + +--- + +## 7. withdraw_money — cash withdrawal (NOT SUPPORTED) + +Cash withdrawal uses INS=0x50 P1=0x02. There is currently no `hf fmcos` +command for this operation. + +--- + +## 8. pin_block — deliberately block the PIN + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 11223344 -k +hf fmcos pin verify --id 00 --pin 11223344 -k +hf fmcos pin verify --id 00 --pin 11223344 -k +hf fmcos pin verify --id 00 --pin 11223344 +``` + +After the error counter reaches zero the card blocks the PIN and returns +SW=`6983`. + +--- + +## 9. pin_unblock — restore a blocked PIN + +Unlock PIN key index is 5 (0-based index = 5 = 0x05). + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin unblock --id 00 --pin 123456 --key d8f60fa2d791f3a658d27c05458243ed +``` + +--- + +## 10. online_debit — online transfer (NOT SUPPORTED) + +Online debit uses INS=0x50 P1=0x05 (initialize) and INS=0x54 P1=0x03 +(commit). There is currently no `hf fmcos` command for this operation. + +--- + +## 11. update_overdraft — set the overdraft limit + +Overdraft key index is 11 (0-based index = 11 = 0x0B). + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos overdraft --id 0B --limit 1000 --terminal 666666666666 --key 94f63c4fae5e4977d749928ad12bc128 --ikey 2b8a438742c851566f02d881b09d58c0 +``` + +`--limit` is a 24-bit unsigned integer (maximum 16777215). + +`--ikey` is the internal key (DTK, 16 bytes). When provided the card's 4-byte TAC response is +verified using DES-CBC-MAC with `tac_key = XOR(ikey[0:8], ikey[8:16])` over: +`balance[4] | online_serial[2] | new_limit[3] | 0x07[1] | terminal[6] | date[4] | time[3]` + +--- + +## 12. get_history — read transaction history + +Read the most recent 10 records from the wallet loop file (SFI 0x18): + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos history --fid 18 +``` + +Read up to 20 records from the passbook loop file (SFI 0x19): + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos history --fid 19 --count 20 +``` + +Example output (after a credit of 1000 and a purchase of 50): + +``` + # | Date | Time | Type | Amount | OD Limit | Serial | Terminal +---+------------+----------+--------------+------------+----------+--------+------------------- + 1 | 2026-05-24 | 14:30:22 | WL purchase | 50 | 0 | 000002 | 66 66 66 66 66 66 + 2 | 2026-05-24 | 14:28:05 | WL purchase | 1000 | 0 | 000001 | 66 66 66 66 66 66 +[+] 2 records +``` + +The SFI bytes (`18`, `19`) match the loop file IDs created in step 2e. Loop file record 1 is +always the most recently written entry. + +--- + +## 13. pin_change — change PIN using old PIN authorization + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos pin change --id 00 --old 123456 --new 13371337 -k +hf fmcos pin verify --id 00 --pin 13371337 -k +hf fmcos pin change --id 00 --old 13371337 --new 123456 -k +hf fmcos pin verify --id 00 --pin 123456 +``` + +--- + +## 14. pin_reset — set new PIN using change-PIN key (no old PIN needed) + +Change PIN key index is 6 (0-based index = 6 = 0x06). + +``` +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos pin reset --id 00 --pin 13371337 --key fb487a6d1b7cbf1bf84c666b8338376e -k +hf fmcos pin verify --id 00 --pin 13371337 -k +hf fmcos pin change --id 00 --old 13371337 --new 123456 -k +hf fmcos pin verify --id 00 --pin 123456 +``` + +--- + +## Complete session (sequential) + +The following block shows a full session in order: reset, setup, load money, +spend, and check balance. Each sub-section starts a new session (field +activates) and ends when the last command drops the field. + +> **Note**: `--space` and `--size` are hex — `--space 1500` = 5376 bytes, `--space 400` = 1024 bytes, `--size 0517` = 1303 bytes, `--size 0208` = 520 bytes. + +``` +# ── 1. Reset ──────────────────────────────────────────────────────────────── +hf fmcos select --id 3f00 -k +hf fmcos erase + +# ── 2. Create ADF ──────────────────────────────────────────────────────────── +hf fmcos select --id 3f00 -k +hf fmcos create dir --id 3F01 --space 1500 --cperm F0 --eperm F0 --appid 95 --name 77616C6C657454657374 -k + +# ── 3. Select ADF ──────────────────────────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k + +# ── 4. Create keyfile ──────────────────────────────────────────────────────── +hf fmcos create keyfile --id 0000 --space 400 --dfsid 95 --perm F0 -k + +# ── 5. Write 13 keys (indexes 0-12) ───────────────────────────────────────── +hf fmcos key --op 01 --id 00 --usage F0 --type desenc --change F4 --version 05 --algo 98 --key c4608b786af1992343e91a076670ae7c -k +hf fmcos key --op 01 --id 00 --usage F0 --type desdec --change F4 --version 05 --algo 98 --key b8d4190c76856901fc686f36ab9b1ce0 -k +hf fmcos key --op 01 --id 00 --usage F0 --type desmac --change F4 --version 05 --algo 98 --key 46a3ea8b254ee2749cc681050fd0dbcc -k +hf fmcos key --op 01 --id 00 --usage F0 --type internal --change 02 --version 00 --algo 01 --key 2b8a438742c851566f02d881b09d58c0 -k +hf fmcos key --op 01 --id 00 --usage F0 --type lineprotect --change 02 --errcount 33 --key 8a021972bfec9d152ca9eb82d7d12c09 -k +hf fmcos key --op 01 --id 00 --usage F0 --type unlockpin --change 02 --errcount 33 --key d8f60fa2d791f3a658d27c05458243ed -k +hf fmcos key --op 01 --id 00 --usage F0 --type changepin --change 02 --errcount 33 --key fb487a6d1b7cbf1bf84c666b8338376e -k +hf fmcos key --op 01 --id 00 --usage F0 --type extauth --change 02 --followup 44 --errcount 33 --key f49dc1ba1b4deb52647186bc59106c0d -k +hf fmcos key --op 01 --id 00 --usage F0 --type purchase --change 02 --version 00 --algo 01 --key eb18ce6986c820970e876219052ce0cf -k +hf fmcos key --op 01 --id 00 --usage F0 --type credit --change 02 --version 00 --algo 01 --key a9e6e145f5df09500a58eef8575d49db -k +hf fmcos key --op 01 --id 00 --usage F0 --type debit --change 02 --version 00 --algo 01 --key 97fb4eda4b5237035946ee62d325d909 -k +hf fmcos key --op 01 --id 00 --usage F0 --type overdraft --change 02 --version 00 --algo 01 --key 94f63c4fae5e4977d749928ad12bc128 -k +hf fmcos key --op 01 --id 00 --usage F0 --type pin --followup 01 --errcount 33 --key 123456 -k + +# ── 6. Create loop files ───────────────────────────────────────────────────── +hf fmcos create file --id 0018 --type loop --size 0517 --rperm F0 --wperm EF --access FF -k +hf fmcos create file --id 0019 --type loop --size 0517 --rperm F0 --wperm EF --access FF -k + +# ── 7. Create balance files ────────────────────────────────────────────────── +hf fmcos create file --id 0002 --type wallet --size 0208 --rperm F0 --wperm 00 --access 18 -k +hf fmcos create file --id 0001 --type wallet --size 0208 --rperm F0 --wperm 00 --access 19 + +# ── 8. Verify PIN ──────────────────────────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 + +# ── 9. Credit wallet +1000, passbook +2000 ─────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos credit --type wallet --id 09 --amount 1000 --terminal 666666666666 --key a9e6e145f5df09500a58eef8575d49db --ikey 2b8a438742c851566f02d881b09d58c0 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos credit --type passbook --id 09 --amount 2000 --terminal 666666666666 --key a9e6e145f5df09500a58eef8575d49db --ikey 2b8a438742c851566f02d881b09d58c0 + +# ── 10. Check balances ─────────────────────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos balance --type wallet -k +hf fmcos balance --type passbook + +# ── 11. Purchase (spend) from wallet then passbook ─────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos purchase --type wallet --id 08 --amount 50 --terminal 666666666666 --key eb18ce6986c820970e876219052ce0cf --ikey 2b8a438742c851566f02d881b09d58c0 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos purchase --type passbook --id 08 --amount 50 --terminal 666666666666 --key eb18ce6986c820970e876219052ce0cf --ikey 2b8a438742c851566f02d881b09d58c0 + +# ── 12. Check balances again ───────────────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos balance --type wallet -k +hf fmcos balance --type passbook + +# ── 13. Update overdraft limit to 1000 ────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos pin verify --id 00 --pin 123456 -k +hf fmcos overdraft --id 0B --limit 1000 --terminal 666666666666 --key 94f63c4fae5e4977d749928ad12bc128 --ikey 2b8a438742c851566f02d881b09d58c0 + +# ── 14. Read transaction history ───────────────────────────────────────────── +hf fmcos select --name 77616C6C657454657374 -k +hf fmcos history --fid 18 -k +hf fmcos history --fid 19 +``` + +--- + +## File operations walkthrough + +That script demos binary/variable/loop file creation with line protection (none, MAC, MAC+enc), +plus application block/unblock. Two DFs are created on the same card: + +- **blockTest** (DF 3FFF, appid 0x94) — block/unblock target +- **fileTest** (DF 3F01, appid 0x95) — file read/write target + +### Keys used + +| Variable | Hex | +|---|---| +| `internal_key` | `659a500f0f1fce35b6884bdff966576a` | +| `line_protection_key` | `980093b4d77ff65f7476bf9019a80892` | +| `external_auth_key` | `7c3f149ed331b11211d2fb62e2df9637` | +| `line_protection_key_1` | `49439874a1f623fc5e14818365d34699` | +| `external_auth_key_1` | `da152a9a56def40a1386ca258788fea6` | +| `internal_key_1` | `bb4a314981b20ce696d6c1e1cda5820c` | +| `enc_external_auth_key` | `44ea0184094995a845b612522a8ab463` | + +DF names as hex: `626c6f636b54657374` = `blockTest`, `66696c6554657374` = `fileTest` + +--- + +### reset + +``` +hf fmcos select --id 3f00 -k +hf fmcos erase +``` + +--- + +### setup — create both DFs + +#### blockTest DF (3FFF) + +``` +hf fmcos select --id 3f00 -k +hf fmcos create dir --id 3FFF --space 500 --cperm F0 --eperm F0 --appid 94 --name 626c6f636b54657374 -k +hf fmcos select --name 626c6f636b54657374 -k +hf fmcos create keyfile --id 0001 --space 200 --dfsid 94 --perm F0 -k +``` + +Keys written into blockTest (keyfile 0001): + +``` +# Key 0 — lineprotect (line_protection_key_1) +hf fmcos key --op 01 --id 00 --usage F0 --type lineprotect --change 02 --errcount 33 --key 49439874a1f623fc5e14818365d34699 -k + +# Key 1 — extauth (external_auth_key_1) +hf fmcos key --op 01 --id 00 --usage F0 --type extauth --change F0 --followup AA --errcount FF --key da152a9a56def40a1386ca258788fea6 -k + +# Key 2 — internal (internal_key_1) +hf fmcos key --op 01 --id 00 --usage F0 --type internal --change F0 --version 00 --algo 01 --key bb4a314981b20ce696d6c1e1cda5820c -k +``` + +Seed binary file 0x0002 in blockTest: + +``` +hf fmcos create file --id 0002 --type bin --size 50 --rperm F0 --wperm F0 --access FF -k +hf fmcos select --id 0002 -k +hf fmcos write binary --p1 00 --p2 00 --data 62696e66696c655f626c6f636b5f74657374 +``` + +(`62696e66696c655f626c6f636b5f74657374` = ASCII `binfile_block_test`) + +#### fileTest DF (3F01) + +``` +hf fmcos select --id 3f00 -k +hf fmcos create dir --id 3F01 --space 1500 --cperm F0 --eperm F0 --appid 95 --name 66696c6554657374 -k +hf fmcos select --name 66696c6554657374 -k +hf fmcos create keyfile --id 0001 --space 200 --dfsid 95 --perm F0 -k +``` + +Keys written into fileTest (keyfile 0001). Keys 2 and 3 are written with MAC+enc line protection +(`--prot enc --authkey `): + +``` +# Key 0 — extauth (external_auth_key), unprotected write +hf fmcos key --op 01 --id 00 --usage F0 --type extauth --change F0 --followup AA --errcount FF --key 7c3f149ed331b11211d2fb62e2df9637 -k + +# Key 1 — internal (internal_key), unprotected write +hf fmcos key --op 01 --id 00 --usage F0 --type internal --change F0 --version 00 --algo 01 --key 659a500f0f1fce35b6884bdff966576a -k + +# Key 2 — lineprotect (line_protection_key), written with enc protection +hf fmcos key --op 01 --id 00 --usage F0 --type lineprotect --change F0 --errcount FF --key 980093b4d77ff65f7476bf9019a80892 --authkey 7c3f149ed331b11211d2fb62e2df9637 --prot enc -k + +# Key 3 (slot 02) — extauth (enc_external_auth_key), written with enc protection +hf fmcos key --op 01 --id 02 --usage F0 --type extauth --change F0 --followup AA --errcount FF --key 44ea0184094995a845b612522a8ab463 --authkey 7c3f149ed331b11211d2fb62e2df9637 --prot enc -k +``` + +Files created in fileTest. Three trios: unprotected (`--access FF`), MAC (`--access 7F --prot mac`), +MAC+enc (`--access 7F --prot enc`). `--access 7F` = protection required, use key 0. + +``` +# Binary files +hf fmcos create file --id 0002 --type bin --size 50 --rperm F0 --wperm F0 --access FF -k +hf fmcos create file --id 0003 --type bin --size 50 --rperm F0 --wperm F0 --access 7F --prot mac -k +hf fmcos create file --id 0004 --type bin --size 50 --rperm F0 --wperm F0 --access 7F --prot enc -k + +# Variable-length record files +hf fmcos create file --id 0006 --type var --size 50 --rperm F0 --wperm F0 --access FF -k +hf fmcos create file --id 0007 --type var --size 50 --rperm F0 --wperm F0 --access 7F --prot mac -k +hf fmcos create file --id 0008 --type var --size 50 --rperm F0 --wperm F0 --access 7F --prot enc -k + +# Loop (cyclic) files --size 210 = 0x210 = 528 bytes +# space = record_count*(record_len+1)+8; e.g. 5 records × (0x50+1) + 8 = 0x19D +hf fmcos create file --id 000A --type loop --size 210 --rperm F0 --wperm F0 --access FF -k +hf fmcos create file --id 000B --type loop --size 210 --rperm F0 --wperm F0 --access 7F --prot mac -k +hf fmcos create file --id 000C --type loop --size 210 --rperm F0 --wperm F0 --access 7F --prot enc +``` + +--- + +### write_binary + +``` +hf fmcos select --id 3f01 -k +hf fmcos select --id 0002 -k +hf fmcos write binary --p1 00 --p2 00 --data 111213141516171819101a1b1c1d1e1f -k + +hf fmcos select --id 0003 -k +hf fmcos write binary --p1 00 --p2 00 --data 212223242526272829202a2b2c2d2e2f --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k + +hf fmcos select --id 0004 -k +hf fmcos write binary --p1 00 --p2 00 --data 313233343536373839303a3b3c3d3e3f --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### write_loop (APPEND RECORD) + +``` +hf fmcos select --id 3f01 -k +hf fmcos select --id 000a -k +hf fmcos append --fid 0a --data 919293949596979899909a9b9c9d9e9f -k + +hf fmcos select --id 000b -k +hf fmcos append --fid 0b --data a1a2a3a4a5a6a7a8a9a0aaabacadaeaf --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k + +hf fmcos select --id 000c -k +hf fmcos append --fid 0c --data b1b2b3b4b5b6b7b8b9b0babbbcbdbebf --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### write_record (UPDATE RECORD) + +P2 encodes the SFI: `(file_id & 0x1F) << 3 | 4`. The CLI `--fid` takes the raw file ID byte +(1–30) and encodes P2 automatically. + +Variable-length files require `--tlv` so the data is wrapped as `00[len][data]`. + +``` +hf fmcos select --id 3f01 -k +hf fmcos select --id 0006 -k +hf fmcos write record --rec 1 --fid 06 --data 515253545556575859505a5b5c5d5e5f --tlv -k + +hf fmcos select --id 0007 -k +hf fmcos write record --rec 1 --fid 07 --data 616263646566676869606a6b6c6d6e6f --tlv --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k + +hf fmcos select --id 0008 -k +hf fmcos write record --rec 1 --fid 08 --data 717273747576777879707a7b7c7d7e7f --tlv --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### read_binary + +For MAC-protected reads the card appends a 4-byte MAC before the SW; the CLI strips it automatically. + +``` +hf fmcos select --id 3f01 -k +hf fmcos select --id 0002 -k +hf fmcos read binary --p1 00 --p2 00 --len 16 -k + +hf fmcos select --id 0003 -k +hf fmcos read binary --p1 00 --p2 00 --len 16 --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k + +hf fmcos select --id 0004 -k +hf fmcos read binary --p1 00 --p2 00 --len 16 --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### read_record + +Variable-length files need `--tlv`; the CLI requests 2 extra bytes and strips the `00[len]` prefix before printing. + +``` +hf fmcos select --id 3f01 -k +hf fmcos read record --rec 01 --fid 06 --len 16 --tlv -k +hf fmcos read record --rec 01 --fid 07 --len 16 --tlv --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k +hf fmcos read record --rec 01 --fid 08 --len 16 --tlv --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### read_loop + +Loop files use the same READ RECORD command; `--rec 01` reads the most recently appended record. + +``` +hf fmcos select --id 3f01 -k +hf fmcos read record --rec 01 --fid 0a --len 16 -k +hf fmcos read record --rec 01 --fid 0b --len 16 --prot mac --key 980093b4d77ff65f7476bf9019a80892 -k +hf fmcos read record --rec 01 --fid 0c --len 16 --prot enc --key 980093b4d77ff65f7476bf9019a80892 +``` + +--- + +### card_block / app_block + +Permanent application block + +``` +hf fmcos select --name 626c6f636b54657374 -k +hf fmcos block --app --perm --key 49439874a1f623fc5e14818365d34699 +``` + +Temporary application block: + +``` +hf fmcos select --name 626c6f636b54657374 -k +hf fmcos block --app --key 49439874a1f623fc5e14818365d34699 +``` + +--- + +### app_unblock + +After a block the app cannot be selected normally (SELECT returns an error SW), but the DF is still +addressed. Send unblock while the field is still on from the failed select, then re-select: + +``` +hf fmcos select --name 626c6f636b54657374 -k +hf fmcos unblock --key 49439874a1f623fc5e14818365d34699 -k +hf fmcos select --name 626c6f636b54657374 -k +hf fmcos select --id 0002 -k +hf fmcos read binary --p1 00 --p2 00 --len 16 +```