Merge pull request #413 from nieldk/t55write

T55write
This commit is contained in:
GameTec-live
2026-05-08 17:26:51 +02:00
committed by GitHub
6 changed files with 534 additions and 16 deletions
+258
View File
@@ -19,6 +19,8 @@
#include "lf_reader_generic.h"
#include "lf_em4x05_data.h"
#include "rc522.h"
#include "mf1_crapto1.h"
#include "parity.h"
#endif
#include "nfc_14a.h"
/* Forward declarations for functions added to nfc_14a.c/h in this PR.
@@ -1872,6 +1874,261 @@ static data_frame_tx_t *cmd_processor_lf_sniff(uint16_t cmd, uint16_t status, ui
}
return data_frame_make(cmd, STATUS_LF_TAG_OK, (uint16_t)outlen, sniff_buf);
}
/* ========================================================================
* HF14A AUTH TRACE — full anticoll + Crypto1 auth flow, every frame returned.
*
* Performs a complete reader-side auth sequence against a real MIFARE Classic
* tag and packs every wire frame (synthesized anticoll + actual auth) into the
* same trace-buffer format used by `hf 14a sniff`, so the existing host-side
* decoder can render the full exchange.
*
* Buffer format (identical to hf 14a sniff): [bits_be16][data...] per frame.
* Bit15 of the bit-count header: 0 = reader→card, 1 = card→reader.
*
* Request payload: [type:1][block:1][key:6] — 8 bytes total
* type: PICC_AUTHENT1A (0x60) or PICC_AUTHENT1B (0x61)
* block: target block number
* key: 6-byte sector key
*
* Response payload: packed frame buffer, decoded host-side.
* Status: STATUS_HF_TAG_OK if the auth completed (NT, NR||AR, AT all captured),
* STATUS_HF_TAG_NO if no card was found,
* STATUS_MF_ERR_AUTH if AT was wrong or missing (partial trace returned),
* STATUS_PAR_ERR on bad payload length.
* ======================================================================== */
#define HF_AUTH_TRACE_BUF_SIZE 256
static uint8_t m_auth_trace_buf[HF_AUTH_TRACE_BUF_SIZE];
static uint16_t m_auth_trace_len = 0;
/* Append one frame to the trace buffer, mirroring hf14a_sniff_store(). */
static void auth_trace_store(const uint8_t *data, uint16_t szBits, bool is_tx) {
uint16_t szBytes = (szBits + 7) / 8;
if (m_auth_trace_len + 2 + szBytes > HF_AUTH_TRACE_BUF_SIZE) return;
uint16_t hdr = szBits | (is_tx ? 0x8000u : 0x0000u);
m_auth_trace_buf[m_auth_trace_len++] = (hdr >> 8) & 0xFF;
m_auth_trace_buf[m_auth_trace_len++] = hdr & 0xFF;
memcpy(&m_auth_trace_buf[m_auth_trace_len], data, szBytes);
m_auth_trace_len += szBytes;
}
/* Synthesize the anticoll/SELECT/SAK/RATS/ATS frames from a populated tag
* struct. The wire frames during scan_auto are perfectly determined by the
* tag descriptor, so we reconstruct them rather than tap rc522.c.
*
* For 4-byte UID (cascade=1): CL1 with full UID.
* For 7-byte UID (cascade=2): CL1 with CT||UID0..2, CL2 with UID3..6.
* For 10-byte UID (cascade=3): CL1, CL2, CL3.
*
* The "first SAK" for an incomplete UID has bit 2 set (cascade flag); we
* synthesize it as 0x04 (cascade required, no further info), which matches
* what every real cascading tag returns. Final SAK is the captured tag->sak.
*/
static void auth_trace_emit_anticoll(const picc_14a_tag_t *tag) {
/* 1. REQA — 7 bits, reader→card */
uint8_t reqa = PICC_REQIDL;
auth_trace_store(&reqa, 7, false);
/* 2. ATQA — 16 bits, card→reader */
auth_trace_store(tag->atqa, 16, true);
/* 3. Anticoll/SELECT cycles — one per cascade level */
const uint8_t cascade_anticoll[3] = { PICC_ANTICOLL1, PICC_ANTICOLL2, PICC_ANTICOLL3 };
uint8_t uid_pos = 0;
for (uint8_t cl = 0; cl < tag->cascade; cl++) {
bool is_last = (cl == (uint8_t)(tag->cascade - 1));
/* Anticoll request: <SEL> 0x20 — 16 bits, reader→card */
uint8_t anticoll[2] = { cascade_anticoll[cl], 0x20 };
auth_trace_store(anticoll, 16, false);
/* Anticoll response: CT||UID3 + BCC for non-last, or UID4 + BCC for last
* with full UID inline. 5 bytes / 40 bits, card→reader. */
uint8_t resp[5];
if (is_last) {
/* Last level — 4 bytes of UID at the end of tag->uid */
memcpy(resp, &tag->uid[uid_pos], 4);
} else {
/* Cascading — CT (0x88) + 3 bytes UID */
resp[0] = 0x88;
memcpy(&resp[1], &tag->uid[uid_pos], 3);
uid_pos += 3;
}
resp[4] = resp[0] ^ resp[1] ^ resp[2] ^ resp[3]; /* BCC */
auth_trace_store(resp, 40, true);
/* SELECT: <SEL> 0x70 + 5-byte CT/UID/BCC + 2-byte CRC = 9 bytes / 72 bits */
uint8_t sel[9] = { cascade_anticoll[cl], 0x70 };
memcpy(&sel[2], resp, 5);
crc_14a_calculate(sel, 7, &sel[7]);
auth_trace_store(sel, 72, false);
/* SAK: 1 byte SAK + 2-byte CRC = 24 bits, card→reader.
* Intermediate cascades: synthesize SAK=0x04 (cascade bit set, generic).
* Last cascade: real tag->sak. */
uint8_t sak_frame[3];
sak_frame[0] = is_last ? tag->sak : 0x04;
crc_14a_calculate(sak_frame, 1, &sak_frame[1]);
auth_trace_store(sak_frame, 24, true);
}
/* 4. RATS / ATS — only if scan_auto did RATS and the tag responded */
if (tag->ats_len > 0) {
/* RATS request: 0xE0 0x40 + 2-byte CRC = 4 bytes / 32 bits, reader→card.
* 0x40 = FSDI=4 (FSD=48), CID=0 — same as pcd_14a_reader_ats_request(). */
uint8_t rats[4] = { PICC_RATS, 0x40 };
crc_14a_calculate(rats, 2, &rats[2]);
auth_trace_store(rats, 32, false);
/* ATS response: ats_len bytes (CRC was stripped by rc522 layer) +
* recomputed 2-byte CRC, card→reader. */
if ((uint16_t)tag->ats_len + 2 <= 64) {
uint8_t ats_frame[64];
memcpy(ats_frame, tag->ats, tag->ats_len);
crc_14a_calculate(ats_frame, tag->ats_len, &ats_frame[tag->ats_len]);
auth_trace_store(ats_frame, (tag->ats_len + 2) * 8, true);
}
}
}
/* Software-side MIFARE Classic auth tap. Mirrors mf1_toolbox.c::authex() but
* stores every TX/RX frame into the trace buffer instead of just returning a
* status. Fixed reader nonce 12345678 is used (same as authex). */
static uint8_t auth_trace_do_auth(picc_14a_tag_t *tag, uint8_t type, uint8_t blockNo, const uint8_t *key6) {
struct Crypto1State pcs = { 0, 0 };
static const uint8_t nr[4] = { 0x12, 0x34, 0x56, 0x78 }; /* fixed reader nonce */
uint8_t par[8] = { 0 };
uint8_t mf_nr_ar[8] = { 0 };
uint8_t answer[8] = { 0 };
uint8_t parity_resp[8] = { 0 };
uint16_t len = 0;
uint8_t status;
/* AUTH command frame: [type, blockNo, CRC, CRC] — 32 bits, reader→card */
uint8_t auth_cmd[4] = { type, blockNo };
crc_14a_calculate(auth_cmd, 2, &auth_cmd[2]);
auth_trace_store(auth_cmd, 32, false);
/* Send AUTH, expect 4-byte NT (plaintext on first auth). We use the same
* primitive as authex's send_cmd() under the non-encrypted path. */
pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, auth_cmd, 4, answer, &len, U8ARR_BIT_LEN(answer));
if (len != 32) {
return STATUS_HF_ERR_STAT; /* no NT — partial trace already in buffer */
}
/* Capture NT — 32 bits, card→reader */
auth_trace_store(answer, 32, true);
uint32_t nt = ((uint32_t)answer[0] << 24) | ((uint32_t)answer[1] << 16)
| ((uint32_t)answer[2] << 8) | (uint32_t)answer[3];
/* Initialise Crypto1 with key (LSB-first, MIFARE convention) */
uint64_t ui64Key = 0;
for (int i = 0; i < 6; i++) ui64Key |= (uint64_t)key6[i] << ((5 - i) * 8);
crypto1_init(&pcs, ui64Key);
/* First auth: feed (nt ^ uid) as plaintext into the cipher */
uint32_t uid32 = get_u32_tag_uid(tag);
crypto1_word(&pcs, nt ^ uid32, 0);
/* Encrypt NR + parity */
for (int pos = 0; pos < 4; pos++) {
mf_nr_ar[pos] = crypto1_byte(&pcs, nr[pos], 0) ^ nr[pos];
par[pos] = filter(pcs.odd) ^ oddparity8(nr[pos]);
}
/* Compute AR = prng_successor(nt, 32) and encrypt byte-by-byte */
uint32_t nt_succ = prng_successor(nt, 32);
for (int pos = 4; pos < 8; pos++) {
nt_succ = prng_successor(nt_succ, 8);
mf_nr_ar[pos] = crypto1_byte(&pcs, 0x00, 0) ^ (nt_succ & 0xFF);
par[pos] = filter(pcs.odd) ^ oddparity8(nt_succ & 0xFF);
}
/* Capture NR||AR (encrypted) — 64 bits, reader→card. Stored before the
* actual transfer so we still have the trace if the transfer fails. */
auth_trace_store(mf_nr_ar, 64, false);
/* Send NR||AR with manual parity, expect 4-byte AT */
pcd_14a_reader_bits_transfer(mf_nr_ar, 64, par, answer, parity_resp, &len, U8ARR_BIT_LEN(answer));
if (len != 32) {
return STATUS_MF_ERR_AUTH; /* no AT — auth was rejected, partial trace */
}
/* Capture AT — 32 bits, card→reader (still encrypted on the wire) */
auth_trace_store(answer, 32, true);
/* Verify AT == prng_successor(nt, 64) ^ ks3 */
uint32_t at_recv = ((uint32_t)answer[0] << 24) | ((uint32_t)answer[1] << 16)
| ((uint32_t)answer[2] << 8) | (uint32_t)answer[3];
uint32_t ntpp = prng_successor(nt_succ, 32) ^ crypto1_word(&pcs, 0, 0);
status = (ntpp == at_recv) ? STATUS_HF_TAG_OK : STATUS_MF_ERR_AUTH;
/* Auth state may be left armed in the RC522 — clear it for safety so
* subsequent reader operations start clean. */
pcd_14a_reader_mf1_unauth();
return status;
}
static data_frame_tx_t *cmd_processor_hf14a_auth_trace(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
/* Payload: [type:1][block:1][key:6][timeout_ms_be16:2] = 10 bytes.
* Backward-compatible short form [type:1][block:1][key:6] = 8 bytes still
* accepted; default 5000ms. */
uint32_t timeout_ms = 5000;
if (length == 10) {
timeout_ms = ((uint32_t)data[8] << 8) | data[9];
if (timeout_ms == 0 || timeout_ms > 30000) timeout_ms = 5000;
} else if (length != 8) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
}
uint8_t type = data[0];
uint8_t block = data[1];
uint8_t *key = &data[2];
if (type != PICC_AUTHENT1A && type != PICC_AUTHENT1B) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
}
m_auth_trace_len = 0;
/* Step 1: poll for a tag in the field until present or timeout.
* scan_auto internally tries twice with REQA; if no card is present
* each attempt fails fast (~3-5 ms) and we back off briefly between
* iterations to keep the WDT happy and avoid hammering the RC522.
* The antenna is already on (before_hf_reader_run hook) and stays on
* for the whole polling window — same behaviour as a desktop reader. */
picc_14a_tag_t tag;
uint8_t scan_status = STATUS_HF_TAG_NO;
autotimer *p_at = bsp_obtain_timer(0);
while (NO_TIMEOUT_1MS(p_at, timeout_ms)) {
scan_status = pcd_14a_reader_scan_auto(&tag);
if (scan_status == STATUS_HF_TAG_OK) {
break;
}
/* 50 ms back-off between attempts — yields to USB/BLE main loop
* and keeps the WDT fed (timeout is 5000 ms). */
for (int i = 0; i < 50; i++) {
bsp_delay_ms(1);
bsp_wdt_feed();
}
}
bsp_return_timer(p_at);
if (scan_status != STATUS_HF_TAG_OK) {
return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL);
}
/* Step 2: synthesize the wire frames that scan_auto just produced */
auth_trace_emit_anticoll(&tag);
/* Step 3: perform the auth, tapping every TX/RX into the same buffer */
uint8_t auth_status = auth_trace_do_auth(&tag, type, block, key);
/* Always return the buffer — even on auth failure, the partial trace is
* useful for diagnosing why (wrong key vs. wrong block vs. no NT etc.). */
if (m_auth_trace_len == 0) {
return data_frame_make(cmd, auth_status, 0, NULL);
}
return data_frame_make(cmd, auth_status, m_auth_trace_len, m_auth_trace_buf);
}
#define HF_SNIFF_BUF_SIZE 3800 /* leave room for USB framing */
#define HF_SNIFF_MAX_FRAMES 200
@@ -2676,6 +2933,7 @@ static cmd_data_map_t m_data_cmd_map[] = {
{ DATA_CMD_EM4X05_SCAN, before_reader_run, cmd_processor_em4x05_scan, NULL },
{ DATA_CMD_LF_SNIFF, before_reader_run, cmd_processor_lf_sniff, NULL },
{ DATA_CMD_HF14A_SNIFF, NULL, cmd_processor_hf14a_sniff, NULL },
{ DATA_CMD_HF14A_AUTH_TRACE, before_hf_reader_run, cmd_processor_hf14a_auth_trace, after_hf_reader_run },
#endif
+1
View File
@@ -70,6 +70,7 @@
#define DATA_CMD_MF1_WRITE_ONE_BLOCK (2009)
#define DATA_CMD_HF14A_RAW (2010)
#define DATA_CMD_HF14A_SCAN_KEEP (2016) /* scan+RATS, keep field alive for APDU exchange */
#define DATA_CMD_HF14A_AUTH_TRACE (2017) /* full anticoll + Crypto1 auth, every frame returned for inspection */
#define DATA_CMD_MF1_MANIPULATE_VALUE_BLOCK (2011)
#define DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS (2012)
#define DATA_CMD_MF1_HARDNESTED_ACQUIRE (2013)
@@ -24,7 +24,6 @@
NRF_LOG_MODULE_REGISTER();
#define ANT_NO_MOD() nrf_gpio_pin_clear(LF_MOD)
#define LF_125KHZ_BROADCAST_MAX (10)
// Whether the USB light effect is allowed to enable
extern bool g_usb_led_marquee_enable;
@@ -43,7 +42,8 @@ static void lf_field_lost(void) {
g_is_tag_emulating = false; // Reset the flag in the emulation
m_is_lf_emulating = false;
TAG_FIELD_LED_OFF() // Make sure the indicator light of the LF field status
NRF_LPCOMP->INTENSET = LPCOMP_INTENCLR_CROSS_Msk | LPCOMP_INTENCLR_UP_Msk | LPCOMP_INTENCLR_DOWN_Msk | LPCOMP_INTENCLR_READY_Msk;
// Re-arm LPCOMP so the next field appearance triggers lpcomp_event_handler.
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
// call sleep_timer_start *after* unsetting g_is_tag_emulating
sleep_timer_start(SLEEP_DELAY_MS_FIELD_125KHZ_LOST); // Start the timer to enter the sleep
NRF_LOG_INFO("LF FIELD LOST");
@@ -68,12 +68,15 @@ bool is_lf_field_exists(void) {
* priority is set to APP_IRQ_PRIORITY_HIGH).
*/
static void lpcomp_event_handler(nrf_lpcomp_event_t event) {
// Only when the lf -frequency emulation is not launched, and the analog card is started
// Only when the lf-frequency emulation is not launched, and the analog card is started
if (m_is_lf_emulating || event != NRF_LPCOMP_EVENT_UP) {
return;
}
sleep_timer_stop(); // turn off dormant delay
// Disable LPCOMP during emulation — LF_RSSI fluctuates during load
// modulation and would trigger spurious DOWN events with DETECT_CROSS.
// Field-loss is checked periodically via EVT_END_SEQ0 in pwm_handler.
nrfx_lpcomp_disable();
// set the emulation status logo bit
@@ -86,8 +89,9 @@ static void lpcomp_event_handler(nrf_lpcomp_event_t event) {
set_slot_light_color(RGB_BLUE);
TAG_FIELD_LED_ON()
// use precise hardware timer to broadcast card id
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, LF_125KHZ_BROADCAST_MAX, NRFX_PWM_FLAG_STOP);
// Loop continuously — no stop/restart gaps between sequence plays.
// Field-loss is detected in pwm_handler via EVT_END_SEQ0.
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, 1, NRFX_PWM_FLAG_LOOP);
NRF_LOG_INFO("LF FIELD DETECTED");
}
@@ -104,21 +108,23 @@ static void lpcomp_init(void) {
}
static void pwm_handler(nrfx_pwm_evt_type_t event_type) {
if (event_type == NRFX_PWM_EVT_END_SEQ0) {
// Fired at end of each loop iteration — check field without stopping PWM.
// Mask UP interrupt while sampling to prevent re-entrancy.
NRF_LPCOMP->INTENCLR = LPCOMP_INTENCLR_UP_Msk;
if (!is_lf_field_exists()) {
// Field gone — stop the loop; pwm_handler will get EVT_STOPPED next.
nrfx_pwm_stop(&m_broadcast, false);
}
// Re-enable will happen either in lf_field_lost (via INTENSET) or stays
// suppressed while PWM keeps looping (we only need it after field_lost).
return;
}
if (event_type != NRFX_PWM_EVT_STOPPED) {
return;
}
// after last broadcast, force NO_MOD on antenna to measure field.
ANT_NO_MOD();
bsp_delay_ms(1);
// We don't need any events, but only need to detect the state of the field
NRF_LPCOMP->INTENCLR = LPCOMP_INTENCLR_CROSS_Msk | LPCOMP_INTENCLR_UP_Msk | LPCOMP_INTENCLR_DOWN_Msk | LPCOMP_INTENCLR_READY_Msk;
if (is_lf_field_exists()) {
nrfx_lpcomp_disable();
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, LF_125KHZ_BROADCAST_MAX, NRFX_PWM_FLAG_STOP);
} else {
lf_field_lost();
}
lf_field_lost();
}
static void pwm_init(void) {
+219
View File
@@ -7560,6 +7560,225 @@ class HF14ASniff(BaseCLIUnit):
@hf_14a.command("auth-trace")
class HF14AAuthTrace(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.description = (
"Run a full reader-side ISO14443A + MIFARE Classic Crypto1 auth "
"against a real card and print every wire frame: REQA → ATQA → "
"anticoll/SELECT → SAK → (RATS/ATS) → AUTH(0x60/0x61) → NT → "
"NR||AR (enc) → AT (enc), with host-side Crypto1 decryption of "
"the auth sub-frames for verification."
)
parser.add_argument(
"--blk", "--block", type=int, required=True, metavar="<dec>",
help="Target block number"
)
keytype_group = parser.add_mutually_exclusive_group()
keytype_group.add_argument("-a", "-A", action="store_true", help="Use Key A (default)")
keytype_group.add_argument("-b", "-B", action="store_true", help="Use Key B")
parser.add_argument(
"-k", "--key", type=str, required=True, metavar="<hex>",
help="6-byte sector key (12 hex chars)"
)
parser.add_argument(
"-t", "--timeout", type=int, default=5000, metavar="<ms>",
help="Tag-presence polling timeout in ms (1-30000, default 5000)"
)
parser.epilog = """
examples:
hf 14a auth-trace --blk 0 -k FFFFFFFFFFFF
hf 14a auth-trace --blk 4 -b -k A0A1A2A3A4A5
hf 14a auth-trace --blk 0 -k FFFFFFFFFFFF -t 10000 # wait up to 10s for tag
"""
return parser
def on_exec(self, args: argparse.Namespace):
# Validate key
key_hex = args.key.replace(" ", "")
if not re.match(r"^[0-9a-fA-F]{12}$", key_hex):
print(f" [!] {color_string((CR, 'Key must be exactly 12 hex characters'))}")
return
key_bytes = bytes.fromhex(key_hex)
key_type = 0x61 if args.b else 0x60
block = args.blk
timeout_ms = max(1, min(30000, int(args.timeout)))
print(f" Running auth trace: block={block} keyType={'B' if args.b else 'A'} key={key_hex.upper()}")
print(f" Waiting up to {timeout_ms} ms for a MIFARE Classic card... "
f"({CY}place CU on a card now{C0})")
print()
try:
resp = self.cmd.hf14a_auth_trace(block, key_type, key_bytes, timeout_ms=timeout_ms)
except Exception as e:
if 'CMDInvalid' in type(e).__name__ or '2017' in str(e):
print(f"{CR}Command not supported — reflash firmware to enable hf 14a auth-trace{C0}")
else:
print(f"{CR}{e}{C0}")
return
# Status legend:
# HF_TAG_OK — auth succeeded, full trace
# HF_TAG_NO — no card / scan failed
# MF_ERR_AUTH — auth failed (wrong key / wrong block), partial trace returned
# HF_ERR_STAT — no NT received, partial trace returned
if resp.status == Status.HF_TAG_NO:
print(f"{CR} No 14443A tag in field — auth aborted{C0}")
return
if not resp.data:
print(f"{CR} No frames returned (status={Status(resp.status).name}){C0}")
return
# Parse packed frame buffer — same format as hf 14a sniff.
# [bits_be16][data...]; bit15 of bits = direction (1 = card→reader).
buf = bytes(resp.data)
frames = [] # (szBits, data, is_tx)
i = 0
while i + 2 <= len(buf):
hdr = (buf[i] << 8) | buf[i + 1]
i += 2
is_tx = bool(hdr & 0x8000)
szBits = hdr & 0x7FFF
if szBits == 0:
break
szBytes = (szBits + 7) // 8
if i + szBytes > len(buf):
break
raw = buf[i:i + szBytes]
i += szBytes
# Auth-trace stores parity-stripped data (firmware byte/bits
# transfer primitives strip parity automatically), so szBits is
# always a multiple of 8 — no unwrap needed.
frames.append((szBits, raw, is_tx))
if not frames:
print(f"{CR}No frames decoded{C0}")
return
# Header
rx_count = sum(1 for _, _, tx in frames if not tx)
tx_count = sum(1 for _, _, tx in frames if tx)
status_label = {
Status.HF_TAG_OK: f"{CG}auth OK{C0}",
Status.MF_ERR_AUTH: f"{CR}auth FAILED{C0}",
Status.HF_ERR_STAT: f"{CR}no NT received{C0}",
}.get(Status(resp.status), f"{CY}status={Status(resp.status).name}{C0}")
print(f" Captured : {CG}{len(frames)}{C0} frame(s) "
f"({CY}{rx_count}{C0} reader→card {CG}{tx_count}{C0} card→reader) "
f"{status_label}")
print()
print(f" {'#':>3} {'dir':<3} {'bits':>4} {'hex data':<42} decoded")
print(f" {'---':>3} {'---':<3} {'----':>4} {'-' * 42} {'-' * 35}")
# Auth-state tracker — annotate AUTH cmd, NT, NR||AR, AT specifically.
auth_state = 'idle' # idle → cmd_seen → nt_seen → nr_ar_seen → done
last_auth_keytype = None
last_auth_block = None
nt_int = None
nr_ar_enc = None
at_enc = None
uid_bytes = b''
for n, (szBits, data, is_tx) in enumerate(frames):
hex_str = ' '.join(f'{b:02x}' for b in data)
decoded_ctx = None
col_ctx = None
# Capture UID from the first anticoll RX (CL1 response): 5 bytes.
# If first byte is 0x88 it's a cascading 7-byte UID — second segment
# gives bytes 3..6. For 4-byte UID, all four bytes are here.
if is_tx and szBits == 40 and len(data) == 5 and not uid_bytes:
if data[0] == 0x88:
uid_bytes = data[1:4] # CT|UID0|UID1|UID2|BCC
else:
uid_bytes = data[0:4]
elif is_tx and szBits == 40 and len(data) == 5 and len(uid_bytes) == 3:
uid_bytes = uid_bytes + data[0:4] # 7-byte UID complete
# AUTH cmd: 0x60/0x61 + block + 2 CRC bytes, reader→card, 32 bits
if (not is_tx) and szBits == 32 and len(data) == 4 and data[0] in (0x60, 0x61):
last_auth_keytype = 'A' if data[0] == 0x60 else 'B'
last_auth_block = data[1]
auth_state = 'cmd_seen'
decoded_ctx = f"AUTH Key{last_auth_keytype} block=0x{last_auth_block:02X} ({last_auth_block}) +CRC"
col_ctx = CG
# NT: 4 bytes, card→reader, immediately after AUTH cmd
elif is_tx and auth_state == 'cmd_seen' and szBits == 32 and len(data) == 4:
nt_int = int.from_bytes(data, 'big')
auth_state = 'nt_seen'
decoded_ctx = f"NT (card nonce, plaintext) = {data.hex().upper()}"
col_ctx = CG
# NR||AR encrypted: 8 bytes, reader→card, after NT
elif (not is_tx) and auth_state == 'nt_seen' and szBits == 64 and len(data) == 8:
nr_ar_enc = bytes(data)
auth_state = 'nr_ar_seen'
decoded_ctx = f"NR||AR (enc) NR={data[:4].hex().upper()} AR={data[4:].hex().upper()}"
col_ctx = CG
# AT encrypted: 4 bytes, card→reader, after NR||AR
elif is_tx and auth_state == 'nr_ar_seen' and szBits == 32 and len(data) == 4:
at_enc = bytes(data)
auth_state = 'done'
decoded_ctx = f"AT (enc) = {data.hex().upper()}"
col_ctx = CG
decoded, col = _decode_14a_frame_col(data, szBits)
if decoded_ctx is not None:
decoded, col = decoded_ctx, col_ctx
dir_str = f'{CG}<<<{C0}' if is_tx else f'{CY}>>>{C0}'
print(f" {CY}{n + 1:>3}{C0} {dir_str} {szBits:>4} {hex_str:<42} {col}{decoded}{C0}")
# Crypto1 verification block — if we have NT + NR||AR, decrypt AR/AT
# and confirm they match prng_successor(NT, 32) / prng_successor(NT, 64).
print()
if nt_int is not None and nr_ar_enc is not None and len(uid_bytes) >= 4:
uid32 = int.from_bytes(uid_bytes[-4:], 'big') # last 4 bytes for cascade≥2
print(f" {CC}Crypto1 analysis:{C0}")
print(f" UID (low 4 bytes) : {uid_bytes[-4:].hex().upper()}")
print(f" NT (plaintext) : {nt_int:08X}")
print(f" NR (fixed in fw) : 12345678 (encrypted on wire: {nr_ar_enc[:4].hex().upper()})")
ar_expected = Crypto1.prng_next(nt_int, 32)
at_expected = Crypto1.prng_next(nt_int, 64)
print(f" AR expected : {ar_expected:08X} = prng_successor(NT, 32)")
print(f" AR (encrypted) : {nr_ar_enc[4:].hex().upper()}")
if at_enc is not None:
# Re-run Crypto1 forward to recover ks2 (keystream over AT).
state = Crypto1()
state.key = key_hex
state.lfsr48_u32(uid32 ^ nt_int, False) # ks0
state.lfsr48_u32(int.from_bytes(nr_ar_enc[:4], 'big'), True) # ks1
state.lfsr48_u32(0, False) # consume ks2 (AR keystream)
ks_at = state.lfsr48_u32(0, False) # ks3 (AT keystream)
at_int = int.from_bytes(at_enc, 'big')
at_decoded = at_int ^ ks_at
ok = (at_decoded == at_expected)
colour = CG if ok else CR
mark = '' if ok else ''
print(f" AT expected : {at_expected:08X} = prng_successor(NT, 64)")
print(f" AT (encrypted) : {at_enc.hex().upper()}")
print(f" AT decrypted : {colour}{at_decoded:08X}{C0} {colour}{mark} "
f"{'MATCH — auth verified' if ok else 'MISMATCH — wrong key or replay'}{C0}")
# Cross-check against mfkey32 prediction too.
nr_enc_int = int.from_bytes(nr_ar_enc[:4], 'big')
ar_enc_int = int.from_bytes(nr_ar_enc[4:], 'big')
key_match = Crypto1.mfkey32_is_reader_has_key(uid32, nt_int, nr_enc_int, ar_enc_int, key_hex)
if key_match:
print(f" mfkey32 forward : {CG}key {key_hex.upper()} verified against NT/NR/AR{C0}")
else:
print(f" {CR}AT not received — auth was rejected by the card{C0}")
elif nt_int is not None:
print(f" {CY}Auth aborted before NR||AR — NT={nt_int:08X}, no further analysis{C0}")
def _decode_sw(sw1: int, sw2: int) -> str:
"""Decode an ISO 7816-4 status word pair."""
+33
View File
@@ -535,6 +535,39 @@ class ChameleonCMD:
timeout_s = (timeout_ms // 1000) + 5
return self.device.send_cmd_sync(Command.HF14A_SNIFF, payload, timeout=timeout_s)
def hf14a_auth_trace(self, block: int, key_type: int, key: bytes, timeout_ms: int = 5000):
"""
Run a full reader-side ISO14443A + MIFARE Classic Crypto1 auth flow
against a real card and return every wire frame for inspection.
The firmware polls for a tag in the field for up to `timeout_ms`
milliseconds, then performs anticoll + SELECT + (optional RATS) +
AUTH and packs all frames synthesized anticoll plus the live
AUTH/NT/NR||AR/AT into the same buffer format used by hf14a_sniff:
[2 bytes: bit count, big-endian] [N bytes: frame data, ceil(bits/8)] ...
Bit 15 of the bit-count header: 0 = readercard, 1 = cardreader.
:param block: target block number (0-255)
:param key_type: 0x60 (Key A) or 0x61 (Key B)
:param key: 6-byte sector key
:param timeout_ms: tag-presence polling timeout in ms (1-30000)
:return: Raw response check .status and .data
"""
if key_type not in (0x60, 0x61):
raise ValueError("key_type must be 0x60 (Key A) or 0x61 (Key B)")
if len(key) != 6:
raise ValueError("key must be exactly 6 bytes")
timeout_ms = max(1, min(30000, int(timeout_ms)))
payload = (
bytes([key_type, block & 0xFF])
+ bytes(key)
+ bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF])
)
# Add a couple of seconds of slack on top of the device-side polling
# window so the USB/BLE round-trip doesn't time out before firmware
# gives up on its own.
return self.device.send_cmd_sync(Command.HF14A_AUTH_TRACE, payload, timeout=(timeout_ms // 1000) + 3)
@expect_response(Status.SUCCESS)
def hf14a_get_config(self):
"""
+1
View File
@@ -72,6 +72,7 @@ class Command(enum.IntEnum):
MF1_WRITE_ONE_BLOCK = 2009
HF14A_RAW = 2010
HF14A_SCAN_KEEP = 2016
HF14A_AUTH_TRACE = 2017
MF1_MANIPULATE_VALUE_BLOCK = 2011
MF1_CHECK_KEYS_OF_SECTORS = 2012
MF1_HARDNESTED_ACQUIRE = 2013