From efa2ea2c7bf1d619d42d1efd2dbf6b9ac1eb982e Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 4 Apr 2026 08:58:13 +0200 Subject: [PATCH 01/17] protocol ISO 14443-4 and emv scan, loading json file from PM3rdv4 --- firmware/application/Makefile | 1 + firmware/application/src/app_cmd.c | 416 ++++++++++++++ firmware/application/src/data_cmd.h | 9 + .../application/src/rfid/nfctag/hf/nfc_14a.c | 13 +- .../application/src/rfid/nfctag/hf/nfc_14a.h | 2 +- .../src/rfid/nfctag/hf/nfc_14a_4.c | 446 +++++++++++++++ .../src/rfid/nfctag/hf/nfc_14a_4.h | 71 +++ .../src/rfid/nfctag/tag_base_type.h | 3 +- .../src/rfid/nfctag/tag_emulation.c | 3 + .../application/src/rfid/reader/hf/rc522.c | 2 +- software/script/chameleon_cli_unit.py | 529 ++++++++++++++++++ software/script/chameleon_cmd.py | 95 ++++ software/script/chameleon_enum.py | 12 +- 13 files changed, 1593 insertions(+), 9 deletions(-) create mode 100644 firmware/application/src/rfid/nfctag/hf/nfc_14a_4.c create mode 100644 firmware/application/src/rfid/nfctag/hf/nfc_14a_4.h diff --git a/firmware/application/Makefile b/firmware/application/Makefile index 74531a5..2a5a743 100644 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -28,6 +28,7 @@ SRC_FILES += \ $(PROJ_DIR)/rfid/nfctag/tag_persistence.c \ $(PROJ_DIR)/rfid/nfctag/hf/crypto1_helper.c \ $(PROJ_DIR)/rfid/nfctag/hf/nfc_14a.c \ + $(PROJ_DIR)/rfid/nfctag/hf/nfc_14a_4.c \ $(PROJ_DIR)/rfid/nfctag/hf/nfc_mf1.c \ $(PROJ_DIR)/rfid/nfctag/hf/nfc_mf0_ntag.c \ $(PROJ_DIR)/rfid/nfctag/lf/lf_tag_em.c \ diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index c020a3a..309dae5 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -20,6 +20,8 @@ #include "lf_em4x05_data.h" #endif #include "nfc_14a.h" +#include "nfc_14a_4.h" +#include "rc522.h" #define NRF_LOG_MODULE_NAME app_cmd @@ -1139,6 +1141,9 @@ static nfc_tag_14a_coll_res_reference_t *get_coll_res_data(bool write) { case TAG_TYPE_NTAG_216: info = nfc_tag_mf0_ntag_get_coll_res(); break; + case TAG_TYPE_HF14A_4: + info = nfc_tag_14a_4_get_coll_res(); + break; default: // no collision resolution data for slot info = NULL; @@ -1889,6 +1894,407 @@ static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, #endif + +/* ======================================================================== + * HF14A-4 ISO14443-4 T=CL emulation commands (6000-range) + * ======================================================================== */ + +/** + * HF14A-4 APDU recv — non-blocking poll. + * Returns STATUS_SUCCESS + APDU bytes if one is pending, STATUS_HF_TAG_NO otherwise. + */ +static data_frame_tx_t *cmd_processor_hf14a_4_apdu_recv(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + static uint8_t apdu_buf[NFC_14A_4_MAX_APDU]; + uint16_t apdu_len = 0; + extern bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length); + if (nfc_tag_14a_4_get_pending_apdu(apdu_buf, &apdu_len)) { + return data_frame_make(cmd, STATUS_SUCCESS, apdu_len, apdu_buf); + } + return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL); +} + +/** + * HF14A-4 APDU send — push a response for the next WTX-waiting I-block. + * payload: len_be16(2) + resp_bytes + */ +static data_frame_tx_t *cmd_processor_hf14a_4_apdu_send(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + extern void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length); + if (length < 2) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + uint16_t resp_len = ((uint16_t)data[0] << 8) | data[1]; + if (resp_len > NFC_14A_4_MAX_APDU || length < 2 + resp_len) + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + nfc_tag_14a_4_set_response(&data[2], resp_len); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); +} + +/** + * HF14A-4 set anti-collision data (UID/ATQA/SAK/ATS). + * payload: uid_len(1) uid(n) atqa(2) sak(1) ats_len(1) ats(m) + */ +static data_frame_tx_t *cmd_processor_hf14a_4_set_anti_coll(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length < 1 || !is_valid_uid_size(data[0]) || + length < 1 + data[0] + 2 + 1 + 1 || + length < 1 + data[0] + 2 + 1 + 1 + data[1 + data[0] + 2 + 1]) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + nfc_tag_14a_coll_res_reference_t *info = get_coll_res_data(true); + if (info == NULL) return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL); + uint16_t offset = 0; + *(info->size) = (nfc_tag_14a_uid_size)data[offset]; offset++; + memcpy(info->uid, &data[offset], *(info->size)); offset += *(info->size); + memcpy(info->atqa, &data[offset], 2); offset += 2; + info->sak[0] = data[offset]; offset++; + info->ats->length = data[offset]; offset++; + memcpy(info->ats->data, &data[offset], info->ats->length); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); +} + +/** + * HF14A-4 add static APDU response pair (pre-load before hw mode -e). + * payload: cmd_len(1) cmd(n) resp_len(1) resp(m) + * If cmd_len==0, clears all static responses. + */ +static data_frame_tx_t *cmd_processor_hf14a_4_static_resp(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length == 0) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + uint8_t cmd_len = data[0]; + if (cmd_len == 0) { + nfc_tag_14a_4_clear_static_responses(); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); + } + if (length < (uint16_t)(1 + cmd_len + 2)) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + /* resp_len is 2 bytes big-endian to support responses > 255 bytes */ + uint16_t resp_len = ((uint16_t)data[1 + cmd_len] << 8) | data[2 + cmd_len]; + if (length < (uint16_t)(1 + cmd_len + 2 + resp_len)) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + nfc_tag_14a_4_add_static_response(&data[1], cmd_len, &data[3 + cmd_len], (uint8_t)resp_len); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); +} + +/** + * HF14A scan keeping field alive after completion — identical to hf14a_scan + * but registered without after_hf_reader_run so the field stays on and the + * card remains in T=CL state for subsequent hf14a_raw APDU calls. + */ +static data_frame_tx_t *cmd_processor_hf14a_scan_keep(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + picc_14a_tag_t taginfo; + status = pcd_14a_reader_scan_auto(&taginfo); + if (status != STATUS_HF_TAG_OK) { + return data_frame_make(cmd, status, 0, NULL); + } + uint8_t payload[1 + sizeof(taginfo.uid) + sizeof(taginfo.atqa) + sizeof(taginfo.sak) + 1 + 254]; + uint16_t offset = 0; + payload[offset++] = taginfo.uid_len; + memcpy(&payload[offset], taginfo.uid, taginfo.uid_len); offset += taginfo.uid_len; + memcpy(&payload[offset], taginfo.atqa, sizeof(taginfo.atqa)); offset += sizeof(taginfo.atqa); + payload[offset++] = taginfo.sak; + payload[offset++] = taginfo.ats_len; + memcpy(&payload[offset], taginfo.ats, taginfo.ats_len); offset += taginfo.ats_len; + return data_frame_make(cmd, STATUS_HF_TAG_OK, offset, payload); +} + + +/** + * HF14A-4 reader APDU — activate field, select card (with RATS), send one + * ISO14443-4 T=CL APDU, return the response, keep field alive. + * + * This performs the full select+RATS+APDU sequence in a single firmware call, + * avoiding the USB round-trip gap that would cause the card to lose power. + * + * payload: apdu_bytes (raw APDU, no PCB wrapping needed — added here) + * returns: raw APDU response bytes (PCB stripped) + */ +static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length == 0 || length > 61) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + + uint8_t resp_buf[64]; /* RC522 FIFO per chain block */ + uint8_t resp_chain[512]; /* reassembled chained response */ + uint16_t resp_chain_len = 0; + uint16_t resp_bits = 0; + + /* Step 1: cycle field briefly to return card to IDLE state, then + * do full select + RATS via scan_auto. This is needed because the card + * may be in T=CL active state from a previous APDU exchange and won't + * respond to REQA/WUPA until powered off. */ + pcd_14a_reader_antenna_off(); + bsp_delay_ms(5); + pcd_14a_reader_reset(); + pcd_14a_reader_antenna_on(); + bsp_delay_ms(8); + + pcd_14a_reader_timeout_set(200); + picc_14a_tag_t taginfo; + status = pcd_14a_reader_scan_auto(&taginfo); + if (status != STATUS_HF_TAG_OK) { + pcd_14a_reader_timeout_set(DEF_COM_TIMEOUT); + uint8_t dbg[2] = {0x01, status}; + return data_frame_make(cmd, STATUS_HF_TAG_NO, 2, dbg); + } + NRF_LOG_INFO("14A4_READER_APDU: scan_auto OK sak=%02x ats_len=%d", + taginfo.sak, taginfo.ats_len); + + /* Step 3: wrap APDU in I-block (PCB=0x02) and send */ + uint8_t frame_buf[64]; + frame_buf[0] = 0x02; /* PCB: I-block, block_num=0, no CID, no NAD */ + memcpy(&frame_buf[1], data, length); + crc_14a_append(frame_buf, length + 1); + uint8_t frame_len = length + 1 + 2; + + NRF_LOG_INFO("14A4_READER_APDU: sending I-block, frame_len=%d", frame_len); + + pcd_14a_reader_timeout_set(600); + resp_bits = 0; + status = pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, + frame_buf, frame_len, resp_buf, &resp_bits, U8ARR_BIT_LEN(resp_buf)); + pcd_14a_reader_timeout_set(DEF_COM_TIMEOUT); + + NRF_LOG_INFO("14A4_READER_APDU: APDU transfer status=%d resp_bits=%d", status, resp_bits); + + if (status != STATUS_HF_TAG_OK || resp_bits < 8) { + uint8_t dbg[6] = {0x03, status, (uint8_t)frame_len, + frame_buf[0], frame_buf[1], frame_buf[2]}; + return data_frame_make(cmd, STATUS_HF_TAG_NO, 6, dbg); + } + + uint8_t resp_bytes = resp_bits / 8; + if (resp_bytes < 3) { + uint8_t dbg[2] = {0x04, resp_bytes}; + return data_frame_make(cmd, STATUS_HF_ERR_CRC, 2, dbg); + } + + /* Verify first block CRC and begin chaining reassembly */ + uint8_t crc_calc[2]; + crc_14a_calculate(resp_buf, resp_bytes - 2, crc_calc); + if (resp_buf[resp_bytes-2] != crc_calc[0] || resp_buf[resp_bytes-1] != crc_calc[1]) { + return data_frame_make(cmd, STATUS_HF_ERR_CRC, resp_bytes, resp_buf); + } + + /* Copy data portion (strip PCB + CRC), then handle chaining */ + uint8_t blk_num = 0; + uint8_t resp_pcb = resp_buf[0]; + uint8_t dlen = resp_bytes - 3; /* subtract PCB(1) + CRC(2) */ + if (dlen > 0 && resp_chain_len + dlen < sizeof(resp_chain)) { + memcpy(&resp_chain[resp_chain_len], &resp_buf[1], dlen); + resp_chain_len += dlen; + } + blk_num ^= 1; + + /* ISO14443-4 chaining: PCB bit5 (0x20) set means more blocks follow */ + while (resp_pcb & 0x20) { + uint8_t rack = 0xA2 | (blk_num & 0x01); /* R(ACK) */ + uint8_t rack_frame[3]; + rack_frame[0] = rack; + crc_14a_append(rack_frame, 1); + resp_bits = 0; + pcd_14a_reader_timeout_set(600); + status = pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, + rack_frame, 3, resp_buf, &resp_bits, U8ARR_BIT_LEN(resp_buf)); + pcd_14a_reader_timeout_set(DEF_COM_TIMEOUT); + if (status != STATUS_HF_TAG_OK || resp_bits < 24) break; + resp_bytes = resp_bits / 8; + crc_14a_calculate(resp_buf, resp_bytes - 2, crc_calc); + if (resp_buf[resp_bytes-2] != crc_calc[0] || resp_buf[resp_bytes-1] != crc_calc[1]) break; + resp_pcb = resp_buf[0]; + dlen = resp_bytes - 3; + if (dlen > 0 && resp_chain_len + dlen < sizeof(resp_chain)) { + memcpy(&resp_chain[resp_chain_len], &resp_buf[1], dlen); + resp_chain_len += dlen; + } + blk_num ^= 1; + } + + return data_frame_make(cmd, STATUS_HF_TAG_OK, resp_chain_len, resp_chain); +} + +/** + * HF14A-4 EMV scan — complete EMV card read in a single firmware call. + * + * Performs: field cycle → scan_auto (select+RATS) → PPSE → SELECT AID → + * GPO → READ RECORDs, all without returning to the host between APDUs. + * + * Response format (packed, little-endian lengths): + * tag_info: uid_len(1) uid(n) atqa(2) sak(1) ats_len(1) ats(m) + * num_apdus(1) + * for each APDU pair: + * cmd_len(1) cmd(n) resp_len(2 LE) resp(m) + * + * Returns STATUS_HF_TAG_NO if card not found. + * Returns STATUS_HF_TAG_OK with packed data on success (partial data if + * some APDUs fail — num_apdus reflects how many completed). + */ +static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + static uint8_t out[NETDATA_MAX_DATA_LENGTH]; + uint16_t out_len = 0; + + /* ---- helpers -------------------------------------------------- */ + static uint8_t abuf[64]; /* TX frame: PCB + APDU + CRC */ + static uint8_t rbuf[64]; /* single-frame receive buffer (RC522 FIFO = 64 bytes) */ + static uint8_t chain_buf[512]; /* reassembled chained response */ + uint16_t rbits; + uint8_t blk = 0; /* alternating block number */ + + /* Send one I-block APDU, handling ISO14443-4 response chaining. + * The RC522 FIFO is 64 bytes. If the card chains its response + * (PCB bit4=1), we send R(ACK) blocks and reassemble here. + * Returns pointer into chain_buf, sets *rlen_ptr to total length. */ + #define SEND_APDU(apdu_ptr, apdu_sz, rdata_ptr, rlen_ptr) ({ bool _ok = false; uint8_t _pcb = 0x02 | (blk & 0x01); abuf[0] = _pcb; memcpy(&abuf[1], (apdu_ptr), (apdu_sz)); rbits = 0; uint8_t _st = pcd_14a_reader_raw_cmd( false, true, true, false, true, false, 600, ((apdu_sz) + 1) * 8, abuf, rbuf, &rbits, sizeof(rbuf) * 8); if (_st == STATUS_HF_TAG_OK && rbits > 0) { uint16_t _rb = rbits; /* raw_cmd checkCrc=false returns byte count */ /* Verify and strip CRC manually (checkCrc=false above) */ if (_rb >= 3) { uint8_t _crc[2]; crc_14a_calculate(rbuf, _rb - 2, _crc); if (rbuf[_rb-2] == _crc[0] && rbuf[_rb-1] == _crc[1]) { blk ^= 1; uint16_t _chain_len = 0; uint8_t _resp_pcb = rbuf[0]; /* Copy data portion (strip PCB and CRC) */ uint8_t _dlen = _rb - 3; if (_dlen > 0 && _chain_len + _dlen < sizeof(chain_buf)) { memcpy(&chain_buf[_chain_len], &rbuf[1], _dlen); _chain_len += _dlen; } /* Handle chaining: PCB bit5=1 (b6 in ISO14443-4) means more data */ while (_resp_pcb & 0x20) { /* Send R(ACK) to request next block */ uint8_t _rack = 0xA2 | (blk & 0x01); abuf[0] = _rack; crc_14a_append(abuf, 1); rbits = 0; _st = pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, abuf, 3, rbuf, &rbits, sizeof(rbuf) * 8); if (_st != STATUS_HF_TAG_OK || rbits < 24) break; _rb = rbits / 8; /* bytes_transfer returns bits */ crc_14a_calculate(rbuf, _rb - 2, _crc); if (rbuf[_rb-2] != _crc[0] || rbuf[_rb-1] != _crc[1]) break; blk ^= 1; _resp_pcb = rbuf[0]; _dlen = _rb - 3; if (_dlen > 0 && _chain_len + _dlen < sizeof(chain_buf)) { memcpy(&chain_buf[_chain_len], &rbuf[1], _dlen); _chain_len += _dlen; } } *(rdata_ptr) = chain_buf; *(rlen_ptr) = (_chain_len < sizeof(chain_buf) ? _chain_len : (uint16_t)(sizeof(chain_buf) - 1)); _ok = true; } } } _ok; }) + + /* Append a cmd+resp pair to out buffer */ + #define APPEND_PAIR(cmd_ptr, cmd_sz, resp_ptr, resp_sz) do { if (out_len + 1 + (cmd_sz) + 2 + (resp_sz) < NETDATA_MAX_DATA_LENGTH) { out[out_len++] = (uint8_t)(cmd_sz); memcpy(&out[out_len], (cmd_ptr), (cmd_sz)); out_len += (cmd_sz); out[out_len++] = (uint8_t)((resp_sz) & 0xFF); out[out_len++] = (uint8_t)((resp_sz) >> 8); memcpy(&out[out_len], (resp_ptr), (resp_sz)); out_len += (resp_sz); } } while(0) + + /* ---- Step 1: scan_auto ----------------------------------------- */ + bsp_delay_ms(10); + static picc_14a_tag_t tag; + memset(&tag, 0, sizeof(tag)); + status = pcd_14a_reader_scan_auto(&tag); + if (status != STATUS_HF_TAG_OK) { + bsp_delay_ms(20); + memset(&tag, 0, sizeof(tag)); + status = pcd_14a_reader_scan_auto(&tag); + if (status != STATUS_HF_TAG_OK) { + return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL); + } + } + + /* After scan_auto completes RATS, give the RC522 time to settle. + * The hf14a_scan_keep + hf14a_raw path works because the USB round-trip + * (~2ms) gives the RC522 time to exit its post-receive state before + * the next transceive. Replicate that delay here. */ + bsp_delay_ms(5); + + /* ---- Clear RC522 stale state after RATS ----------------------- */ + /* After scan_auto+RATS, CommandReg=0x0C (PCD_TRANSCEIVE) and + * ComIrqReg=0x64 (RxIRq+TxIRq+b6 set). bytes_transfer's wait loop + * reads ComIrqReg immediately after StartSend — if RxIRq is already + * set it exits before the PPSE frame is even transmitted. + * + * Fix sequence: + * 1. Idle the RC522 — stops active TRANSCEIVE state + * 2. Wait for CommandReg to confirm idle (RC522 state machine settles) + * 3. Clear all ComIrqReg interrupt flags + * 4. Flush FIFO and clear StartSend bit */ + write_register_single(CommandReg, PCD_IDLE); + /* Spin until CommandReg confirms idle (usually immediate) */ + { + uint16_t _w = 0; + while ((read_register_single(CommandReg) & 0x0F) != PCD_IDLE && _w++ < 1000); + } + write_register_single(ComIrqReg, 0x7F); /* clear ALL IRQ flags */ + set_register_mask(FIFOLevelReg, 0x80); /* flush FIFO */ + clear_register_mask(BitFramingReg, 0x80); /* clear StartSend */ + + /* ---- Pack tag info ------------------------------------------- */ + out[out_len++] = tag.uid_len; + memcpy(&out[out_len], tag.uid, tag.uid_len); out_len += tag.uid_len; + memcpy(&out[out_len], tag.atqa, 2); out_len += 2; + out[out_len++] = tag.sak; + out[out_len++] = tag.ats_len; + memcpy(&out[out_len], tag.ats, tag.ats_len); out_len += tag.ats_len; + + /* Placeholder for num_apdus — fill in at end */ + uint16_t num_apdus_offset = out_len; + out[out_len++] = 0; + uint8_t num_apdus = 0; + + pcd_14a_reader_timeout_set(600); + + /* ---- Step 2: SELECT PPSE ------------------------------------- */ + static const uint8_t ppse_cmd[] = { + 0x00, 0xA4, 0x04, 0x00, 0x0E, + 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, + 0x44, 0x44, 0x46, 0x30, 0x31, 0x00 + }; + uint8_t *ppse_resp = NULL; uint16_t ppse_rlen = 0; + { + bool _ppse_ok = SEND_APDU(ppse_cmd, sizeof(ppse_cmd), &ppse_resp, &ppse_rlen); + if (!_ppse_ok) { + goto done; + } + } + APPEND_PAIR(ppse_cmd, sizeof(ppse_cmd), ppse_resp, ppse_rlen); + num_apdus++; + + /* ---- Extract first AID from PPSE ----------------------------- */ + uint8_t aid[16]; uint8_t aid_len = 0; + for (uint8_t i = 0; i + 1 < ppse_rlen; i++) { + if (ppse_resp[i] == 0x4F && ppse_resp[i+1] > 0 && ppse_resp[i+1] <= 16) { + aid_len = ppse_resp[i+1]; + memcpy(aid, &ppse_resp[i+2], aid_len); + break; + } + } + if (aid_len == 0) goto done; + + /* ---- Step 3: SELECT AID -------------------------------------- */ + uint8_t sel_cmd[32]; + uint8_t sel_len = 0; + sel_cmd[sel_len++] = 0x00; sel_cmd[sel_len++] = 0xA4; + sel_cmd[sel_len++] = 0x04; sel_cmd[sel_len++] = 0x00; + sel_cmd[sel_len++] = aid_len; + memcpy(&sel_cmd[sel_len], aid, aid_len); sel_len += aid_len; + sel_cmd[sel_len++] = 0x00; + + uint8_t *sel_resp; uint16_t sel_rlen; + if (!SEND_APDU(sel_cmd, sel_len, &sel_resp, &sel_rlen)) goto done; + APPEND_PAIR(sel_cmd, sel_len, sel_resp, sel_rlen); + num_apdus++; + + /* ---- Step 4: GPO -------------------------------------------- */ + static const uint8_t gpo_cmd[] = {0x80, 0xA8, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00}; + uint8_t *gpo_resp; uint16_t gpo_rlen; + if (!SEND_APDU(gpo_cmd, sizeof(gpo_cmd), &gpo_resp, &gpo_rlen)) goto done; + APPEND_PAIR(gpo_cmd, sizeof(gpo_cmd), gpo_resp, gpo_rlen); + num_apdus++; + + /* ---- Step 5: parse AFL and READ RECORDs --------------------- */ + /* Find AFL in GPO response (tag 0x94 in format 2, or bytes 3+ in format 1) */ + uint8_t *afl = NULL; uint8_t afl_len = 0; + if (gpo_rlen > 0 && gpo_resp[0] == 0x77) { + /* Format 2: search for tag 94 */ + for (uint8_t i = 2; i + 1 < gpo_rlen; ) { + uint8_t t = gpo_resp[i]; uint8_t l = gpo_resp[i+1]; + if (t == 0x94) { afl = &gpo_resp[i+2]; afl_len = l; break; } + i += 2 + l; + } + } else if (gpo_rlen > 3 && gpo_resp[0] == 0x80) { + /* Format 1: skip tag(1)+len(1)+AIP(2) */ + afl = &gpo_resp[3]; afl_len = gpo_rlen - 3 - 2; /* -2 for SW */ + } + + /* READ each record */ + for (uint8_t a = 0; a + 3 < afl_len; a += 4) { + uint8_t sfi = (afl[a] >> 3) & 0x1F; + uint8_t rec_s = afl[a+1]; + uint8_t rec_e = afl[a+2]; + if (sfi == 0 || rec_s > rec_e) continue; + for (uint8_t r = rec_s; r <= rec_e; r++) { + uint8_t rr_cmd[5] = {0x00, 0xB2, r, (uint8_t)((sfi << 3) | 4), 0x00}; + uint8_t *rr_resp; uint16_t rr_rlen; + if (!SEND_APDU(rr_cmd, 5, &rr_resp, &rr_rlen)) { + /* RC522 FIFO is 64 bytes — records > 61 bytes fail. + * Skip silently rather than aborting the whole scan. */ + NRF_LOG_INFO("14A4_EMV_SCAN: READ RECORD SFI=%d rec=%d failed (response too large?)", sfi, r); + continue; + } + APPEND_PAIR(rr_cmd, 5, rr_resp, rr_rlen); + num_apdus++; + } + } + +done: + pcd_14a_reader_timeout_set(DEF_COM_TIMEOUT); + out[num_apdus_offset] = num_apdus; + /* Return HF_TAG_OK even with 0 APDUs so Python can see tag info */ + return data_frame_make(cmd, STATUS_HF_TAG_OK, out_len, out); +} + + +static data_frame_tx_t *cmd_processor_hf14a_4_debug_counters(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + uint8_t buf[4]; + nfc_tag_14a_4_get_debug_counters(&buf[0], &buf[1], &buf[2], &buf[3]); + return data_frame_make(cmd, STATUS_SUCCESS, 4, buf); +} + static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_GET_APP_VERSION, NULL, cmd_processor_get_app_version, NULL }, { DATA_CMD_CHANGE_DEVICE_MODE, NULL, cmd_processor_change_device_mode, NULL }, @@ -2027,6 +2433,16 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_VIKING_GET_EMU_ID, NULL, cmd_processor_viking_get_emu_id, NULL }, { DATA_CMD_PAC_SET_EMU_ID, NULL, cmd_processor_pac_set_emu_id, NULL }, { DATA_CMD_PAC_GET_EMU_ID, NULL, cmd_processor_pac_get_emu_id, NULL }, + /* ISO14443-4 T=CL emulation */ + { DATA_CMD_HF14A_4_APDU_RECV, NULL, cmd_processor_hf14a_4_apdu_recv, NULL }, + { DATA_CMD_HF14A_4_APDU_SEND, NULL, cmd_processor_hf14a_4_apdu_send, NULL }, + { DATA_CMD_HF14A_4_SET_ANTI_COLL, NULL, cmd_processor_hf14a_4_set_anti_coll, NULL }, + { DATA_CMD_HF14A_4_STATIC_RESP, NULL, cmd_processor_hf14a_4_static_resp, NULL }, + { DATA_CMD_HF14A_4_READER_APDU, before_hf_reader_run, cmd_processor_hf14a_4_reader_apdu, NULL }, + { DATA_CMD_HF14A_4_EMV_SCAN, before_hf_reader_run, cmd_processor_hf14a_4_emv_scan, NULL }, + { 6010, NULL, cmd_processor_hf14a_4_debug_counters, NULL }, + /* HF14A scan keeping field alive */ + { DATA_CMD_HF14A_SCAN_KEEP, before_hf_reader_run, cmd_processor_hf14a_scan_keep, NULL }, }; data_frame_tx_t *cmd_processor_get_device_capabilities(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index ec89711..2705a8e 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -67,6 +67,7 @@ #define DATA_CMD_MF1_READ_ONE_BLOCK (2008) #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_MF1_MANIPULATE_VALUE_BLOCK (2011) #define DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS (2012) #define DATA_CMD_MF1_HARDNESTED_ACQUIRE (2013) @@ -167,6 +168,14 @@ // // ****************************************************************** +/* ISO14443-4 T=CL emulation commands */ +#define DATA_CMD_HF14A_4_APDU_RECV (6000) /* non-blocking poll: firmware->host APDU */ +#define DATA_CMD_HF14A_4_APDU_SEND (6001) /* host->firmware APDU response */ +#define DATA_CMD_HF14A_4_SET_ANTI_COLL (6002) /* set UID/ATQA/SAK/ATS */ +#define DATA_CMD_HF14A_4_STATIC_RESP (6003) /* add/clear static APDU response pair */ +#define DATA_CMD_HF14A_4_READER_APDU (6004) /* select+RATS+send APDU, keep field */ +#define DATA_CMD_HF14A_4_EMV_SCAN (6005) /* full EMV scan in one call */ + #define DATA_CMD_EM410X_SET_EMU_ID (5000) #define DATA_CMD_EM410X_GET_EMU_ID (5001) #define DATA_CMD_HIDPROX_SET_EMU_ID (5002) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c index d6fd0f6..50aa32b 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c @@ -527,6 +527,10 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { } // RATS instruction if (p_data[0] == NFC_TAG_14A_CMD_RATS && nfc_tag_14a_checks_crc(p_data, 4)) { + // Reset T=CL layer state for the new session + if (m_tag_handler.cb_reset != NULL) { + m_tag_handler.cb_reset(); + } // Make sure the sub -packaging opens the support of ATS if (auto_coll_res->ats->length > 0) { // Take out FSD and return according to the maximum FSD @@ -571,11 +575,10 @@ static inline void nrf_nfct_reset(void) { // Use Window Grid frame delay mode. nrf_nfct_frame_delay_mode_set(NRF_NFCT_FRAME_DELAY_MODE_WINDOWGRID); - /* Begin: Workaround for anomaly 25 */ - /* Workaround for wrong SENSRES values require using SDD00001, but here SDD00100 is used - because it is required to operate with Windows Phone */ - nrf_nfct_sensres_bit_frame_sdd_set(NRF_NFCT_SENSRES_BIT_FRAME_SDD_00100); - /* End: Workaround for anomaly 25 */ + /* Use SDD00001 per ISO14443-3 standard. + * Note: SDD00100 was previously used for Windows Phone compatibility + * but breaks standard readers (including Proxmark3). SDD00001 is correct. */ + nrf_nfct_sensres_bit_frame_sdd_set(NRF_NFCT_SENSRES_BIT_FRAME_SDD_00001); // Restore interrupts. nrf_nfct_int_enable(int_enabled); diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h index 0d92022..cd1702b 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h @@ -4,7 +4,7 @@ #include "tag_emulation.h" #define MAX_NFC_RX_BUFFER_SIZE 257 -#define MAX_NFC_TX_BUFFER_SIZE 64 +#define MAX_NFC_TX_BUFFER_SIZE 512 /* must hold PCB + max APDU response */ #define NFC_TAG_14A_CRC_LENGTH 2 diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.c new file mode 100644 index 0000000..8d23c6d --- /dev/null +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.c @@ -0,0 +1,446 @@ +/** + * @file nfc_14a_4.c + * @brief ISO14443-4 T=CL emulation for ChameleonUltra + * + * Implements a full ISO14443-4 tag emulator with a static APDU response + * table. The table is populated by the host before field activation, so + * the firmware can respond to an EMV reader autonomously without any USB + * communication while the RF field is active. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include "nfc_14a_4.h" +#include "nfc_14a.h" +#include "tag_emulation.h" +#include "tag_persistence.h" +#include "fds_util.h" +#include "nrf_log.h" + +/* ------------------------------------------------------------------ */ +/* PCB byte constants (ISO14443-4 §7) */ +/* ------------------------------------------------------------------ */ +#define PCB_IBLOCK_MASK 0xC0 +#define PCB_IBLOCK_VAL 0x00 +#define PCB_RBLOCK_MASK 0xE0 +#define PCB_RBLOCK_VAL 0x80 /* R(ACK) = 0xA2/0xA3, R(NAK) = 0xB2/0xB3 */ +#define PCB_SBLOCK_MASK 0xC0 +#define PCB_SBLOCK_VAL 0xC0 +#define PCB_BLOCK_NUM 0x01 +#define PCB_CID_FOLLOWING 0x10 /* bit4: CID follows */ +#define PCB_NAD_FOLLOWING 0x08 /* bit3: NAD follows */ +#define PCB_CHAIN 0x20 /* bit5: chaining flag per ISO14443-4 Table 3 */ +#define PCB_SBLOCK_WTX 0x30 +#define PCB_SBLOCK_DESELECT 0xC2 +#define WTX_VALUE 0x3B /* WTXM=59 (~3s extra wait) */ + +static inline bool is_iblock(uint8_t pcb) { + return (pcb & PCB_IBLOCK_MASK) == PCB_IBLOCK_VAL; +} +static inline bool is_rblock(uint8_t pcb) { + /* R-block: bit7=1, bit6=0, bit2=1, bit1=0 (mask 0xC6, value 0x82) */ + return (pcb & 0xC6) == 0x82; +} +static inline bool is_sblock(uint8_t pcb) { + return (pcb & PCB_SBLOCK_MASK) == PCB_SBLOCK_VAL; +} + +/* ------------------------------------------------------------------ */ +/* Module state */ +/* ------------------------------------------------------------------ */ +static nfc_tag_14a_4_information_t *m_tag_information = NULL; + +/* Shadow coll-res references into m_tag_information */ +static nfc_tag_14a_coll_res_reference_t m_shadow_coll_res; + +/* T=CL session state */ +static uint8_t m_block_num = 0; +static bool m_cid_supported = false; +static uint8_t m_cid = 0; +static uint8_t m_apdu_buf[NFC_14A_4_MAX_APDU]; +static uint16_t m_apdu_len = 0; +static bool m_apdu_pending = false; +static uint8_t m_resp_buf[NFC_14A_4_MAX_APDU]; +static uint16_t m_resp_len = 0; +static bool m_response_ready = false; + +/* TX scratch buffer */ +static uint8_t m_tx_buf[NFC_14A_4_MAX_APDU + 4]; + +/* Debug counters — readable via hf 14a debug */ +static uint8_t m_dbg_iblocks_rx = 0; /* I-blocks received */ +static uint8_t m_dbg_iblocks_tx = 0; /* I-blocks sent */ +static uint8_t m_dbg_last_rx_pcb = 0; /* PCB of last received I-block */ +static uint8_t m_dbg_last_match = 0; /* last find_static_response result */ + +/* Static APDU response table (RAM copy, populated from m_tag_information) */ +static nfc_tag_14a_4_static_response_t m_static_resp[NFC_14A_4_MAX_STATIC_RESPONSES]; +static uint8_t m_static_resp_count = 0; + +/* Large response overflow (RAM only, > NFC_14A_4_MAX_STATIC_RESP_LEN bytes). + * NOT persisted to flash. Must reload via emv load after power cycle. */ +typedef struct { + uint8_t cmd[NFC_14A_4_MAX_STATIC_CMD_LEN]; + uint8_t cmd_len; + uint8_t resp[NFC_14A_4_MAX_LARGE_RESP_LEN]; + uint16_t resp_len; +} nfc_tag_14a_4_large_response_t; +static nfc_tag_14a_4_large_response_t m_large_resp[NFC_14A_4_MAX_LARGE_RESPONSES]; +static uint8_t m_large_resp_count = 0; + +/* ------------------------------------------------------------------ */ +/* Static response table */ +/* ------------------------------------------------------------------ */ + +void nfc_tag_14a_4_add_static_response(const uint8_t *cmd, uint8_t cmd_len, + const uint8_t *resp, uint16_t resp_len) { + if (cmd_len > NFC_14A_4_MAX_STATIC_CMD_LEN) cmd_len = NFC_14A_4_MAX_STATIC_CMD_LEN; + + if (resp_len > NFC_14A_4_MAX_STATIC_RESP_LEN) { + /* Large response: RAM-only overflow table */ + if (m_large_resp_count >= NFC_14A_4_MAX_LARGE_RESPONSES) return; + if (resp_len > NFC_14A_4_MAX_LARGE_RESP_LEN) resp_len = NFC_14A_4_MAX_LARGE_RESP_LEN; + nfc_tag_14a_4_large_response_t *le = &m_large_resp[m_large_resp_count++]; + le->cmd_len = cmd_len; + le->resp_len = resp_len; + memcpy(le->cmd, cmd, cmd_len); + memcpy(le->resp, resp, resp_len); + return; + } + + /* Normal response: flash-backed table */ + if (m_static_resp_count >= NFC_14A_4_MAX_STATIC_RESPONSES) return; + nfc_tag_14a_4_static_response_t *e = &m_static_resp[m_static_resp_count++]; + e->cmd_len = cmd_len; + e->resp_len = (uint8_t)resp_len; + memcpy(e->cmd, cmd, cmd_len); + memcpy(e->resp, resp, resp_len); + if (m_tag_information && + m_tag_information->static_resp_count < NFC_14A_4_MAX_STATIC_RESPONSES) { + memcpy(&m_tag_information->static_resp[m_tag_information->static_resp_count++], + e, sizeof(*e)); + } +} + +void nfc_tag_14a_4_clear_static_responses(void) { + m_static_resp_count = 0; + m_large_resp_count = 0; + if (m_tag_information) { + m_tag_information->static_resp_count = 0; + } +} + +static bool find_static_response(const uint8_t *apdu, uint16_t apdu_len, + uint8_t **resp_out, uint16_t *resp_len_out) { + /* Flash-backed table */ + for (uint8_t i = 0; i < m_static_resp_count; i++) { + nfc_tag_14a_4_static_response_t *e = &m_static_resp[i]; + if (apdu_len >= e->cmd_len && + memcmp(apdu, e->cmd, e->cmd_len) == 0) { + *resp_out = e->resp; + *resp_len_out = e->resp_len; + return true; + } + } + /* RAM-only large response table */ + for (uint8_t i = 0; i < m_large_resp_count; i++) { + nfc_tag_14a_4_large_response_t *e = &m_large_resp[i]; + if (apdu_len >= e->cmd_len && + memcmp(apdu, e->cmd, e->cmd_len) == 0) { + *resp_out = e->resp; + *resp_len_out = e->resp_len; + return true; + } + } + return false; +} + +/* ------------------------------------------------------------------ */ +/* TX helpers */ +/* ------------------------------------------------------------------ */ + +static void send_iblock(const uint8_t *data, uint16_t len) { + uint8_t pcb = 0x02 | (m_block_num & 0x01); + if (m_cid_supported) pcb |= PCB_CID_FOLLOWING; + uint8_t off = 0; + m_tx_buf[off++] = pcb; + if (m_cid_supported) m_tx_buf[off++] = m_cid & 0x0F; + if (len > NFC_14A_4_MAX_APDU) len = NFC_14A_4_MAX_APDU; + memcpy(&m_tx_buf[off], data, len); + nfc_tag_14a_tx_bytes(m_tx_buf, off + len, true); + m_block_num ^= 1; +} + +static void send_rack(void) { + uint8_t pcb = 0xA2 | (m_block_num & 0x01); + if (m_cid_supported) { + pcb |= PCB_CID_FOLLOWING; + uint8_t buf[2] = { pcb, m_cid & 0x0F }; + nfc_tag_14a_tx_bytes(buf, 2, true); + } else { + nfc_tag_14a_tx_bytes(&pcb, 1, true); + } +} + +static void send_wtx(void) { + uint8_t buf[3]; + uint8_t off = 0; + buf[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0); + if (m_cid_supported) buf[off++] = m_cid & 0x0F; + buf[off++] = WTX_VALUE; + nfc_tag_14a_tx_bytes(buf, off, true); +} + +/* ------------------------------------------------------------------ */ +/* State handler (called from NFCT ISR on each received frame) */ +/* ------------------------------------------------------------------ */ + +static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) { + if (szBytes == 0) return; + uint8_t pcb = data[0]; + + /* ---- S-block ---- */ + if (is_sblock(pcb)) { + if ((pcb & 0xF7) == PCB_SBLOCK_DESELECT) { + /* Echo DESELECT */ + nfc_tag_14a_tx_bytes(data, szBytes, true); + nfc_tag_14a_4_reset_handler(); + return; + } + if ((pcb & 0x3F) == (PCB_SBLOCK_WTX & 0x3F)) { + /* Reader sending WTX — echo back with our WTXM */ + uint8_t wtxm = (szBytes > 1) ? data[szBytes - 1] & 0x3F : WTX_VALUE; + uint8_t resp[3]; + uint8_t off = 0; + resp[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0); + if (m_cid_supported) resp[off++] = m_cid & 0x0F; + resp[off++] = wtxm; + nfc_tag_14a_tx_bytes(resp, off, true); + /* If we now have a response ready, send it next I-block */ + if (m_response_ready) { + m_response_ready = false; + send_iblock(m_resp_buf, m_resp_len); + } + return; + } + return; + } + + /* ---- R-block ---- */ + if (is_rblock(pcb)) { + send_rack(); + return; + } + + /* ---- I-block ---- */ + if (is_iblock(pcb)) { + uint8_t reader_blknum = pcb & PCB_BLOCK_NUM; + bool has_cid = (pcb & PCB_CID_FOLLOWING) != 0; + bool has_nad = (pcb & PCB_NAD_FOLLOWING) != 0; + bool more_chain = (pcb & PCB_CHAIN) != 0; + + uint8_t offset = 1; + if (has_cid) { + /* CID acknowledged but not used in responses (keeps protocol simpler) */ + m_cid_supported = false; + offset++; /* skip CID byte */ + } + if (has_nad) offset++; + + if (offset >= szBytes) { + send_rack(); + return; + } + + uint16_t apdu_len = szBytes - offset; + if (apdu_len > NFC_14A_4_MAX_APDU) apdu_len = NFC_14A_4_MAX_APDU; + + m_dbg_iblocks_rx++; + m_dbg_last_rx_pcb = pcb; + NRF_LOG_INFO("14A4 I-block #%d: reader_blk=%d m_block_num=%d apdu_len=%d", + m_dbg_iblocks_rx, reader_blknum, m_block_num, apdu_len); + + /* Block number check per ISO14443-4 §7.5.3.3: + * If block number matches expected, process new APDU. + * If block number does NOT match, it is a retransmit — + * resend the last response without re-processing. */ + if (reader_blknum != (m_block_num & 0x01)) { + /* Retransmit: resend last response */ + if (m_resp_len > 0) { + /* Restore block num to what we sent last time and resend */ + m_block_num ^= 1; /* undo the increment from last send */ + send_iblock(m_resp_buf, m_resp_len); + } else { + send_rack(); + } + return; + } + + memcpy(m_apdu_buf, &data[offset], apdu_len); + m_apdu_len = apdu_len; + m_apdu_pending = true; + m_response_ready = false; + + if (more_chain) { + send_rack(); + return; + } + + /* APDU complete — check static table first, then WTX */ + { + uint8_t *static_resp = NULL; + uint16_t static_len = 0; + bool _found = find_static_response(m_apdu_buf, apdu_len, + &static_resp, &static_len); + m_dbg_last_match = _found ? 1 : 0; + NRF_LOG_INFO("14A4 find_static: found=%d static_len=%d resp_count=%d", + _found, static_len, m_static_resp_count); + if (_found) { + m_dbg_iblocks_tx++; + memcpy(m_resp_buf, static_resp, static_len); + m_resp_len = static_len; + send_iblock(m_resp_buf, m_resp_len); + } else if (m_response_ready) { + m_response_ready = false; + send_iblock(m_resp_buf, m_resp_len); + } else { + /* No response ready — keep reader alive with WTX */ + send_wtx(); + } + } + return; + } + + NRF_LOG_INFO("14A-4: unknown PCB 0x%02x", pcb); +} + + +/* ------------------------------------------------------------------ */ +/* APDU relay API (for host-driven responses) */ +/* ------------------------------------------------------------------ */ + +bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length) { + if (!m_apdu_pending) return false; + m_apdu_pending = false; + *length = m_apdu_len; + memcpy(buf, m_apdu_buf, m_apdu_len); + return true; +} + +void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length) { + if (length > NFC_14A_4_MAX_APDU) length = NFC_14A_4_MAX_APDU; + memcpy(m_resp_buf, data, length); + m_resp_len = length; + m_response_ready = true; +} + +/* ------------------------------------------------------------------ */ +/* Reset handler */ +/* ------------------------------------------------------------------ */ + +void nfc_tag_14a_4_reset_handler(void) { + m_block_num = 0; + m_cid_supported = false; + m_cid = 0; + m_apdu_pending = false; + m_response_ready = false; + m_apdu_len = 0; + m_resp_len = 0; +} + +void nfc_tag_14a_4_get_debug_counters(uint8_t *rx, uint8_t *tx, + uint8_t *last_pcb, uint8_t *last_match) { + *rx = m_dbg_iblocks_rx; + *tx = m_dbg_iblocks_tx; + *last_pcb = m_dbg_last_rx_pcb; + *last_match = m_dbg_last_match; +} + +/* ------------------------------------------------------------------ */ +/* Anti-collision resource */ +/* ------------------------------------------------------------------ */ + +nfc_tag_14a_coll_res_reference_t *nfc_tag_14a_4_get_coll_res(void) { + if (m_tag_information == NULL) return NULL; + m_shadow_coll_res.sak = m_tag_information->res_coll.sak; + m_shadow_coll_res.atqa = m_tag_information->res_coll.atqa; + m_shadow_coll_res.uid = m_tag_information->res_coll.uid; + m_shadow_coll_res.size = &m_tag_information->res_coll.size; + m_shadow_coll_res.ats = &m_tag_information->res_coll.ats; + return &m_shadow_coll_res; +} + +/* ------------------------------------------------------------------ */ +/* Data load / save / factory callbacks */ +/* ------------------------------------------------------------------ */ + +int nfc_tag_14a_4_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) { + int info_size = sizeof(nfc_tag_14a_4_information_t); + if (buffer->length < info_size) { + NRF_LOG_ERROR("14A-4 loadcb: buffer too small (%d < %d)", + buffer->length, info_size); + return info_size; + } + m_tag_information = (nfc_tag_14a_4_information_t *)buffer->buffer; + + /* Populate RAM static table from persisted slot data */ + m_static_resp_count = m_tag_information->static_resp_count; + if (m_static_resp_count > NFC_14A_4_MAX_STATIC_RESPONSES) + m_static_resp_count = NFC_14A_4_MAX_STATIC_RESPONSES; + memcpy(m_static_resp, m_tag_information->static_resp, + m_static_resp_count * sizeof(nfc_tag_14a_4_static_response_t)); + + nfc_tag_14a_handler_t handler = { + .get_coll_res = nfc_tag_14a_4_get_coll_res, + .cb_state = nfc_tag_14a_4_state_handler, + .cb_reset = nfc_tag_14a_4_reset_handler, + }; + nfc_tag_14a_set_handler(&handler); + NRF_LOG_INFO("14A-4 loadcb OK: SAK=%02x uid_sz=%d static_resp=%d", + m_tag_information->res_coll.sak[0], + m_tag_information->res_coll.size, + m_static_resp_count); + return info_size; +} + +int nfc_tag_14a_4_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer) { + return sizeof(nfc_tag_14a_4_information_t); +} + +bool nfc_tag_14a_4_data_factory(uint8_t slot, tag_specific_type_t tag_type) { + if (tag_type != TAG_TYPE_HF14A_4) return false; + + /* Build factory defaults on stack and write directly to FDS + * (same pattern as nfc_tag_mf1_data_factory). */ + nfc_tag_14a_4_information_t info; + memset(&info, 0, sizeof(info)); + + /* Placeholder 7-byte NXP-style UID */ + info.res_coll.size = NFC_TAG_14A_UID_DOUBLE_SIZE; + info.res_coll.atqa[0] = 0x04; + info.res_coll.atqa[1] = 0x00; + info.res_coll.sak[0] = 0x20; /* ISO14443-4 */ + info.res_coll.uid[0] = 0x04; + info.res_coll.uid[1] = 0x01; + info.res_coll.uid[2] = 0x02; + info.res_coll.uid[3] = 0x03; + info.res_coll.uid[4] = 0x04; + info.res_coll.uid[5] = 0x05; + info.res_coll.uid[6] = 0x06; + + static const uint8_t default_ats[] = { + 0x10, 0x78, 0x80, 0x70, 0x02, 0x00, + 0x31, 0xC1, 0x64, 0x09, 0x97, 0x61, + 0x26, 0x00, 0x90, 0x00 + }; + info.res_coll.ats.length = sizeof(default_ats); + memcpy(info.res_coll.ats.data, default_ats, sizeof(default_ats)); + info.static_resp_count = 0; + + fds_slot_record_map_t map_info; + get_fds_map_by_slot_sense_type_for_dump(slot, TAG_SENSE_HF, &map_info); + bool ret = fds_write_sync(map_info.id, map_info.key, sizeof(info), &info); + NRF_LOG_INFO("14A-4 factory slot %d: %s", slot, ret ? "OK" : "FAIL"); + return ret; +} diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.h b/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.h new file mode 100644 index 0000000..3c4a5ef --- /dev/null +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a_4.h @@ -0,0 +1,71 @@ +/** + * @file nfc_14a_4.h + * @brief ISO14443-4 T=CL emulation for ChameleonUltra + * + * Implements a full ISO14443-4 tag emulator: + * - I-blocks (information, chaining, CID) + * - R-blocks (ACK/NAK retransmit) + * - S-blocks (WTX to keep reader alive, DESELECT) + * - Static APDU response table (pre-loaded before field, no USB needed + * during field exchange) + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef NFC_14A_4_H +#define NFC_14A_4_H + +#include "nfc_14a.h" +#include "tag_emulation.h" + +/* Maximum APDU size (FSCI=8 → FSC=256, minus PCB+CRC = 253) */ +#define NFC_14A_4_MAX_APDU 260 /* max APDU in RAM; flash entries capped at 253 */ + +/* Static APDU response table — up to 12 pre-configured command/response pairs. + * Loaded before field activation; firmware responds autonomously without USB. */ +#define NFC_14A_4_MAX_STATIC_RESPONSES 12 +#define NFC_14A_4_MAX_LARGE_RESPONSES 4 /* RAM-only, for resp > 253 bytes */ +#define NFC_14A_4_MAX_LARGE_RESP_LEN 260 /* max large response size */ +#define NFC_14A_4_MAX_STATIC_CMD_LEN 16 +#define NFC_14A_4_MAX_STATIC_RESP_LEN 253 /* max bytes in flash-backed slot */ + +typedef struct __attribute__((packed)) { + uint8_t cmd_len; + uint8_t cmd[NFC_14A_4_MAX_STATIC_CMD_LEN]; + uint8_t resp_len; + uint8_t resp[NFC_14A_4_MAX_STATIC_RESP_LEN]; +} nfc_tag_14a_4_static_response_t; + +/** + * Per-slot persistent data layout stored in FDS flash. + * Anti-collision response (UID/ATQA/SAK/ATS) plus the static response table. + */ +typedef struct __attribute__((packed)) { + nfc_tag_14a_coll_res_entity_t res_coll; + uint8_t static_resp_count; + nfc_tag_14a_4_static_response_t static_resp[NFC_14A_4_MAX_STATIC_RESPONSES]; +} nfc_tag_14a_4_information_t; + +/* Anti-collision resource — used by get_coll_res_data in app_cmd.c */ +nfc_tag_14a_coll_res_reference_t *nfc_tag_14a_4_get_coll_res(void); + +/* tag_base_map callbacks */ +int nfc_tag_14a_4_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer); +int nfc_tag_14a_4_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer); +bool nfc_tag_14a_4_data_factory(uint8_t slot, tag_specific_type_t tag_type); + +/* Static response table management (called before hw mode -e) */ +void nfc_tag_14a_4_add_static_response(const uint8_t *cmd, uint8_t cmd_len, + const uint8_t *resp, uint16_t resp_len); +void nfc_tag_14a_4_clear_static_responses(void); + +/* APDU relay — host-driven responses */ +bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length); +void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length); + +/* Reset handler */ +void nfc_tag_14a_4_reset_handler(void); + +#endif /* NFC_14A_4_H */ + +void nfc_tag_14a_4_get_debug_counters(uint8_t *rx, uint8_t *tx, uint8_t *last_pcb, uint8_t *last_match); diff --git a/firmware/application/src/rfid/nfctag/tag_base_type.h b/firmware/application/src/rfid/nfctag/tag_base_type.h index 6a26cb0..b759bdb 100644 --- a/firmware/application/src/rfid/nfctag/tag_base_type.h +++ b/firmware/application/src/rfid/nfctag/tag_base_type.h @@ -91,6 +91,7 @@ typedef enum { // ST25TA series 2000 // HF14A-4 series 3000 + TAG_TYPE_HF14A_4 = 3000, } tag_specific_type_t; @@ -115,7 +116,7 @@ typedef enum { TAG_TYPE_MIFARE_4096, TAG_TYPE_NTAG_213, TAG_TYPE_NTAG_215, \ TAG_TYPE_NTAG_216, TAG_TYPE_MF0ICU1, TAG_TYPE_MF0ICU2, \ TAG_TYPE_MF0UL11, TAG_TYPE_MF0UL21, TAG_TYPE_NTAG_210, \ - TAG_TYPE_NTAG_212 + TAG_TYPE_NTAG_212, TAG_TYPE_HF14A_4 typedef struct { tag_specific_type_t tag_hf; diff --git a/firmware/application/src/rfid/nfctag/tag_emulation.c b/firmware/application/src/rfid/nfctag/tag_emulation.c index 66a23fd..25e6353 100644 --- a/firmware/application/src/rfid/nfctag/tag_emulation.c +++ b/firmware/application/src/rfid/nfctag/tag_emulation.c @@ -7,6 +7,7 @@ #include "nfc_14a.h" #include "nfc_mf0_ntag.h" #include "nfc_mf1.h" +#include "nfc_14a_4.h" #include "rgb_marquee.h" #include "tag_persistence.h" @@ -111,6 +112,8 @@ static tag_base_handler_map_t tag_base_map[] = { {TAG_SENSE_HF, TAG_TYPE_MF0ICU2, nfc_tag_mf0_ntag_data_loadcb, nfc_tag_mf0_ntag_data_savecb, nfc_tag_mf0_ntag_data_factory, &m_tag_data_hf}, {TAG_SENSE_HF, TAG_TYPE_MF0UL11, nfc_tag_mf0_ntag_data_loadcb, nfc_tag_mf0_ntag_data_savecb, nfc_tag_mf0_ntag_data_factory, &m_tag_data_hf}, {TAG_SENSE_HF, TAG_TYPE_MF0UL21, nfc_tag_mf0_ntag_data_loadcb, nfc_tag_mf0_ntag_data_savecb, nfc_tag_mf0_ntag_data_factory, &m_tag_data_hf}, + // ISO14443-4 T=CL emulation + {TAG_SENSE_HF, TAG_TYPE_HF14A_4, nfc_tag_14a_4_data_loadcb, nfc_tag_14a_4_data_savecb, nfc_tag_14a_4_data_factory, &m_tag_data_hf}, }; static void tag_emulation_load_config(void); diff --git a/firmware/application/src/rfid/reader/hf/rc522.c b/firmware/application/src/rfid/reader/hf/rc522.c index a75c56a..a179047 100644 --- a/firmware/application/src/rfid/reader/hf/rc522.c +++ b/firmware/application/src/rfid/reader/hf/rc522.c @@ -861,7 +861,7 @@ uint8_t pcd_14a_reader_scan_auto(picc_14a_tag_t *tag) { * @retval : Status value hf_tag_ok, success */ uint8_t pcd_14a_reader_ats_request(uint8_t *pAts, uint16_t *szAts, uint16_t szAtsBitMax) { - uint8_t rats[] = { PICC_RATS, 0x80, 0x31, 0x73 }; // FSD=256, FSDI=8, CID=0 + uint8_t rats[] = { PICC_RATS, 0x40, 0x3D, 0xB5 }; // FSD=48, FSDI=4, CID=0 (fits RC522 64-byte FIFO) uint8_t status; status = pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, rats, sizeof(rats), pAts, szAts, szAtsBitMax); diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 9e4ab6e..f1fcc4d 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -755,6 +755,7 @@ lf = root.subgroup("lf", "Low Frequency commands") lf_em = lf.subgroup("em", "EM commands") lf_em_4x05 = lf_em.subgroup("4x05", "EM4x05/EM4x69 commands") data = root.subgroup('data', 'Data analysis and visualization commands') +emv = root.subgroup('emv', 'EMV contactless payment card commands') lf_em_410x = lf_em.subgroup("410x", "EM410x commands") @@ -7939,3 +7940,531 @@ class DataModulation(BaseCLIUnit): else: print(f" RTF gaps : {CR}none ^`^t no gap commands detected{C0}") + +# ============================================================================ +# EMV contactless payment card commands (emv subgroup) +# ============================================================================ + +def _emv_decode_apdu(data: bytes) -> str: + """Return a brief human-readable description of a command APDU.""" + if len(data) < 4: + return '' + cla, ins, p1, p2 = data[0], data[1], data[2], data[3] + lc = data[4] if len(data) > 4 else 0 + body = data[5:5 + lc] if len(data) > 5 else b'' + if cla == 0x00 and ins == 0xA4 and p1 == 0x04 and body: + known = { + bytes.fromhex('325041592e5359532e4444463031'): 'PPSE (2PAY.SYS.DDF01)', + bytes.fromhex('a0000000031010'): 'Visa Credit/Debit', + bytes.fromhex('a0000000041010'): 'Mastercard Debit', + bytes.fromhex('a000000025010402'): 'Amex', + } + return 'SELECT AID ' + known.get(body.lower(), body.hex().upper()) + if cla == 0x80 and ins == 0xA8: + return 'GET PROCESSING OPTIONS (GPO)' + if cla == 0x00 and ins == 0xB2: + return f'READ RECORD SFI={(p2 >> 3) & 0x1F} rec={p1}' + return f'CLA={cla:02x} INS={ins:02x} P1={p1:02x} P2={p2:02x}' + + +@emv.command('scan') +class EMVScan(DeviceRequiredUnit): + """ + Full EMV contactless card scan — equivalent to PM3 'emv scan -at'. + + Scans an ISO14443-4 card, performs the full EMV transaction sequence + (SELECT PPSE, SELECT AID, GPO, READ RECORDs) and saves results to a + JSON file compatible with PM3's emv scan output format. + + Place the card on the CU antenna before running. + + Usage: + emv scan print results to terminal + emv scan -f /tmp/card.json save to JSON file + """ + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'EMV contactless card scan (reader mode) — like PM3 emv scan -at' + parser.add_argument('-f', '--file', default='', metavar='', + help='Save results to JSON file (PM3-compatible format)') + parser.add_argument('-s', '--slot', type=int, default=None, + metavar='<1-8>', help='Also load scanned card into this slot for emulation') + return parser + + def on_exec(self, args: argparse.Namespace): + import time, json as jsonlib + cmd = self.cmd + + # Ensure reader mode + try: + if not cmd.is_device_reader_mode(): + cmd.set_device_reader_mode(True) + time.sleep(0.5) + except Exception: + time.sleep(0.3) + + print(f' {CY}Scanning... (place card on antenna){C0}') + + # Single firmware call — full EMV sequence without USB round-trips + resp = cmd.hf14a_4_emv_scan() + if resp.status != Status.HF_TAG_OK or not resp.data: + print(f' {CR}No card found or scan failed (status={resp.status}){C0}') + return + + # Parse packed response + d = bytes(resp.data) + off = 0 + + uid_len = d[off]; off += 1 + uid = d[off:off+uid_len]; off += uid_len + atqa = d[off:off+2]; off += 2 + sak = d[off]; off += 1 + ats_len = d[off]; off += 1 + ats = d[off:off+ats_len]; off += ats_len + + uid_str = ' '.join(f'{b:02X}' for b in uid) + atqa_str = ' '.join(f'{b:02X}' for b in atqa) + ats_str = ' '.join(f'{b:02X}' for b in ats) + print(f' {CG}UID : {uid_str}{C0}') + print(f' {CG}ATQA: {atqa_str} SAK: {sak:02X}{C0}') + print(f' {CG}ATS : {ats_str}{C0}') + + num_apdus = d[off]; off += 1 + pairs = [] + for _ in range(num_apdus): + cl = d[off]; off += 1 + c = d[off:off+cl]; off += cl + rl = d[off] | (d[off+1] << 8); off += 2 + r = d[off:off+rl]; off += rl + pairs.append((c, r)) + + if not pairs: + print(f' {CR}No APDU responses captured{C0}') + return + result = {} + result['File'] = {'Created': 'chameleon emv scan'} + result['Card'] = {'Contactless': { + 'Communication': 'iso14443-4a', + 'UID': uid_str, 'ATQA': atqa_str, + 'SAK': f'{sak:02X}', 'ATS': ats_str, + }} + + def tlv_to_dict(data): + if not data: return {} + i = 0 + tl = 2 if (data[i] & 0x1F) == 0x1F else 1 + tag_hex = data[:tl].hex().upper(); i += tl + if i >= len(data): return {} + if data[i] & 0x80: + nb = data[i] & 0x7F; i += 1 + vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb + else: + vlen = data[i]; i += 1 + val = data[i:i+vlen] + return {'tag': tag_hex, 'length': f'{vlen:02X}', + 'value': ' '.join(f'{b:02X}' for b in val)} + + def find_tag(data, tag): + results = []; i = 0 + while i < len(data) - 1: + tl = 2 if (data[i] & 0x1F) == 0x1F else 1 + if i + tl > len(data): break + cur = data[i:i+tl]; i += tl + if i >= len(data): break + if data[i] & 0x80: + nb = data[i] & 0x7F; i += 1 + vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb + else: + vlen = data[i]; i += 1 + val = data[i:i+vlen]; i += vlen + if int.from_bytes(cur, 'big') == tag: results.append(val) + elif cur[0] & 0x20: results.extend(find_tag(val, tag)) + return results + + # PPSE + if pairs: + ppse_cmd, ppse_resp = pairs[0] + ppse_body = ppse_resp[:-2] if len(ppse_resp) >= 2 else ppse_resp + print(f'\n {CG}PPSE OK ({len(ppse_resp)}b){C0}') + result['PPSE'] = { + 'AID': '32 50 41 59 2E 53 59 53 2E 44 44 46 30 31', + 'FCITemplate': tlv_to_dict(ppse_body), + } + + if len(pairs) >= 2: + sel_cmd, sel_resp = pairs[1] + sel_body = sel_resp[:-2] if len(sel_resp) >= 2 else sel_resp + aid_bytes = sel_cmd[5:-1] if len(sel_cmd) > 6 else b'' + aid_str = ' '.join(f'{b:02X}' for b in aid_bytes) + print(f' {CG}SELECT AID OK ({len(sel_resp)}b){C0}') + result['Application'] = {'AID': aid_str, + 'FCITemplate': tlv_to_dict(sel_body)} + + if len(pairs) >= 3: + gpo_cmd, gpo_resp = pairs[2] + gpo_body = gpo_resp[:-2] if len(gpo_resp) >= 2 else gpo_resp + print(f' {CG}GPO OK ({len(gpo_resp)}b){C0}') + result['Application']['GPO'] = tlv_to_dict(gpo_body) + records = [] + for cb, rb in pairs[3:]: + sfi_n = (cb[3] >> 3) & 0x1F if len(cb) >= 4 else 0 + rec_n = cb[2] if len(cb) >= 3 else 0 + r_body = rb[:-2] if len(rb) >= 2 else rb + print(f' {CG}READ RECORD SFI={sfi_n} rec={rec_n} OK ({len(rb)}b){C0}') + records.append({'SFI': f'{sfi_n:02X}', 'RecordNum': f'{rec_n:02X}', + 'Offline': '01', 'Data': tlv_to_dict(r_body)}) + result['Application']['Records'] = records + + json_str = jsonlib.dumps(result, indent=2) + if args.file: + try: + with open(args.file, 'w') as fp: fp.write(json_str) + print(f'\n {CG}Saved to {args.file}{C0}') + except Exception as e: + print(f' {CR}Save failed: {e}{C0}') + else: + print(f'\n{json_str}') + + if args.slot is not None and pairs: + target_slot = SlotNumber(args.slot) + print(f'\n {CY}Loading into slot {target_slot}...{C0}') + try: + cmd.set_slot_tag_type(target_slot, TagSpecificType.HF14A_4) + cmd.set_slot_data_default(target_slot, TagSpecificType.HF14A_4) + cmd.set_slot_enable(target_slot, TagSenseType.HF, True) + cmd.hf14a_4_set_anti_coll(uid, atqa, sak, ats) # atqa already in wire order + cmd.hf14a_4_clear_static_responses() + for c, r in pairs: + cmd.hf14a_4_add_static_response(c, r) # use full cmd as match key + cmd.slot_data_config_save() + print(f' {CG}Slot {target_slot} ready. Run: hw slot change -s {args.slot} && hw mode -e{C0}') + except Exception as e: + print(f' {CR}Slot load failed: {e}{C0}') + + +@emv.command('debug') +class EMVDebug(DeviceRequiredUnit): + """Show T=CL emulation debug counters (I-blocks rx/tx, last PCB, last match).""" + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Show T=CL emulation debug counters' + return parser + + def on_exec(self, args: argparse.Namespace): + resp = self.cmd.device.send_cmd_sync(6010, b'') + if resp.status != Status.SUCCESS or not resp.data or len(resp.data) < 4: + print(f' {CR}Debug command failed{C0}') + return + d = resp.data + print(f' {CY}T=CL debug counters:{C0}') + print(f' I-blocks received : {d[0]}') + print(f' I-blocks sent : {d[1]}') + print(f' Last rx PCB : {d[2]:02x} (blk_num={(d[2] & 0x01)}, chain={(d[2]>>5)&1}, cid={(d[2]>>4)&1})') + print(f' Last static match : {"yes" if d[3] else "no"}') + + +@emv.command('load') +class EMVLoad(DeviceRequiredUnit): + """ + Load EMV card data into an HF14A_4 slot for emulation. + + Supports two modes: + 1. Load from a JSON file (PM3 emv scan -at output) + 2. Add a single custom APDU command/response pair + + Usage: + emv load -f /tmp/card.json -s 3 load full card from PM3 JSON + emv load --clear clear static responses + emv load --cmd 00A4... --resp 6F.. add single APDU pair + emv load --defaults load Mastercard test defaults + """ + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Load EMV APDU responses into HF14A_4 slot for autonomous emulation' + parser.add_argument('-f', '--file', default='', metavar='', + help='Load from PM3 emv scan JSON file') + parser.add_argument('-s', '--slot', type=int, default=None, + metavar='<1-8>', help='Target slot when using --file (default: active)') + parser.add_argument('--clear', action='store_true', + help='Clear all static responses from active slot') + parser.add_argument('--cmd', default='', metavar='', + help='Command APDU prefix to match (hex)') + parser.add_argument('--resp', default='', metavar='', + help='Response APDU to return (hex)') + parser.add_argument('--defaults', action='store_true', + help='Load built-in Mastercard test responses') + return parser + + def on_exec(self, args: argparse.Namespace): + cmd = self.cmd + + if args.clear: + cmd.hf14a_4_clear_static_responses() + print(f' {CG}Static responses cleared.{C0}') + return + + if args.cmd and args.resp: + try: + c = bytes.fromhex(args.cmd.replace(' ', '')) + r = bytes.fromhex(args.resp.replace(' ', '')) + cmd.hf14a_4_add_static_response(c, r) + print(f' {CG}Added: {c.hex().upper()} → {r.hex().upper()}{C0}') + except ValueError as e: + print(f' {CR}Invalid hex: {e}{C0}') + return + + if args.defaults: + self._load_defaults(cmd) + return + + if args.file: + if args.slot is not None: + target_slot = SlotNumber(args.slot) + else: + target_slot = SlotNumber.from_fw(cmd.get_active_slot()) + self._load_from_json(args.file, target_slot, cmd) + return + + print(f' {CY}Specify --file, --cmd/--resp, --clear, or --defaults{C0}') + + def _load_defaults(self, cmd): + """Load built-in Mastercard test APDU responses.""" + cmd.hf14a_4_clear_static_responses() + pairs = [ + # SELECT PPSE + (bytes.fromhex('00a404000e325041592e5359532e4444463031'), + bytes.fromhex('6f23840e325041592e5359532e4444463031' + 'a511bf0c0e610c4f07a000000004101087010190 00'.replace(' ', '')), + 'SELECT PPSE'), + # SELECT Mastercard Debit AID + (bytes.fromhex('00a4040007a0000000041010'), + bytes.fromhex('6f1d8407a0000000041010a512500a' + '4d6173746572436172648701019f38009000'), + 'SELECT Mastercard AID'), + # GPO — decline gracefully + (bytes.fromhex('80a80000'), + bytes.fromhex('6985'), + 'GPO (conditions not satisfied)'), + ] + for c, r, name in pairs: + resp = cmd.hf14a_4_add_static_response(c, r) + if resp.status == Status.SUCCESS: + print(f' {CG}Loaded: {name}{C0}') + else: + print(f' {CR}Failed: {name}{C0}') + print(f'\n {CY}Default responses loaded. Run: hw mode -e{C0}') + + def _tlv_encode_len(self, n: int) -> bytes: + """Encode integer n as BER-TLV length (short or long form).""" + if n < 0x80: + return bytes([n]) + elif n <= 0xFF: + return bytes([0x81, n]) + else: + return bytes([0x82, (n >> 8) & 0xFF, n & 0xFF]) + + def _load_from_json(self, filepath, target_slot, cmd): + """Load card data from a PM3 emv scan JSON file.""" + import json as jsonlib, os + if not os.path.exists(filepath): + print(f' {CR}File not found: {filepath}{C0}') + return + try: + with open(filepath) as f: + data = jsonlib.load(f) + except Exception as e: + print(f' {CR}JSON parse error: {e}{C0}') + return + + # Parse card info + try: + card = data['Card']['Contactless'] + uid = bytes.fromhex(card['UID'].replace(' ', '')) + atqa = bytes.fromhex(card['ATQA'].replace(' ', '')) + sak = int(card['SAK'], 16) + ats_raw = bytes.fromhex(card['ATS'].replace(' ', '')) + ats = ats_raw[:ats_raw[0]] if ats_raw else b'' + except Exception as e: + print(f' {CR}Card info parse error: {e}{C0}') + return + + uid_str = ' '.join(f'{b:02X}' for b in uid) + print(f' {CG}Card from JSON:{C0}') + print(f' UID : {CG}{uid_str}{C0}') + print(f' ATQA : {CG}{atqa.hex().upper()}{C0} SAK: {CG}{sak:02X}{C0}') + print(f' ATS : {CG}{ats.hex().upper()}{C0}') + + static_pairs = [] + + def tlv_resp(tag_hex, len_hex, val_hex): + """Reconstruct TLV response with proper BER length encoding + SW 9000.""" + tag_b = bytes.fromhex(tag_hex) + val_b = bytes.fromhex(val_hex) + n = int(len_hex, 16) + len_b = self._tlv_encode_len(n) + return tag_b + len_b + val_b + bytes([0x90, 0x00]) + + try: + v = data['PPSE']['FCITemplate']['value'].replace(' ', '') + l = data['PPSE']['FCITemplate']['length'] + static_pairs.append(( + bytes.fromhex('00a404000e325041592e5359532e4444463031'), + tlv_resp('6F', l, v), + 'SELECT PPSE')) + except Exception as e: + print(f' {CR}PPSE: {e}{C0}') + + try: + v = data['Application']['FCITemplate']['value'].replace(' ', '') + l = data['Application']['FCITemplate']['length'] + aid = data['Application']['AID'].replace(' ', '') + static_pairs.append(( + bytes.fromhex('00a4040007' + aid), + tlv_resp('6F', l, v), + 'SELECT AID')) + except Exception as e: + print(f' {CR}Application FCI: {e}{C0}') + + try: + v = data['Application']['GPO']['value'].replace(' ', '') + l = data['Application']['GPO']['length'] + tag = data['Application']['GPO'].get('tag', '77') + static_pairs.append(( + bytes.fromhex('80a80000'), + tlv_resp(tag, l, v), + 'GPO')) + except Exception as e: + print(f' {CR}GPO: {e}{C0}') + + try: + for rec in data['Application'].get('Records', []): + sfi_n = int(rec['SFI'], 16) + rec_n = int(rec['RecordNum'], 16) + v = rec['Data']['value'].replace(' ', '') + l = rec['Data']['length'] + tag = rec['Data'].get('tag', '70') + p2 = (sfi_n << 3) | 4 + static_pairs.append(( + bytes([0x00, 0xB2, rec_n, p2, 0x00]), + tlv_resp(tag, l, v), + f'READ RECORD SFI={sfi_n} rec={rec_n}')) + except Exception as e: + print(f' {CR}Records: {e}{C0}') + + # Configure slot + print(f'\n {CY}Configuring slot {target_slot}...{C0}') + cmd.set_slot_tag_type(target_slot, TagSpecificType.HF14A_4) + cmd.set_slot_data_default(target_slot, TagSpecificType.HF14A_4) + cmd.set_slot_enable(target_slot, TagSenseType.HF, True) + + # PM3 JSON stores ATQA in display order (byte1,byte0) — swap to wire order + atqa_wire = bytes([atqa[1], atqa[0]]) if len(atqa) == 2 else atqa + cmd.hf14a_4_set_anti_coll(uid, atqa_wire, sak, ats) + + cmd.hf14a_4_clear_static_responses() + for c, r, name in static_pairs: + try: + cmd.hf14a_4_add_static_response(c, r) + print(f' {CG}+ {name} ({len(r)}b){C0}') + except Exception as e: + print(f' {CR} Failed {name}: {e}{C0}') + + cmd.slot_data_config_save() + print(f'\n {CG}Done! Slot {target_slot} ready with {len(static_pairs)} response(s).{C0}') + print(f' {C0}Next: hw slot change -s {target_slot} && hw mode -e{C0}') + + +@emv.command('apdu') +class EMVApdu(DeviceRequiredUnit): + """ + ISO14443-4 T=CL interactive APDU relay. + + CU emulates an ISO14443-4 card and relays APDUs to/from the terminal. + For each APDU from the reader, you type the hex response bytes. + + Requires HF14A_4 slot configured with SAK=20 and ATS. Run hw mode -e first. + + Usage: + emv apdu + emv apdu --timeout 30000 + """ + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'ISO14443-4 T=CL interactive APDU relay (manual response mode)' + parser.add_argument('--timeout', type=int, default=15000, metavar='', + help='Total relay timeout in ms (default: 15000)') + return parser + + def on_exec(self, args: argparse.Namespace): + import time + cmd = self.cmd + timeout_ms = max(1000, min(60000, args.timeout)) + + print(f' {CY}ISO14443-4 T=CL APDU relay started{C0}') + print(f' Waiting for a reader to connect (SAK=20 slot required)...') + print(f' Type {CY}quit{C0} to exit, or enter hex response bytes when prompted.') + + exchange_count = 0 + + while True: + resp = None + deadline = time.monotonic() + (timeout_ms / 1000.0) + while time.monotonic() < deadline: + try: + r = cmd.hf14a_4_apdu_recv() + except Exception as e: + print(f' {CR}Error polling for APDU: {e}{C0}') + resp = None + break + if r.status == Status.SUCCESS: + resp = r + break + elif r.status != Status.HF_TAG_NO: + print(f' {CR}Firmware error: {r.status}{C0}') + resp = None + break + time.sleep(0.02) + + if resp is None: + print(f' {C0}No APDU received within timeout.{C0}') + break + + apdu = bytes(resp.data) + desc = _emv_decode_apdu(apdu) + exchange_count += 1 + apdu_hex = ' '.join(f'{b:02x}' for b in apdu) + print(f'\n [{exchange_count}] {CY}APDU →→ {apdu_hex}{C0}') + if desc: + print(f' {C0}{desc}{C0}') + + try: + user_input = input(f' Response (hex) [{CG}90 00{C0}]: ').strip() + except (EOFError, KeyboardInterrupt): + break + + if user_input.lower() == 'quit': + break + if not user_input: + user_input = '9000' + + try: + response_bytes = bytes.fromhex(user_input.replace(' ', '')) + except ValueError: + print(f' {CR}Invalid hex — sending 6F00 (error){C0}') + response_bytes = bytes.fromhex('6F00') + + try: + cmd.hf14a_4_apdu_send(response_bytes) + resp_hex = ' '.join(f'{b:02x}' for b in response_bytes) + print(f' {CG}←← Response {resp_hex}{C0}') + except Exception as e: + print(f' {CR}Error sending response: {e}{C0}') + break + + print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') + + diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 932f9eb..71ae813 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -235,6 +235,101 @@ class ChameleonCMD: return resp @expect_response(Status.HF_TAG_OK) + def hf14a_scan_keep(self): + """ + Scan ISO14443-A tag with full select + RATS, keeping field alive. + + Identical to hf14a_scan but does NOT tear down the RF field afterward. + The card remains powered and in ISO14443-4 T=CL state so subsequent + hf14a_raw calls can exchange APDUs without re-selecting. + """ + resp = self.device.send_cmd_sync(Command.HF14A_SCAN_KEEP) + if resp.status == Status.HF_TAG_OK: + offset = 0 + data = [] + while offset < len(resp.data): + uidlen, = struct.unpack_from('!B', resp.data, offset); offset += 1 + uid, atqa, sak, atslen = struct.unpack_from( + f'!{uidlen}s2s1sB', resp.data, offset) + offset += struct.calcsize(f'!{uidlen}s2s1sB') + ats, = struct.unpack_from(f'!{atslen}s', resp.data, offset) + offset += atslen + data.append({'uid': uid, 'atqa': atqa, 'sak': sak, 'ats': ats}) + resp.parsed = data + return resp + + def hf14a_4_set_anti_coll(self, uid: bytes, atqa: bytes, sak: int, ats: bytes): + """ + Set UID / ATQA / SAK / ATS for the active HF14A_4 slot. + + :param uid: UID bytes (4 or 7 bytes) + :param atqa: ATQA 2 bytes (wire order, e.g. b'\x04\x00' for ATQA 00 04) + :param sak: SAK byte value (int), use 0x20 for ISO14443-4 + :param ats: ATS bytes (without CRC) + """ + uid_size = len(uid) + payload = (bytes([uid_size]) + bytes(uid) + bytes(atqa) + + bytes([sak]) + bytes([len(ats)]) + bytes(ats)) + return self.device.send_cmd_sync(Command.HF14A_4_SET_ANTI_COLL, payload) + + def hf14a_4_apdu_recv(self): + """ + Non-blocking poll for a pending APDU from the ISO14443-4 T=CL stack. + + Returns immediately: STATUS_SUCCESS + APDU bytes if one is pending, + STATUS_HF_TAG_NO if no APDU is waiting. Call in a tight loop from + the host side for relay/capture use cases. + """ + return self.device.send_cmd_sync(Command.HF14A_4_APDU_RECV, b'', timeout=2) + + def hf14a_4_apdu_send(self, resp: bytes): + """Send an APDU response to the ISO14443-4 T=CL stack.""" + payload = bytes([(len(resp) >> 8) & 0xFF, len(resp) & 0xFF]) + bytes(resp) + return self.device.send_cmd_sync(Command.HF14A_4_APDU_SEND, payload) + + def hf14a_4_add_static_response(self, cmd: bytes, resp: bytes): + """ + Add a static APDU command→response pair to the HF14A_4 slot. + + The firmware will automatically reply with resp whenever it receives + an APDU whose first len(cmd) bytes match cmd, without USB involvement. + Must be called before hw mode -e. + """ + rlen = len(resp); payload = bytes([len(cmd)]) + bytes(cmd) + bytes([(rlen >> 8) & 0xFF, rlen & 0xFF]) + bytes(resp) + return self.device.send_cmd_sync(Command.HF14A_4_STATIC_RESP, payload) + + def hf14a_4_reader_apdu(self, apdu: bytes): + """ + Select card (with RATS) and send one ISO14443-4 T=CL APDU in a single + firmware call — avoiding the USB round-trip gap that would depower the card. + + :param apdu: raw APDU bytes (no PCB wrapping needed) + :return: response object with resp.data = APDU response bytes (no PCB/CRC) + """ + return self.device.send_cmd_sync( + Command.HF14A_4_READER_APDU, bytes(apdu), timeout=3) + + def hf14a_4_emv_scan(self): + """ + Full EMV card scan in a single firmware call. + + The firmware performs the complete sequence (field cycle, select, RATS, + PPSE, SELECT AID, GPO, READ RECORDs) without returning to the host + between APDUs, avoiding the field-drop issue with separate calls. + + Response format: + uid_len(1) uid(n) atqa(2) sak(1) ats_len(1) ats(m) + num_apdus(1) + for each APDU pair: + cmd_len(1) cmd(n) resp_len_le(2) resp(m) + """ + resp = self.device.send_cmd_sync(Command.HF14A_4_EMV_SCAN, b'', timeout=10) + return resp + + def hf14a_4_clear_static_responses(self): + """Clear all static APDU responses from the active HF14A_4 slot.""" + return self.device.send_cmd_sync(Command.HF14A_4_STATIC_RESP, b'\x00') + def hf14a_raw(self, options, resp_timeout_ms=100, data=[], bitlen=None): """ Send raw cmd to 14a tag. diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index 4b4baef..da9baa3 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -68,6 +68,7 @@ class Command(enum.IntEnum): MF1_READ_ONE_BLOCK = 2008 MF1_WRITE_ONE_BLOCK = 2009 HF14A_RAW = 2010 + HF14A_SCAN_KEEP = 2016 MF1_MANIPULATE_VALUE_BLOCK = 2011 MF1_CHECK_KEYS_OF_SECTORS = 2012 MF1_HARDNESTED_ACQUIRE = 2013 @@ -138,6 +139,14 @@ class Command(enum.IntEnum): MF1_SET_FIELD_OFF_DO_RESET = 4038 MF1_GET_FIELD_OFF_DO_RESET = 4039 + # ISO14443-4 T=CL emulation + HF14A_4_APDU_RECV = 6000 + HF14A_4_APDU_SEND = 6001 + HF14A_4_SET_ANTI_COLL = 6002 + HF14A_4_STATIC_RESP = 6003 + HF14A_4_READER_APDU = 6004 + HF14A_4_EMV_SCAN = 6005 + EM410X_SET_EMU_ID = 5000 EM410X_GET_EMU_ID = 5001 HIDPROX_SET_EMU_ID = 5002 @@ -333,7 +342,8 @@ class TagSpecificType(enum.IntEnum): # ST25TA series 2000 - # HF14A-4 series 3000 + # ISO14443-4 T=CL emulation + HF14A_4 = 3000 @staticmethod def list(exclude_meta=True): From c7e038cc612e119a91b529206e70afa219f2c75f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 4 Apr 2026 13:35:26 +0200 Subject: [PATCH 02/17] Remove duplicate rc522.h include Removed duplicate rc522.h include and adjusted spacing. --- firmware/application/src/app_cmd.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 309dae5..2aaf78d 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -18,11 +18,10 @@ #include "bsp_wdt.h" #include "lf_reader_generic.h" #include "lf_em4x05_data.h" +#include "rc522.h" #endif #include "nfc_14a.h" #include "nfc_14a_4.h" -#include "rc522.h" - #define NRF_LOG_MODULE_NAME app_cmd #include "nrf_log.h" From 9183ac40e4712a4dda4537d7f22ecea9b09c0855 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 4 Apr 2026 13:52:52 +0200 Subject: [PATCH 03/17] Add PROJECT_CHAMELEON_ULTRA specific commands --- firmware/application/src/app_cmd.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 2aaf78d..6cec716 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -1967,7 +1967,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_static_resp(uint16_t cmd, uint16_t nfc_tag_14a_4_add_static_response(&data[1], cmd_len, &data[3 + cmd_len], (uint8_t)resp_len); return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); } - +#if defined(PROJECT_CHAMELEON_ULTRA) /** * HF14A scan keeping field alive after completion — identical to hf14a_scan * but registered without after_hf_reader_run so the field stays on and the @@ -2121,6 +2121,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t * Returns STATUS_HF_TAG_OK with packed data on success (partial data if * some APDUs fail — num_apdus reflects how many completed). */ +#if defined(PROJECT_CHAMELEON_ULTRA) static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { static uint8_t out[NETDATA_MAX_DATA_LENGTH]; uint16_t out_len = 0; @@ -2286,7 +2287,7 @@ done: /* Return HF_TAG_OK even with 0 APDUs so Python can see tag info */ return data_frame_make(cmd, STATUS_HF_TAG_OK, out_len, out); } - +#endif static data_frame_tx_t *cmd_processor_hf14a_4_debug_counters(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { uint8_t buf[4]; @@ -2433,6 +2434,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_PAC_SET_EMU_ID, NULL, cmd_processor_pac_set_emu_id, NULL }, { DATA_CMD_PAC_GET_EMU_ID, NULL, cmd_processor_pac_get_emu_id, NULL }, /* ISO14443-4 T=CL emulation */ +#if defined(PROJECT_CHAMELEON_ULTRA) +/* ISO14443-4 T=CL emulation */ { DATA_CMD_HF14A_4_APDU_RECV, NULL, cmd_processor_hf14a_4_apdu_recv, NULL }, { DATA_CMD_HF14A_4_APDU_SEND, NULL, cmd_processor_hf14a_4_apdu_send, NULL }, { DATA_CMD_HF14A_4_SET_ANTI_COLL, NULL, cmd_processor_hf14a_4_set_anti_coll, NULL }, @@ -2443,7 +2446,7 @@ static cmd_data_map_t m_data_cmd_map[] = { /* HF14A scan keeping field alive */ { DATA_CMD_HF14A_SCAN_KEEP, before_hf_reader_run, cmd_processor_hf14a_scan_keep, NULL }, }; - +#endif data_frame_tx_t *cmd_processor_get_device_capabilities(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { size_t count = ARRAYLEN(m_data_cmd_map); uint16_t commands[count]; From fcf0c31ca5d5843cbe57273ba100e033f763c781 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 4 Apr 2026 14:01:18 +0200 Subject: [PATCH 04/17] Fix syntax error in app_cmd.c --- firmware/application/src/app_cmd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 6cec716..2e6f4c4 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -2445,8 +2445,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { 6010, NULL, cmd_processor_hf14a_4_debug_counters, NULL }, /* HF14A scan keeping field alive */ { DATA_CMD_HF14A_SCAN_KEEP, before_hf_reader_run, cmd_processor_hf14a_scan_keep, NULL }, -}; #endif +}; data_frame_tx_t *cmd_processor_get_device_capabilities(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { size_t count = ARRAYLEN(m_data_cmd_map); uint16_t commands[count]; From a3d3c1fc34b7854fd6ac976da13297e7fe57d138 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 4 Apr 2026 14:10:44 +0200 Subject: [PATCH 05/17] Remove conditional compilation for PROJECT_CHAMELEON_ULTRA --- firmware/application/src/app_cmd.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 2e6f4c4..212d1e2 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -1891,9 +1891,6 @@ static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, return data_frame_make(cmd, STATUS_SUCCESS, m_sniff_buf_len, m_sniff_buf); } -#endif - - /* ======================================================================== * HF14A-4 ISO14443-4 T=CL emulation commands (6000-range) * ======================================================================== */ @@ -1967,7 +1964,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_static_resp(uint16_t cmd, uint16_t nfc_tag_14a_4_add_static_response(&data[1], cmd_len, &data[3 + cmd_len], (uint8_t)resp_len); return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); } -#if defined(PROJECT_CHAMELEON_ULTRA) + /** * HF14A scan keeping field alive after completion — identical to hf14a_scan * but registered without after_hf_reader_run so the field stays on and the @@ -2121,7 +2118,6 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t * Returns STATUS_HF_TAG_OK with packed data on success (partial data if * some APDUs fail — num_apdus reflects how many completed). */ -#if defined(PROJECT_CHAMELEON_ULTRA) static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { static uint8_t out[NETDATA_MAX_DATA_LENGTH]; uint16_t out_len = 0; @@ -2287,14 +2283,13 @@ done: /* Return HF_TAG_OK even with 0 APDUs so Python can see tag info */ return data_frame_make(cmd, STATUS_HF_TAG_OK, out_len, out); } -#endif static data_frame_tx_t *cmd_processor_hf14a_4_debug_counters(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { uint8_t buf[4]; nfc_tag_14a_4_get_debug_counters(&buf[0], &buf[1], &buf[2], &buf[3]); return data_frame_make(cmd, STATUS_SUCCESS, 4, buf); } - +#endif static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_GET_APP_VERSION, NULL, cmd_processor_get_app_version, NULL }, { DATA_CMD_CHANGE_DEVICE_MODE, NULL, cmd_processor_change_device_mode, NULL }, From e16505e6a749864130cc32be8af59913dfe299dc Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 5 Apr 2026 12:38:41 +0200 Subject: [PATCH 06/17] FEAT! Add T55 write commands --- firmware/application/src/app_cmd.c | 31 +++- firmware/application/src/data_cmd.h | 1 + .../src/rfid/nfctag/lf/protocols/t55xx.h | 4 +- .../src/rfid/reader/lf/lf_reader_main.c | 33 ++++ .../src/rfid/reader/lf/lf_reader_main.h | 3 + software/script/chameleon_cli_unit.py | 168 ++++++++++++++++++ software/script/chameleon_cmd.py | 1 + software/script/chameleon_enum.py | 1 + 8 files changed, 234 insertions(+), 8 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 212d1e2..36b2a39 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -833,17 +833,33 @@ static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16 return data_frame_make(cmd, status, 0, NULL); } -static data_frame_tx_t *cmd_processor_pac_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { +static data_frame_tx_t *cmd_processor_lf_t55xx_write(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { typedef struct { - uint8_t id[LF_PAC_TAG_ID_SIZE]; - uint8_t new_key[4]; - uint8_t old_keys[4]; + uint8_t block; /* block number */ + uint8_t word[4]; /* 32-bit data word, big-endian */ + uint8_t use_pwd; /* 1 = password write, 0 = open write */ + uint8_t pwd[4]; /* 32-bit password, big-endian (ignored when use_pwd == 0) */ + uint8_t page1; /* 1 = target page 1, 0 = page 0 */ } PACKED payload_t; - payload_t *payload = (payload_t *)data; - if (length < sizeof(payload_t) || (length - offsetof(payload_t, old_keys)) % sizeof(payload->old_keys) != 0) { + + if (length < sizeof(payload_t)) { return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); } - status = write_pac_to_t55xx(payload->id, payload->new_key, payload->old_keys, (length - offsetof(payload_t, old_keys)) / sizeof(payload->old_keys)); + + payload_t *p = (payload_t *)data; + + bool page1 = (bool)p->page1; + uint8_t max_block = page1 ? 3u : 7u; + + if (p->block > max_block) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + + uint32_t word = bytes_to_num(p->word, 4); + uint32_t passwd = bytes_to_num(p->pwd, 4); + bool use_pwd = (bool)p->use_pwd; + + status = lf_t55xx_write_block(p->block, word, passwd, use_pwd, page1); return data_frame_make(cmd, status, 0, NULL); } @@ -2361,6 +2377,7 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_IOPROX_WRITE_TO_T55XX, before_reader_run, cmd_processor_ioprox_write_to_t55xx, NULL }, { DATA_CMD_PAC_SCAN, before_reader_run, cmd_processor_pac_scan, NULL }, { DATA_CMD_PAC_WRITE_TO_T55XX, before_reader_run, cmd_processor_pac_write_to_t55xx, NULL }, + { DATA_CMD_LF_T55XX_WRITE, before_reader_run, cmd_processor_lf_t55xx_write, NULL }, { DATA_CMD_ADC_GENERIC_READ, before_reader_run, cmd_processor_generic_read, NULL }, { DATA_CMD_HF14A_SET_FIELD_ON, before_reader_run, cmd_processor_hf14a_set_field_on, NULL }, diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 2705a8e..b681d00 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -106,6 +106,7 @@ #define DATA_CMD_IOPROX_WRITE_TO_T55XX (3011) #define DATA_CMD_IOPROX_DECODE_RAW (3012) #define DATA_CMD_IOPROX_COMPOSE_ID (3013) +#define DATA_CMD_LF_T55XX_WRITE (3014) // // ****************************************************************** diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h index c0383ba..4242f56 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h +++ b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h @@ -81,9 +81,11 @@ extern "C" { T5577_PWD | \ (4 << T5577_MAXBLOCK_SHIFT)) +#if defined(PROJECT_CHAMELEON_ULTRA) void t55xx_write_data(uint32_t passwd, uint32_t *blks, uint8_t blk_count); void t55xx_reset_passwd(uint32_t old_passwd, uint32_t new_passwd); - +void t55xx_send_cmd(uint8_t opcode, uint32_t *passwd, uint8_t data_len, uint32_t *data, uint8_t block); +#endif #ifdef __cplusplus } #endif diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_main.c b/firmware/application/src/rfid/reader/lf/lf_reader_main.c index b96fa0d..6aa8383 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.c @@ -1,5 +1,6 @@ #include "lf_reader_main.h" +#include #include "bsp_delay.h" #include "bsp_time.h" #include "hex_utils.h" @@ -209,3 +210,35 @@ uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_pass * Set the LF card scanning timeout value (in milliseconds). */ void set_scan_tag_timeout(uint32_t ms) { g_timeout_readem_ms = ms; } + +#if defined(PROJECT_CHAMELEON_ULTRA) +/** + * Write a single raw 32-bit word to a T55xx block. + * + * Unlike write_em410x_to_t55xx() and friends, this writes the exact word + * supplied with no protocol encoding — useful for custom configuration + * words, recovery of locked tags, or scripted programming. + * + * Only available on Chameleon Ultra (Lite has no LF writer hardware). + * + * @param block Block number (0-7 for page 0, 0-3 for page 1) + * @param word 32-bit data word to write + * @param passwd Password for password-protected write (ignored when use_passwd is false) + * @param use_passwd true = password-protected write, false = open write + * @param page1 true = target page 1, false = page 0 + * @return STATUS_LF_TAG_OK always (T55xx gives no ACK; verify by reading back) + */ +uint8_t lf_t55xx_write_block(uint8_t block, uint32_t word, uint32_t passwd, bool use_passwd, bool page1) { + uint8_t opcode = page1 ? T5577_OPCODE_PAGE1 : T5577_OPCODE_PAGE0; + uint32_t *pwd_ptr = use_passwd ? &passwd : NULL; + + start_lf_125khz_radio(); + bsp_delay_ms(1); // Delay for a while after starting the field + + t55xx_send_cmd(opcode, pwd_ptr, 0, &word, block); + t55xx_send_cmd(T5577_OPCODE_RESET, NULL, 0, NULL, 0); + + stop_lf_125khz_radio(); + return STATUS_LF_TAG_OK; +} +#endif diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_main.h b/firmware/application/src/rfid/reader/lf/lf_reader_main.h index 629f015..df4a820 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -24,3 +24,6 @@ uint8_t write_hidprox_to_t55xx(uint8_t format, uint32_t fc, uint64_t cn, uint32_ uint8_t write_ioprox_to_t55xx(uint8_t *raw_data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); uint8_t write_viking_to_t55xx(uint8_t *uid, uint8_t *newkey, uint8_t *old_keys, uint8_t old_key_count); uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); +#if defined(PROJECT_CHAMELEON_ULTRA) +uint8_t lf_t55xx_write_block(uint8_t block, uint32_t word, uint32_t passwd, bool use_passwd, bool page1); +#endif diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index f1fcc4d..f62a5d2 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -764,6 +764,7 @@ lf_hid_prox = lf_hid.subgroup("prox", "HID Prox commands") lf_ioprox = lf.subgroup("ioprox", "ioProx commands") lf_pac = lf.subgroup("pac", "PAC/Stanley commands") lf_viking = lf.subgroup("viking", "Viking commands") +lf_t55xx = lf.subgroup("t55xx", "T55xx raw commands") lf_generic = lf.subgroup("generic", "Generic commands") @@ -6008,6 +6009,173 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit): print(f" - Viking ID(8H): {id_hex} write done.") +@lf_t55xx.command("clone") +class LFT55xxClone(ReaderRequiredUnit): + """ + Clone a scanned or manually-specified LF card ID onto a blank T55xx tag. + + Supported types and their required arguments: + + em410x --id <10 hex> e.g. --id DEADBEEF88 + electra --id <26 hex> e.g. --id DEADBEEF880102030405060708 + hid -f --cn e.g. -f H10301 --fc 10 --cn 1234 + ioprox --ver --fc --cn OR --raw8 <16 hex> + viking --id <8 hex> e.g. --id DEADBEEF + + Only supported on Chameleon Ultra (Lite has no LF writer). + """ + + TYPES = ["em410x", "electra", "hid", "ioprox", "viking"] + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Clone a LF card ID onto a blank T55xx tag.\n" + "Supported types: em410x, electra, hid, ioprox, viking.\n" + "Only supported on Chameleon Ultra (Lite has no LF writer)." + ) + parser.add_argument( + "-t", "--type", + type=str, + required=True, + choices=self.TYPES, + metavar="TYPE", + help="Card type: " + ", ".join(self.TYPES), + ) + # EM410x / Electra / Viking + parser.add_argument( + "--id", + type=str, + required=False, + metavar="HEX", + help="Card ID in hex: 10 hex for em410x, 26 for electra, 8 for viking", + ) + # HID Prox + parser.add_argument( + "-f", "--format", + type=str, + required=False, + choices=[x.name for x in HIDFormat], + metavar="FORMAT", + help="HID Prox format, e.g. H10301 (required for hid type)", + ) + parser.add_argument( + "--fc", + type=int, + required=False, + metavar="INT", + help="Facility code (HID / ioProx)", + ) + parser.add_argument( + "--cn", + type=int, + required=False, + metavar="INT", + help="Card number (HID / ioProx)", + ) + parser.add_argument( + "--il", + type=int, + required=False, + metavar="INT", + help="Issue level (HID, optional)", + ) + parser.add_argument( + "--oem", + type=int, + required=False, + metavar="INT", + help="OEM code (HID, optional)", + ) + # ioProx + parser.add_argument( + "--ver", + type=int, + required=False, + metavar="INT", + help="Version byte (ioProx)", + ) + parser.add_argument( + "--raw8", + type=str, + required=False, + metavar="HEX", + help="ioProx raw 8 bytes in hex, e.g. 007854E03A5D65AB", + ) + return parser + + def on_exec(self, args: argparse.Namespace): + t = args.type + + if t in ("em410x", "electra"): + if args.id is None: + raise ArgsParserError("--id is required for em410x / electra") + expected = 10 if t == "em410x" else 26 + if not re.match(r"^[a-fA-F0-9]{" + str(expected) + r"}$", args.id): + raise ArgsParserError( + f"--id must be exactly {expected} hex characters for {t}" + ) + id_bytes = bytes.fromhex(args.id) + self.cmd.em410x_write_to_t55xx(id_bytes) + label = "EM410x Electra" if t == "electra" else "EM410x" + print(f" - {label} ID cloned to T55xx: {args.id.upper()}") + + elif t == "hid": + if args.format is None: + raise ArgsParserError("-f/--format is required for hid") + if args.cn is None: + raise ArgsParserError("--cn is required for hid") + fmt = HIDFormat[args.format] + fc = args.fc if args.fc is not None else 0 + il = args.il if args.il is not None else 0 + oem = args.oem if args.oem is not None else 0 + LFHIDIdArgsUnit.check_limits(fmt.value, fc, args.cn, il, oem) + cn = args.cn + id_bytes = struct.pack( + ">BIBIBH", + fmt.value, + fc, + (cn >> 32), + cn & 0xFFFFFFFF, + il, + oem, + ) + self.cmd.hidprox_write_to_t55xx(id_bytes) + print(f" - HID Prox cloned to T55xx") + print(f" Format : {fmt.name}") + if fc: print(f" FC : {fc}") + if il: print(f" IL : {il}") + if oem: print(f" OEM : {oem}") + print(f" CN : {cn}") + + elif t == "ioprox": + ver = args.ver if args.ver is not None else 1 + fc = int(args.fc, 0) if args.fc is not None else 0 + cn = args.cn if args.cn is not None else 0 + if args.raw8 is not None: + raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8) + ver, fc, cn, raw8, *_ = self.cmd.ioprox_decode_raw(raw8) + else: + res = self.cmd.ioprox_compose_id(ver, fc, cn) + raw8 = res[3] + payload16 = struct.pack(">BBH8s4x", ver & 0xFF, fc & 0xFF, cn & 0xFFFF, raw8) + self.cmd.ioprox_write_to_t55xx(payload16) + print(f" - ioProx cloned to T55xx") + print(f" Ver : {ver}") + print(f" FC : {fc} [0x{fc:02X}]") + print(f" CN : {cn}") + print(f" Raw8 : {raw8.hex().upper()}") + + elif t == "viking": + if args.id is None: + raise ArgsParserError("--id is required for viking") + if not re.match(r"^[a-fA-F0-9]{8}$", args.id): + raise ArgsParserError("--id must be exactly 8 hex characters for viking") + id_bytes = bytes.fromhex(args.id) + self.cmd.viking_write_to_t55xx(id_bytes) + print(f" - Viking ID cloned to T55xx: {args.id.upper()}") + + @lf_generic.command("adcread") class LFADCGenericRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 71ae813..a59bdea 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -668,6 +668,7 @@ class ChameleonCMD: + def lf_sniff(self, timeout_ms: int = 2000): """ Capture raw LF field ADC samples. diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index da9baa3..3e0cdb4 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -92,6 +92,7 @@ class Command(enum.IntEnum): IOPROX_WRITE_TO_T55XX = 3011 IOPROX_DECODE_RAW = 3012 IOPROX_COMPOSE_ID = 3013 + LF_T55XX_WRITE = 3014 MF1_WRITE_EMU_BLOCK_DATA = 4000 HF14A_SET_ANTI_COLL_DATA = 4001 From bbfda3070de6603794c1ce93b6fc31d551056670 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 5 Apr 2026 12:52:15 +0200 Subject: [PATCH 07/17] Fix: T55 write commands help --- software/script/chameleon_utils.py | 94 +++++++++++++++++------------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/software/script/chameleon_utils.py b/software/script/chameleon_utils.py index 420609a..9a90051 100644 --- a/software/script/chameleon_utils.py +++ b/software/script/chameleon_utils.py @@ -61,56 +61,68 @@ class ArgumentParserNoExit(argparse.ArgumentParser): args = {'prog': self.prog, 'message': message} raise ArgsParserError('%(prog)s: error: %(message)s\n' % args) - def print_help(self): +def print_help(self): """ Colorize argparse help """ print("-" * 80) print(color_string((CR, self.prog))) - lines = self.format_help().splitlines() - usage = lines[:lines.index('')] - assert usage[0].startswith('usage:') - usage[0] = usage[0].replace('usage:', f'{color_string((CG, "usage:"))}\n ') - usage[0] = usage[0].replace(self.prog, color_string((CR, self.prog))) - usage = [usage[0]] + [x[4:] for x in usage[1:]] + [''] - lines = lines[lines.index('')+1:] - desc = lines[:lines.index('')] - print(color_string((CC, "\n".join(desc)))) - print('\n'.join(usage)) - lines = lines[lines.index('')+1:] - if '' in lines: - options = lines[:lines.index('')] - lines = lines[lines.index('')+1:] - else: - options = lines - lines = [] - if len(options) > 0 and options[0].strip() == 'positional arguments:': - positional_args = options - positional_args[0] = positional_args[0].replace('positional arguments:', color_string((CG, "positional arguments:"))) - if len(positional_args) > 1: - positional_args.append('') - print('\n'.join(positional_args)) - if '' in lines: - options = lines[:lines.index('')] - lines = lines[lines.index('')+1:] - else: - options = lines + + # Get the help text and split it, filtering out leading empty lines + raw_lines = self.format_help().splitlines() + lines = [line for line in raw_lines if line.strip() or line == ''] + + # Find the usage block safely + usage_start = -1 + for i, line in enumerate(lines): + if line.strip().startswith('usage:'): + usage_start = i + break + + if usage_start != -1: + # We found a usage line, extract the block until the first empty line + try: + empty_after_usage = lines.index('', usage_start) + usage = lines[usage_start:empty_after_usage] + + # Apply coloring to the usage string + usage[0] = usage[0].replace('usage:', f'{color_string((CG, "usage:"))}\n ') + usage[0] = usage[0].replace(self.prog, color_string((CR, self.prog))) + + # Reformat indentation and print + usage_to_print = [usage[0]] + [x[4:] for x in usage[1:]] + [''] + print('\n'.join(usage_to_print)) + + # Advance lines pointer to after the usage block + lines = lines[empty_after_usage + 1:] + except ValueError: + # If no empty line found, just print what we have + print('\n'.join(lines[usage_start:])) lines = [] - if len(options) > 0: - # 2 variants depending on Python version(?) - assert options[0].strip() in ['options:', 'optional arguments:'] - options[0] = options[0].replace('options:', color_string((CG, "options:"))) - options[0] = options[0].replace('optional arguments:', color_string((CG, "optional arguments:"))) - if len(options) > 1: - options.append('') - print('\n'.join(options)) - if len(lines) > 0: - lines[0] = color_string((CG, lines[0])) - print('\n'.join(lines)) + + # Print description if available + if lines and lines[0].strip() != '': + try: + desc_end = lines.index('') + desc = lines[:desc_end] + print(color_string((CC, "\n".join(desc)))) + lines = lines[desc_end + 1:] + except ValueError: + pass + + # Handle options and positional arguments without crashing on strict matches + for line in lines: + clean_line = line.strip().lower() + if clean_line == 'positional arguments:': + print(color_string((CG, line))) + elif clean_line in ['options:', 'optional arguments:']: + print(color_string((CG, line))) + else: + print(line) + print('') self.help_requested = True - def print_mem_dump(bindata, blocksize): hexadecimal_len = blocksize*3+1 From 67c1c36212e3b72a815958754770981aa82a1418 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 7 Apr 2026 06:32:58 +0200 Subject: [PATCH 08/17] Clarify exit method behavior with comments Added comments to clarify behavior of exit method. --- software/script/chameleon_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/software/script/chameleon_utils.py b/software/script/chameleon_utils.py index 9a90051..0577c1a 100644 --- a/software/script/chameleon_utils.py +++ b/software/script/chameleon_utils.py @@ -56,6 +56,9 @@ class ArgumentParserNoExit(argparse.ArgumentParser): def exit(self, status: int = 0, message: Union[str, None] = None): if message: raise ParserExitIntercept(message) + # status=0 means help was printed; raise to stop argparse continuing + # to validate required args (which would cause a second print_help call) + raise ParserExitIntercept('') def error(self, message: str): args = {'prog': self.prog, 'message': message} From 350a774d7ce96ba02cee54d1b7887f5b1900f284 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 7 Apr 2026 10:47:41 +0200 Subject: [PATCH 09/17] align with RRG --- firmware/application/src/data_cmd.h | 2 +- software/script/chameleon_enum.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index b681d00..f9f00c6 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -106,7 +106,7 @@ #define DATA_CMD_IOPROX_WRITE_TO_T55XX (3011) #define DATA_CMD_IOPROX_DECODE_RAW (3012) #define DATA_CMD_IOPROX_COMPOSE_ID (3013) -#define DATA_CMD_LF_T55XX_WRITE (3014) +#define DATA_CMD_LF_T55XX_WRITE (3016) // // ****************************************************************** diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index 3e0cdb4..c402d52 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -92,7 +92,7 @@ class Command(enum.IntEnum): IOPROX_WRITE_TO_T55XX = 3011 IOPROX_DECODE_RAW = 3012 IOPROX_COMPOSE_ID = 3013 - LF_T55XX_WRITE = 3014 + LF_T55XX_WRITE = 3016 MF1_WRITE_EMU_BLOCK_DATA = 4000 HF14A_SET_ANTI_COLL_DATA = 4001 From e4dca3fcc4892e81d6d4ef999a4343dce8112e48 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 7 Apr 2026 10:57:08 +0200 Subject: [PATCH 10/17] align with RRG --- firmware/application/src/app_cmd.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 36b2a39..9f2991e 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -760,6 +760,8 @@ static data_frame_tx_t *cmd_processor_ioprox_write_to_t55xx(uint16_t cmd, uint16 return data_frame_make(cmd, status, 0, NULL); } + + /** * @brief Decode raw8 data to structured ioProx format * @param raw8 Input 8 bytes @@ -833,6 +835,21 @@ static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16 return data_frame_make(cmd, status, 0, NULL); } + +static data_frame_tx_t *cmd_processor_pac_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + typedef struct { + uint8_t id[LF_PAC_TAG_ID_SIZE]; + uint8_t new_key[4]; + uint8_t old_keys[4]; + } PACKED payload_t; + payload_t *payload = (payload_t *)data; + if (length < sizeof(payload_t) || (length - offsetof(payload_t, old_keys)) % sizeof(payload->old_keys) != 0) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + status = write_pac_to_t55xx(payload->id, payload->new_key, payload->old_keys, (length - offsetof(payload_t, old_keys)) / sizeof(payload->old_keys)); + return data_frame_make(cmd, status, 0, NULL); +} + static data_frame_tx_t *cmd_processor_lf_t55xx_write(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { typedef struct { uint8_t block; /* block number */ From 12284d5f71a4f1a24829a1adbdedbd10018b868f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Wed, 8 Apr 2026 12:36:12 +0200 Subject: [PATCH 11/17] Fix: emv scan truncation --- firmware/application/src/app_cmd.c | 105 ++++++++++++- software/script/chameleon_cli_unit.py | 202 +++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 10 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 9f2991e..1f2b28d 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -2110,7 +2110,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t /* ISO14443-4 chaining: PCB bit5 (0x20) set means more blocks follow */ while (resp_pcb & 0x20) { - uint8_t rack = 0xA2 | (blk_num & 0x01); /* R(ACK) */ + uint8_t rack = 0xA2 | (resp_pcb & 0x01); /* R(ACK) block_num matches received I-block */ uint8_t rack_frame[3]; rack_frame[0] = rack; crc_14a_append(rack_frame, 1); @@ -2135,6 +2135,81 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t return data_frame_make(cmd, STATUS_HF_TAG_OK, resp_chain_len, resp_chain); } +/* ----------------------------------------------------------------------- + * tcl_apdu_: ISO 14443-4 APDU helper used by cmd_processor_hf14a_4_emv_scan. + * Sends one I-block, receives full response handling card-side chaining. + * ----------------------------------------------------------------------- */ +static bool tcl_apdu_( + const uint8_t *apdu, uint8_t apdu_sz, + uint8_t **rdata_ptr, uint16_t *rlen_ptr, + uint8_t *abuf, uint8_t *rbuf, uint8_t *chain_buf, + uint16_t *rbits_p, uint8_t *blk_p) +{ + /* Build I-block: PCB + APDU + CRC */ + abuf[0] = 0x02 | (*blk_p & 0x01); + memcpy(&abuf[1], apdu, apdu_sz); + crc_14a_append(abuf, apdu_sz + 1); + uint8_t frame_len = apdu_sz + 3; /* PCB + APDU + CRC */ + + /* Clear stale RxIRq before transmit. + * bytes_transfer only clears ComIrqReg bit7 (Set1). + * RxIRq (bit4) stays set from the previous receive and causes the + * wait-loop to exit instantly, returning garbage from FIFO. */ + write_register_single(ComIrqReg, 0x7F); + pcd_14a_reader_timeout_set(600); + uint16_t rbits = 0; + uint8_t st = pcd_14a_reader_bytes_transfer( + PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 70u * 8u); + if (st != STATUS_HF_TAG_OK || rbits < 24u) return false; + + uint16_t rb = rbits / 8u; + uint8_t crc[2]; + crc_14a_calculate(rbuf, rb - 2u, crc); + if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) return false; + + *blk_p ^= 1; + uint8_t resp_pcb = rbuf[0]; + uint16_t chain_len = 0; + uint8_t dlen = (uint8_t)(rb - 3u); + if (dlen > 0 && dlen < 512u) { + memcpy(chain_buf, &rbuf[1], dlen); + chain_len = dlen; + } + + /* Handle card-side chaining ---------------------------------------- */ + while (resp_pcb & 0x20u) { + if ((resp_pcb & 0xC0u) != 0x00u) break; /* not an I-block */ + + /* R(ACK) block_num must match the received I-block's block_num */ + uint8_t rf[3]; + rf[0] = 0xA2u | (resp_pcb & 0x01u); + crc_14a_append(rf, 1); + + /* Use bytes_transfer for chain R(ACK) — clear stale RxIRq first */ + write_register_single(ComIrqReg, 0x7F); + pcd_14a_reader_timeout_set(600); + uint16_t chain_rbits = 0; + uint8_t chain_st = pcd_14a_reader_bytes_transfer( + PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 70u * 8u); + if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break; + rb = chain_rbits / 8u; /* bytes_transfer returns BIT count */ + + crc_14a_calculate(rbuf, rb - 2u, crc); + if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) break; + + resp_pcb = rbuf[0]; + dlen = (uint8_t)(rb - 3u); + if (dlen > 0u && chain_len + dlen < 512u) { + memcpy(&chain_buf[chain_len], &rbuf[1], dlen); + chain_len += dlen; + } + } + + *rdata_ptr = chain_buf; + *rlen_ptr = chain_len; + return chain_len > 0u; +} + /** * HF14A-4 EMV scan — complete EMV card read in a single firmware call. * @@ -2157,16 +2232,15 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st /* ---- helpers -------------------------------------------------- */ static uint8_t abuf[64]; /* TX frame: PCB + APDU + CRC */ - static uint8_t rbuf[64]; /* single-frame receive buffer (RC522 FIFO = 64 bytes) */ + static uint8_t rbuf[70]; /* single-frame receive buffer: FIFO(64) + PCB(1) + CRC(2) + slack */ static uint8_t chain_buf[512]; /* reassembled chained response */ uint16_t rbits; uint8_t blk = 0; /* alternating block number */ - /* Send one I-block APDU, handling ISO14443-4 response chaining. - * The RC522 FIFO is 64 bytes. If the card chains its response - * (PCB bit4=1), we send R(ACK) blocks and reassemble here. - * Returns pointer into chain_buf, sets *rlen_ptr to total length. */ - #define SEND_APDU(apdu_ptr, apdu_sz, rdata_ptr, rlen_ptr) ({ bool _ok = false; uint8_t _pcb = 0x02 | (blk & 0x01); abuf[0] = _pcb; memcpy(&abuf[1], (apdu_ptr), (apdu_sz)); rbits = 0; uint8_t _st = pcd_14a_reader_raw_cmd( false, true, true, false, true, false, 600, ((apdu_sz) + 1) * 8, abuf, rbuf, &rbits, sizeof(rbuf) * 8); if (_st == STATUS_HF_TAG_OK && rbits > 0) { uint16_t _rb = rbits; /* raw_cmd checkCrc=false returns byte count */ /* Verify and strip CRC manually (checkCrc=false above) */ if (_rb >= 3) { uint8_t _crc[2]; crc_14a_calculate(rbuf, _rb - 2, _crc); if (rbuf[_rb-2] == _crc[0] && rbuf[_rb-1] == _crc[1]) { blk ^= 1; uint16_t _chain_len = 0; uint8_t _resp_pcb = rbuf[0]; /* Copy data portion (strip PCB and CRC) */ uint8_t _dlen = _rb - 3; if (_dlen > 0 && _chain_len + _dlen < sizeof(chain_buf)) { memcpy(&chain_buf[_chain_len], &rbuf[1], _dlen); _chain_len += _dlen; } /* Handle chaining: PCB bit5=1 (b6 in ISO14443-4) means more data */ while (_resp_pcb & 0x20) { /* Send R(ACK) to request next block */ uint8_t _rack = 0xA2 | (blk & 0x01); abuf[0] = _rack; crc_14a_append(abuf, 1); rbits = 0; _st = pcd_14a_reader_bytes_transfer(PCD_TRANSCEIVE, abuf, 3, rbuf, &rbits, sizeof(rbuf) * 8); if (_st != STATUS_HF_TAG_OK || rbits < 24) break; _rb = rbits / 8; /* bytes_transfer returns bits */ crc_14a_calculate(rbuf, _rb - 2, _crc); if (rbuf[_rb-2] != _crc[0] || rbuf[_rb-1] != _crc[1]) break; blk ^= 1; _resp_pcb = rbuf[0]; _dlen = _rb - 3; if (_dlen > 0 && _chain_len + _dlen < sizeof(chain_buf)) { memcpy(&chain_buf[_chain_len], &rbuf[1], _dlen); _chain_len += _dlen; } } *(rdata_ptr) = chain_buf; *(rlen_ptr) = (_chain_len < sizeof(chain_buf) ? _chain_len : (uint16_t)(sizeof(chain_buf) - 1)); _ok = true; } } } _ok; }) + /* SEND_APDU: thin wrapper that calls the static tcl_apdu_ helper. */ + #define SEND_APDU(ap, asz, rd, rl) tcl_apdu_((ap),(asz),(rd),(rl),abuf,rbuf,chain_buf,&rbits,&blk) + + /* Append a cmd+resp pair to out buffer */ #define APPEND_PAIR(cmd_ptr, cmd_sz, resp_ptr, resp_sz) do { if (out_len + 1 + (cmd_sz) + 2 + (resp_sz) < NETDATA_MAX_DATA_LENGTH) { out[out_len++] = (uint8_t)(cmd_sz); memcpy(&out[out_len], (cmd_ptr), (cmd_sz)); out_len += (cmd_sz); out[out_len++] = (uint8_t)((resp_sz) & 0xFF); out[out_len++] = (uint8_t)((resp_sz) >> 8); memcpy(&out[out_len], (resp_ptr), (resp_sz)); out_len += (resp_sz); } } while(0) @@ -2243,6 +2317,23 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st APPEND_PAIR(ppse_cmd, sizeof(ppse_cmd), ppse_resp, ppse_rlen); num_apdus++; + /* ---- Re-establish T=CL after PPSE ---------------------------- + * The PPSE exchange leaves the RC522 in an unknown internal state. + * Rather than trying to clear it piecemeal, do a full reset: + * turn the field off briefly, rescan the card, re-run RATS. + * This guarantees a clean RC522 state before SELECT AID. + * blk resets to 0 because a new T=CL session starts after RATS. */ + pcd_14a_reader_antenna_off(); + bsp_delay_ms(10); + { + picc_14a_tag_t tag2; + pcd_14a_reader_reset(); + pcd_14a_reader_antenna_on(); + bsp_delay_ms(8); + if (pcd_14a_reader_scan_auto(&tag2) != STATUS_HF_TAG_OK) goto done; + } + blk = 0; /* new T=CL session: block number restarts at 0 */ + /* ---- Extract first AID from PPSE ----------------------------- */ uint8_t aid[16]; uint8_t aid_len = 0; for (uint8_t i = 0; i + 1 < ppse_rlen; i++) { diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index f62a5d2..4f70af2 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -8172,7 +8172,7 @@ class EMVScan(DeviceRequiredUnit): except Exception: time.sleep(0.3) - print(f' {CY}Scanning... (place card on antenna){C0}') + print(f' {CY}Scanning... (place card on antenna) [fw-canary:v5]{C0}') # Single firmware call — full EMV sequence without USB round-trips resp = cmd.hf14a_4_emv_scan() @@ -8284,6 +8284,204 @@ class EMVScan(DeviceRequiredUnit): 'Offline': '01', 'Data': tlv_to_dict(r_body)}) result['Application']['Records'] = records + # ---- Decode and display key card fields from EMV records -------- + def _pan_luhn(pan: str) -> bool: + digits = [int(c) for c in pan if c.isdigit()] + digits.reverse() + total = sum(d if i % 2 == 0 else (d * 2 - 9 if d * 2 > 9 else d * 2) + for i, d in enumerate(digits)) + return total % 10 == 0 + + def _find_tag_all(data: bytes, *tags: int): + """Recursively find all values for any of the given tags.""" + results = {} + for t in tags: + results[t] = [] + i = 0 + while i < len(data) - 1: + tl = 2 if (data[i] & 0x1F) == 0x1F else 1 + if i + tl > len(data): + break + cur_tag = int.from_bytes(data[i:i+tl], 'big') + i += tl + if i >= len(data): + break + if data[i] & 0x80: + nb = data[i] & 0x7F; i += 1 + vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb + else: + vlen = data[i]; i += 1 + val = data[i:i+vlen]; i += vlen + if cur_tag in results: + results[cur_tag].append(val) + # recurse into constructed TLV + if data[i - vlen - (1 if vlen < 128 else 2)] & 0x20 if False else (data[i - vlen - 1] & 0x20 if vlen < 128 else False): + sub = _find_tag_all(val, *tags) + for t in tags: + results[t].extend(sub[t]) + return results + + # Simpler recursive TLV walker + def tlv_find(data: bytes, *want_tags: int) -> dict: + found = {t: [] for t in want_tags} + i = 0 + while i < len(data): + if i + 1 >= len(data): + break + b0 = data[i] + tl = 2 if (b0 & 0x1F) == 0x1F else 1 + if i + tl > len(data): + break + tag = int.from_bytes(data[i:i+tl], 'big') + i += tl + if i >= len(data): + break + constructed = bool(b0 & 0x20) + if data[i] & 0x80: + nb = data[i] & 0x7F; i += 1 + if i + nb > len(data): + break + vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb + else: + vlen = data[i]; i += 1 + # For truncated TLV: read whatever bytes are available and + # continue parsing — don't break, so we can find tags inside + # truncated constructed TLV (e.g. 6F/A5 larger than received data) + truncated = (i + vlen > len(data)) + val = data[i:i+vlen] if not truncated else data[i:] + i = (i + vlen) if not truncated else len(data) + if tag in found and not truncated: + found[tag].append(val) + if constructed: + sub = tlv_find(val, *want_tags) + for t in want_tags: + found[t].extend(sub[t]) + return found + + # Collect all response bodies for tag search. + # tlv_to_dict stores the VALUE (content) of the outermost tag — + # so rec['Data']['value'] is already the unwrapped inner bytes. + all_record_data = b'' + for rec in result.get('Application', {}).get('Records', []): + raw_hex = rec.get('Data', {}).get('value', '') + try: + all_record_data += bytes.fromhex(raw_hex.replace(' ', '')) + except Exception: + pass + # Also include GPO and SELECT AID FCI values for label/name tags + extra_data = b'' + for key in ('GPO', 'FCITemplate'): + v = result.get('Application', {}).get(key, {}) + if isinstance(v, dict): + try: + extra_data += bytes.fromhex(v.get('value', '').replace(' ', '')) + except Exception: + pass + all_search_data = all_record_data + extra_data + + # EMV tag definitions: + # 0x5A = PAN + # 0x5F24 = Expiry Date (YYMMDD) + # 0x5F20 = Cardholder Name + # 0x5F28 = Issuer Country Code + # 0x8C / 0x8D = CDOL — skip + # 0x9F12 = Application Preferred Name + # 0x50 = Application Label + tags = tlv_find(all_record_data, 0x5A, 0x57, 0x5F24, 0x5F20, 0x5F28) + app_tags = tlv_find(all_search_data, 0x9F12, 0x50) + tags[0x9F12] = app_tags[0x9F12] + tags[0x50] = app_tags[0x50] + + print(f'') + print(f' {CG}── Card Details ──────────────────────{C0}') + + # App label + for v in tags.get(0x50, []): + try: + lbl = v.decode('ascii', errors='replace').strip() + if lbl: + print(f' {CG}App Label :{C0} {CY}{lbl}{C0}') + except Exception: + pass + + # PAN — prefer Track2 D-separator (authoritative, no padding ambiguity) + pan_hex = None + for v in tags.get(0x57, []): + t2 = v.hex().upper() + sep = t2.find('D') + if sep > 0: + pan_hex = t2[:sep] + break + if not pan_hex: + for v in tags.get(0x5A, []): + raw = v.hex().upper() + pan_hex = raw.rstrip('F') if raw.endswith('F') else raw + break + if pan_hex: + pan_fmt = ' '.join(pan_hex[i:i+4] for i in range(0, len(pan_hex), 4)) + luhn_ok = _pan_luhn(pan_hex) + luhn_str = f'{CG}✓{C0}' if luhn_ok else f'{CR}✗{C0}' + print(f' {CG}PAN :{C0} {CY}{pan_fmt}{C0} Luhn: {luhn_str}') + result.setdefault('Decoded', {})['PAN'] = pan_hex + else: + print(f' {CR}PAN : not found{C0}') + + # Expiry — 5F24 is 3 bytes BCD: YYMMDD + expiry_found = False + for v in tags.get(0x5F24, []): + if len(v) == 3: + exp = v.hex().upper() + exp_fmt = f'20{exp[0:2]}/{exp[2:4]}' + print(f' {CG}Expiry :{C0} {CY}{exp_fmt}{C0}') + result.setdefault('Decoded', {})['Expiry'] = exp_fmt + expiry_found = True + # Fallback: extract expiry from Track2 after D separator (YYMM) + if not expiry_found and pan_hex: + for v in tags.get(0x57, []): + t2 = v.hex().upper() + sep = t2.find('D') + if sep > 0 and len(t2) >= sep + 5: + yymm = t2[sep+1:sep+5] + if yymm.isdigit(): + exp_fmt = f'20{yymm[0:2]}/{yymm[2:4]}' + print(f' {CG}Expiry :{C0} {CY}{exp_fmt}{C0} (from Track2)') + result.setdefault('Decoded', {})['Expiry'] = exp_fmt + expiry_found = True + break + if not expiry_found: + print(f' {CR}Expiry : not found{C0}') + + # Cardholder Name (tag 5F20: printable ASCII only) + for v in tags[0x5F20]: + try: + if v and all(0x20 <= b <= 0x7E for b in v): + name = v.decode('ascii').strip() + if name: + print(f' {CG}Cardholder :{C0} {CY}{name}{C0}') + result.setdefault('Decoded', {})['CardholderName'] = name + except Exception: + pass + + # Issuer Country Code (ISO 3166-1 numeric, BCD) + for v in tags[0x5F28]: + country = v.hex().upper().lstrip('0') or '0' + print(f' {CG}Issuer Country:{C0} {CY}{country}{C0}') + result.setdefault('Decoded', {})['IssuerCountry'] = country + + # Application Preferred Name / Label + for v in tags[0x9F12]: + try: + print(f' {CG}App Name :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}') + except Exception: + pass + for v in tags[0x50]: + try: + print(f' {CG}App Label :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}') + except Exception: + pass + + print(f' {CG}──────────────────────────────────────{C0}') + json_str = jsonlib.dumps(result, indent=2) if args.file: try: @@ -8634,5 +8832,3 @@ class EMVApdu(DeviceRequiredUnit): break print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') - - From 63a465ce9b5efabebbc6499c6ed88e3909dccfec Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 12 Apr 2026 20:10:36 +0200 Subject: [PATCH 12/17] Fix argument parsing for 'fc' in ioprox --- software/script/chameleon_cli_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 204e56e..8b99832 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -6385,7 +6385,7 @@ class LFT55xxClone(ReaderRequiredUnit): elif t == "ioprox": ver = args.ver if args.ver is not None else 1 - fc = int(args.fc, 0) if args.fc is not None else 0 + fc = int(args.fc) if args.fc is not None else 0 cn = args.cn if args.cn is not None else 0 if args.raw8 is not None: raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8) From 0ce680b5c7381da7ba0983a02af8a719fa05ce41 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Mon, 13 Apr 2026 06:39:03 +0200 Subject: [PATCH 13/17] Refactor LF clone command and update usage examples --- software/script/chameleon_cli_unit.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 8b99832..5a43673 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -6244,18 +6244,20 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit): print(f" - Viking ID(8H): {id_hex} write done.") -@lf_t55xx.command("clone") +@lf.command("clone") class LFT55xxClone(ReaderRequiredUnit): """ - Clone a scanned or manually-specified LF card ID onto a blank T55xx tag. + Clone a LF card ID onto a blank T55xx tag. + + Usage: lf clone -t [args] Supported types and their required arguments: - em410x --id <10 hex> e.g. --id DEADBEEF88 - electra --id <26 hex> e.g. --id DEADBEEF880102030405060708 - hid -f --cn e.g. -f H10301 --fc 10 --cn 1234 + em410x --id <10 hex> e.g. lf clone -t em410x --id DEADBEEF88 + electra --id <26 hex> e.g. lf clone -t electra --id DEADBEEF880102030405060708 + hid -f --cn e.g. lf clone -t hid -f H10301 --fc 10 --cn 1234 ioprox --ver --fc --cn OR --raw8 <16 hex> - viking --id <8 hex> e.g. --id DEADBEEF + viking --id <8 hex> e.g. lf clone -t viking --id DEADBEEF Only supported on Chameleon Ultra (Lite has no LF writer). """ @@ -6266,6 +6268,7 @@ class LFT55xxClone(ReaderRequiredUnit): parser = ArgumentParserNoExit() parser.description = ( "Clone a LF card ID onto a blank T55xx tag.\n" + "Usage: lf clone -t [args]\n" "Supported types: em410x, electra, hid, ioprox, viking.\n" "Only supported on Chameleon Ultra (Lite has no LF writer)." ) @@ -6385,7 +6388,7 @@ class LFT55xxClone(ReaderRequiredUnit): elif t == "ioprox": ver = args.ver if args.ver is not None else 1 - fc = int(args.fc) if args.fc is not None else 0 + fc = args.fc if args.fc is not None else 0 cn = args.cn if args.cn is not None else 0 if args.raw8 is not None: raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8) From d70a0dd63f0fb586b46233d5bb7ce9ce19ec8de0 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 14 Apr 2026 09:32:35 +0200 Subject: [PATCH 14/17] fix hf14a sniff --- firmware/application/src/app_cmd.c | 133 +++++++-- software/script/chameleon_cli_unit.py | 373 +++++++------------------- 2 files changed, 218 insertions(+), 288 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 1f2b28d..322f928 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -2159,7 +2159,7 @@ static bool tcl_apdu_( pcd_14a_reader_timeout_set(600); uint16_t rbits = 0; uint8_t st = pcd_14a_reader_bytes_transfer( - PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 70u * 8u); + PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 270u * 8u); if (st != STATUS_HF_TAG_OK || rbits < 24u) return false; uint16_t rb = rbits / 8u; @@ -2177,8 +2177,39 @@ static bool tcl_apdu_( } /* Handle card-side chaining ---------------------------------------- */ + uint16_t chain_rbits = 0; /* hoisted: used in both WTX and R(ACK) paths */ + uint8_t chain_st = STATUS_HF_TAG_OK; while (resp_pcb & 0x20u) { - if ((resp_pcb & 0xC0u) != 0x00u) break; /* not an I-block */ + if ((resp_pcb & 0xC0u) != 0x00u) { + /* S-block: handle S(WTX), reject others. + * Some Visa/MC cards send WTX (PCB=0xF2) before their FCI, + * requesting more processing time. We must echo it back. + * The WTXM byte was spuriously added to chain_buf — undo it. */ + if ((resp_pcb & 0xF0u) == 0xF0u) { + chain_len -= dlen; /* remove spurious WTXM byte(s) */ + uint8_t wtx_r[4]; + wtx_r[0] = resp_pcb; /* mirror the S(WTX) PCB */ + wtx_r[1] = rbuf[1]; /* WTXM from last received frame */ + crc_14a_append(wtx_r, 2); + write_register_single(ComIrqReg, 0x7F); + pcd_14a_reader_timeout_set(600); + chain_rbits = 0; + chain_st = pcd_14a_reader_bytes_transfer( + PCD_TRANSCEIVE, wtx_r, 4, rbuf, &chain_rbits, 270u * 8u); + if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break; + rb = chain_rbits / 8u; + crc_14a_calculate(rbuf, rb - 2u, crc); + if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) break; + resp_pcb = rbuf[0]; + dlen = (uint8_t)(rb - 3u); + if (dlen > 0u && chain_len + dlen < 512u) { + memcpy(&chain_buf[chain_len], &rbuf[1], dlen); + chain_len += dlen; + } + continue; /* re-check while with new resp_pcb */ + } + break; /* other S-blocks (DESELECT etc.): stop */ + } /* R(ACK) block_num must match the received I-block's block_num */ uint8_t rf[3]; @@ -2188,9 +2219,9 @@ static bool tcl_apdu_( /* Use bytes_transfer for chain R(ACK) — clear stale RxIRq first */ write_register_single(ComIrqReg, 0x7F); pcd_14a_reader_timeout_set(600); - uint16_t chain_rbits = 0; - uint8_t chain_st = pcd_14a_reader_bytes_transfer( - PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 70u * 8u); + chain_rbits = 0; + chain_st = pcd_14a_reader_bytes_transfer( + PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 270u * 8u); if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break; rb = chain_rbits / 8u; /* bytes_transfer returns BIT count */ @@ -2232,7 +2263,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st /* ---- helpers -------------------------------------------------- */ static uint8_t abuf[64]; /* TX frame: PCB + APDU + CRC */ - static uint8_t rbuf[70]; /* single-frame receive buffer: FIFO(64) + PCB(1) + CRC(2) + slack */ + static uint8_t rbuf[270]; /* single-frame receive buffer: up to 256 bytes data + PCB + CRC + slack (FSDI=8 → FSD=256) */ static uint8_t chain_buf[512]; /* reassembled chained response */ uint16_t rbits; uint8_t blk = 0; /* alternating block number */ @@ -2359,27 +2390,95 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st APPEND_PAIR(sel_cmd, sel_len, sel_resp, sel_rlen); num_apdus++; - /* ---- Step 4: GPO -------------------------------------------- */ - static const uint8_t gpo_cmd[] = {0x80, 0xA8, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00}; - uint8_t *gpo_resp; uint16_t gpo_rlen; - if (!SEND_APDU(gpo_cmd, sizeof(gpo_cmd), &gpo_resp, &gpo_rlen)) goto done; - APPEND_PAIR(gpo_cmd, sizeof(gpo_cmd), gpo_resp, gpo_rlen); + /* ---- Step 4: GPO — parse PDOL from SELECT AID FCI, fill zeros ------- */ + /* PDOL is tag 9F38 in the FCI (sel_resp). Parse it to know how many + * bytes the card expects. Fill all fields with zeros (offline scan). */ + uint8_t pdol_len = 0; + for (uint8_t pi = 0; pi + 2 < sel_rlen; pi++) { + /* 2-byte tag detection: first byte has bits[4:0] == 0x1F */ + uint8_t ptag1 = sel_resp[pi]; + uint8_t ptag2 = (((ptag1 & 0x1F) == 0x1F) && pi+1 < sel_rlen) ? sel_resp[pi+1] : 0; + uint16_t ftag = ((ptag1 & 0x1F) == 0x1F) ? (((uint16_t)ptag1<<8)|ptag2) : ptag1; + uint8_t flen_off = (((ptag1 & 0x1F) == 0x1F)) ? 2 : 1; + if (pi + flen_off >= sel_rlen) break; + uint8_t flen = sel_resp[pi + flen_off]; + if (ftag == 0x9F38) { + /* Sum DOL field lengths to get total PDOL data size */ + uint8_t di = pi + flen_off + 1; + uint8_t dend = di + flen; + while (di < dend && di + 1 < sel_rlen) { + uint8_t dol_tl = ((sel_resp[di] & 0x1F) == 0x1F) ? 2 : 1; + if (di + dol_tl >= sel_rlen) break; + pdol_len += sel_resp[di + dol_tl]; + di += dol_tl + 1; + } + break; + } + if (flen_off + flen < 255) pi += flen_off + flen - 1; else break; + } + /* ---- Step 5: GPO with PDOL retry -------------------------------- + * When the FCI is truncated (45b for long cards), we can't read the + * full PDOL. Try progressively larger PDOL sizes (all zeros) until the + * card accepts. pdol_len from FCI parsing is tried first. Common sizes + * cover most Mastercard/Visa variants. */ + static const uint8_t gpo_try_pl[] = {0, 4, 8, 12, 18, 22, 26, 29, 34, 38, 44}; + uint8_t gpo_buf[8 + 44]; /* PCB(1)+header(7)+max_PDOL(44)+Le(1)+CRC(2) fits abuf[64] */ + uint8_t gpo_len = 0; + uint8_t *gpo_resp = NULL; uint16_t gpo_rlen = 0; + if (pdol_len > 44) pdol_len = 0; /* clamp to abuf-safe size */ + { + bool gpo_ok = false; + for (uint8_t ti = 0; ti <= sizeof(gpo_try_pl) && !gpo_ok; ti++) { + uint8_t pl = (ti == 0) ? pdol_len + : gpo_try_pl[ti - 1]; + /* skip sizes we already tried */ + bool dup = false; + for (uint8_t si = 0; si < ti && !dup; si++) + dup = ((si == 0 ? pdol_len : gpo_try_pl[si-1]) == pl); + if (dup) continue; + gpo_len = 0; + gpo_buf[gpo_len++]=0x80; gpo_buf[gpo_len++]=0xA8; + gpo_buf[gpo_len++]=0x00; gpo_buf[gpo_len++]=0x00; + gpo_buf[gpo_len++]=(uint8_t)(pl + 2); /* Lc */ + gpo_buf[gpo_len++]=0x83; + gpo_buf[gpo_len++]=pl; + memset(&gpo_buf[gpo_len], 0x00, pl); gpo_len += pl; + gpo_buf[gpo_len++]=0x00; /* Le */ + if (!SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen)) continue; + /* Accept if response template (0x77/0x80) or SW 9000 */ + if (gpo_rlen >= 2) { + uint8_t s1 = gpo_resp[gpo_rlen-2]; + uint8_t s2 = gpo_resp[gpo_rlen-1]; + if (gpo_resp[0] == 0x77 || gpo_resp[0] == 0x80 || + (s1 == 0x90 && s2 == 0x00)) + gpo_ok = true; + } + } + if (!gpo_ok) goto done; + } + APPEND_PAIR(gpo_buf, gpo_len, gpo_resp, gpo_rlen); num_apdus++; - - /* ---- Step 5: parse AFL and READ RECORDs --------------------- */ - /* Find AFL in GPO response (tag 0x94 in format 2, or bytes 3+ in format 1) */ uint8_t *afl = NULL; uint8_t afl_len = 0; if (gpo_rlen > 0 && gpo_resp[0] == 0x77) { - /* Format 2: search for tag 94 */ + /* Format 2: find tag 94 */ for (uint8_t i = 2; i + 1 < gpo_rlen; ) { uint8_t t = gpo_resp[i]; uint8_t l = gpo_resp[i+1]; if (t == 0x94) { afl = &gpo_resp[i+2]; afl_len = l; break; } i += 2 + l; } } else if (gpo_rlen > 3 && gpo_resp[0] == 0x80) { - /* Format 1: skip tag(1)+len(1)+AIP(2) */ - afl = &gpo_resp[3]; afl_len = gpo_rlen - 3 - 2; /* -2 for SW */ + afl = &gpo_resp[3]; afl_len = gpo_rlen - 3 - 2; } + if (afl == NULL || afl_len == 0) goto done; + + /* Copy AFL to local buffer before READ RECORDs. + * afl points into chain_buf which is overwritten by each SEND_APDU call. + * Without this copy, the 2nd+ AFL entries become garbage after the first + * READ RECORD, causing last=0xFF and up to 255 timeout loops per entry. */ + static uint8_t afl_buf[32]; /* max 8 AFL entries × 4 bytes */ + if (afl_len > sizeof(afl_buf)) afl_len = (uint8_t)sizeof(afl_buf); + memcpy(afl_buf, afl, afl_len); + afl = afl_buf; /* READ each record */ for (uint8_t a = 0; a + 3 < afl_len; a += 4) { diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 5a43673..75113c6 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -105,8 +105,8 @@ def check_tools(): if missing_tools: missing_tool_str = ", ".join(missing_tools) - warn_str = f"Warning, {missing_tool_str} not found. Corresponding commands will not work as intended." - print(color_string((CR, warn_str))) + warn_str = f"Note: optional Mifare tools not found: {missing_tool_str}. Mifare attack commands will not work." + print(color_string((CY, warn_str))) class BaseCLIUnit: @@ -5556,242 +5556,7 @@ class HFMFUEDetect(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): print(f"{actual_index:3d}: {color_string((CY, password.upper()))}") -@hf_mfu.command('nfcimport') -class HFMFUNfcImport(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): - # Mapping from Flipper Zero device type strings to CU TagSpecificType - FLIPPER_TYPE_MAP = { - 'NTAG203': TagSpecificType.NTAG_215, # best-effort: no native NTAG203 support - 'NTAG210': TagSpecificType.NTAG_210, - 'NTAG212': TagSpecificType.NTAG_212, - 'NTAG213': TagSpecificType.NTAG_213, - 'NTAG215': TagSpecificType.NTAG_215, - 'NTAG216': TagSpecificType.NTAG_216, - 'NTAGI2C1K': TagSpecificType.NTAG_216, # best-effort - 'NTAGI2C2K': TagSpecificType.NTAG_216, # best-effort - 'NTAGI2CPlus1K': TagSpecificType.NTAG_216, # best-effort - 'NTAGI2CPlus2K': TagSpecificType.NTAG_216, # best-effort - 'Mifare Ultralight': TagSpecificType.MF0ICU1, - 'Mifare Ultralight C': TagSpecificType.MF0ICU2, - 'Mifare Ultralight 11': TagSpecificType.MF0UL11, - 'Mifare Ultralight 21': TagSpecificType.MF0UL21, - # "Mifare Ultralight EV1" is disambiguated by page count in on_exec - } - - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = 'Import a Flipper Zero .nfc file into a MIFARE Ultralight / NTAG emulator slot' - self.add_slot_args(parser) - parser.add_argument('-f', '--file', required=True, type=str, help="Path to Flipper Zero .nfc file") - parser.add_argument('--amiibo', action='store_true', default=False, - help="Derive and write correct PWD/PACK for amiibo (NTAG215)") - return parser - - def on_exec(self, args: argparse.Namespace): - file_path = args.file - file_name = os.path.basename(file_path) - - # --- Parse the .nfc file --- - try: - with open(file_path, 'r') as f: - lines = f.readlines() - except FileNotFoundError: - print(color_string((CR, f"File not found: {file_path}"))) - return - except OSError as e: - print(color_string((CR, f"Error reading file: {e}"))) - return - - device_type = None - uid = None - atqa = None - sak = None - signature = None - version = None - counters = {} - tearing = {} - pages_total = None - pages = {} - - for line in lines: - line = line.strip() - if line.startswith('#') or not line: - continue - - if line.startswith('Device type:'): - device_type = line.split(':', 1)[1].strip() - elif line.startswith('UID:'): - uid = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', '')) - elif line.startswith('ATQA:'): - atqa = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', '')) - elif line.startswith('SAK:'): - sak = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', '')) - elif line.startswith('Signature:'): - signature = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', '')) - elif line.startswith('Mifare version:'): - version = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', '')) - elif line.startswith('Counter '): - match = re.match(r'Counter\s+(\d+):\s+(\d+)', line) - if match: - counters[int(match.group(1))] = int(match.group(2)) - elif line.startswith('Tearing '): - match = re.match(r'Tearing\s+(\d+):\s+([0-9A-Fa-f]+)', line) - if match: - tearing[int(match.group(1))] = int(match.group(2), 16) - elif line.startswith('Pages total:'): - pages_total = int(line.split(':', 1)[1].strip()) - elif line.startswith('Page '): - match = re.match(r'Page\s+(\d+):\s+(.*)', line) - if match: - page_num = int(match.group(1)) - page_data = bytes.fromhex(match.group(2).strip().replace(' ', '')) - pages[page_num] = page_data - - # --- Validate required fields --- - if device_type is None: - print(color_string((CR, "No 'Device type' found in .nfc file."))) - return - if uid is None: - print(color_string((CR, "No 'UID' found in .nfc file."))) - return - if atqa is None: - print(color_string((CR, "No 'ATQA' found in .nfc file."))) - return - if sak is None: - print(color_string((CR, "No 'SAK' found in .nfc file."))) - return - - # --- Map device type to TagSpecificType --- - tag_type = self.FLIPPER_TYPE_MAP.get(device_type) - - if tag_type is None and device_type.startswith('Mifare Ultralight EV1'): - # Disambiguate EV1 by page count - nr = pages_total if pages_total else len(pages) - tag_type = TagSpecificType.MF0UL11 if nr <= 20 else TagSpecificType.MF0UL21 - - if tag_type is None: - print(color_string((CR, f"Unsupported Flipper device type: '{device_type}'"))) - print(f" Supported types: {', '.join(sorted(self.FLIPPER_TYPE_MAP.keys()))}, Mifare Ultralight EV1") - return - - # --- Print summary --- - print(f"Importing Flipper NFC file: {file_name}") - print(f" Device type: {device_type} -> {tag_type}") - print(f" UID: {uid.hex(' ').upper()}") - print(f" ATQA: {atqa.hex(' ').upper()} SAK: {sak.hex().upper()}") - if version: - print(f" Version: {version.hex(' ').upper()}") - if signature: - print(f" Signature: {signature.hex(' ').upper()}") - if counters: - print(f" Counters: {', '.join(str(counters.get(i, 0)) for i in range(max(counters.keys()) + 1))}") - nr_pages = pages_total if pages_total else len(pages) - print(f" Pages: {nr_pages}") - print() - - # --- Step 1: Set slot tag type --- - print(f"Setting slot {self.slot_num} tag type to {tag_type}...") - self.cmd.set_slot_tag_type(self.slot_num, tag_type) - self.cmd.set_slot_data_default(self.slot_num, tag_type) - # Must re-activate slot after changing type so subsequent commands target the new type - self.cmd.set_active_slot(self.slot_num) - - # --- Step 2: Set anti-collision data --- - print("Setting anti-collision data...") - self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak) - - # --- Step 3: Set version data --- - if version and len(version) == 8: - print("Setting version data...") - try: - self.cmd.mf0_ntag_set_version_data(version) - except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): - print(color_string((CY, " Warning: tag type does not support GET_VERSION."))) - - # --- Step 4: Set signature data --- - if signature and len(signature) == 32: - print("Setting signature data...") - try: - self.cmd.mf0_ntag_set_signature_data(signature) - except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): - print(color_string((CY, " Warning: tag type does not support READ_SIG."))) - - # --- Step 5: Set counter and tearing data --- - if counters: - print("Setting counter data...") - # NTAG types have a single counter accessed via NFC at index 2, - # but stored at firmware internal index 0 - ntag_types = { - TagSpecificType.NTAG_210, TagSpecificType.NTAG_212, - TagSpecificType.NTAG_213, TagSpecificType.NTAG_215, - TagSpecificType.NTAG_216, - } - for i in sorted(counters.keys()): - value = counters[i] - if value > 0xFFFFFF: - print(color_string((CY, f" Warning: counter {i} value {value:#x} exceeds 24-bit, skipping."))) - continue - # Map Flipper counter index to firmware internal index - if tag_type in ntag_types: - if i != 2: - continue # NTAG only has counter at NFC index 2 - fw_index = 0 - else: - fw_index = i - # Reset tearing flag if tearing byte is BD (default / no tearing) - tearing_val = tearing.get(i, 0x00) - reset_tearing = (tearing_val == 0xBD or tearing_val == 0x00) - try: - self.cmd.mfu_write_emu_counter_data(fw_index, value, reset_tearing) - except (ValueError, chameleon_com.CMDInvalidException, UnexpectedResponseError, TimeoutError): - print(color_string((CY, f" Warning: could not set counter {i}."))) - - # --- Step 6: Write page data --- - if pages: - # Get total pages for the configured slot - slot_pages = self.cmd.mfu_get_emu_pages_count() - - # Build contiguous data from parsed pages - max_page = max(pages.keys()) - write_pages = min(max_page + 1, slot_pages) - - print(f"Writing {write_pages} pages...", end=' ', flush=True) - - page = 0 - while page < write_pages: - cur_count = min(16, write_pages - page) - batch = bytearray() - for p in range(page, page + cur_count): - batch.extend(pages.get(p, b'\x00\x00\x00\x00')) - self.cmd.mfu_write_emu_page_data(page, bytes(batch)) - page += cur_count - - print("done") - - # --- Step 7: Derive and write amiibo PWD/PACK --- - if args.amiibo: - if tag_type != TagSpecificType.NTAG_215: - print(color_string((CY, f" Warning: --amiibo flag ignored (tag type is {tag_type}, not NTAG 215)."))) - elif uid is None or len(uid) != 7: - print(color_string((CY, " Warning: --amiibo flag ignored (UID is not 7 bytes)."))) - else: - pwd = bytes([ - 0xAA ^ uid[1] ^ uid[3], - 0x55 ^ uid[2] ^ uid[4], - 0xAA ^ uid[3] ^ uid[5], - 0x55 ^ uid[4] ^ uid[6], - ]) - pack = bytes([0x80, 0x80, 0x00, 0x00]) - print(f"Setting amiibo PWD: {pwd.hex(' ').upper()}, PACK: {pack[:2].hex(' ').upper()}...") - self.cmd.mfu_write_emu_page_data(133, pwd) - self.cmd.mfu_write_emu_page_data(134, pack) - - self.cmd.set_slot_enable(self.slot_num, TagSenseType.HF, True) - - print() - print(f" - Import complete. Slot {self.slot_num} is now emulating {device_type} ({file_name})") - - -@lf_em_410x.command('read') +@lf_em_410x.command("read") class LFEMRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: parser = ArgumentParserNoExit() @@ -6244,20 +6009,18 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit): print(f" - Viking ID(8H): {id_hex} write done.") -@lf.command("clone") +@lf_t55xx.command("clone") class LFT55xxClone(ReaderRequiredUnit): """ - Clone a LF card ID onto a blank T55xx tag. - - Usage: lf clone -t [args] + Clone a scanned or manually-specified LF card ID onto a blank T55xx tag. Supported types and their required arguments: - em410x --id <10 hex> e.g. lf clone -t em410x --id DEADBEEF88 - electra --id <26 hex> e.g. lf clone -t electra --id DEADBEEF880102030405060708 - hid -f --cn e.g. lf clone -t hid -f H10301 --fc 10 --cn 1234 + em410x --id <10 hex> e.g. --id DEADBEEF88 + electra --id <26 hex> e.g. --id DEADBEEF880102030405060708 + hid -f --cn e.g. -f H10301 --fc 10 --cn 1234 ioprox --ver --fc --cn OR --raw8 <16 hex> - viking --id <8 hex> e.g. lf clone -t viking --id DEADBEEF + viking --id <8 hex> e.g. --id DEADBEEF Only supported on Chameleon Ultra (Lite has no LF writer). """ @@ -6268,7 +6031,6 @@ class LFT55xxClone(ReaderRequiredUnit): parser = ArgumentParserNoExit() parser.description = ( "Clone a LF card ID onto a blank T55xx tag.\n" - "Usage: lf clone -t [args]\n" "Supported types: em410x, electra, hid, ioprox, viking.\n" "Only supported on Chameleon Ultra (Lite has no LF writer)." ) @@ -6388,7 +6150,7 @@ class LFT55xxClone(ReaderRequiredUnit): elif t == "ioprox": ver = args.ver if args.ver is not None else 1 - fc = args.fc if args.fc is not None else 0 + fc = int(args.fc, 0) if args.fc is not None else 0 cn = args.cn if args.cn is not None else 0 if args.raw8 is not None: raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8) @@ -7552,12 +7314,16 @@ class HF14ASniff(BaseCLIUnit): return # Parse packed frame buffer: [2 bytes bits BE][N bytes data] ... + # Bit 15 of szBits: 0 = reader→card, 1 = card→reader (new firmware). + # Old firmware always sends bit15=0; parser is backward compatible. buf = bytes(resp.data) - frames = [] + frames = [] # (szBits, data, is_tx) i = 0 while i + 2 <= len(buf): - szBits = (buf[i] << 8) | buf[i+1] + 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 @@ -7586,29 +7352,83 @@ class HF14ASniff(BaseCLIUnit): else: data = raw - frames.append((szBits, data)) + frames.append((szBits, data, is_tx)) if not frames: print(f"{CR}No frames decoded{C0}") return - print(f" Captured : {CG}{len(frames)}{C0} frame(s)") + rx_count = sum(1 for _, _, tx in frames if not tx) + tx_count = sum(1 for _, _, tx in frames if tx) + if tx_count > 0: + print(f" Captured : {CG}{len(frames)}{C0} frame(s) " + f"({CY}{rx_count}{C0} reader→card {CG}{tx_count}{C0} card→reader)") + else: + print(f" Captured : {CG}{len(frames)}{C0} frame(s) " + f"{CY}(reader→card only — reflash for both directions){C0}") print() - print(f" {'#':>3} {'bits':>4} {'hex data':<42} decoded") - print(f" {'---':>3} {'----':>4} {'-'*42} {'-'*35}") + print(f" {'#':>3} {'dir':<3} {'bits':>4} {'hex data':<42} decoded") + print(f" {'---':>3} {'---':<3} {'----':>4} {'-'*42} {'-'*35}") - for n, (szBits, data) in enumerate(frames): - hex_str = ' '.join(f'{b:02x}' for b in data) - decoded, col = _decode_14a_frame_col(data, szBits) - print(f" {CY}{n+1:>3}{C0} {szBits:>4} {hex_str:<42} {col}{decoded}{C0}") + for n, (szBits, data, is_tx) in enumerate(frames): + hex_str = ' '.join(f'{b:02x}' for b in data) + decoded, col = _decode_14a_frame_col(data, szBits) + 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}") - # Summary block + # Summary block (pass only reader→card frames for protocol decode) print() - _print_14a_sniff_summary(frames) + _print_14a_sniff_summary([(s, d) for s, d, tx in frames if not tx]) + +def _decode_sw(sw1: int, sw2: int) -> str: + """Decode an ISO 7816-4 status word pair.""" + exact = { + 0x9000: 'OK', + 0x6100: 'Response bytes available', + 0x6283: 'File deactivated', + 0x6300: 'Auth failed', + 0x6400: 'No changes', + 0x6581: 'Memory failure', + 0x6700: 'Wrong length', + 0x6881: 'Logical channel not supported', + 0x6882: 'Secure messaging not supported', + 0x6900: 'Command not allowed', + 0x6981: 'Command incompatible with file structure', + 0x6982: 'Security status not satisfied', + 0x6983: 'Auth method blocked', + 0x6984: 'Referenced data invalidated', + 0x6985: 'Conditions of use not satisfied', + 0x6986: 'Command not allowed — no EF selected', + 0x6A00: 'Wrong parameters P1-P2', + 0x6A80: 'Incorrect data in command', + 0x6A81: 'Function not supported', + 0x6A82: 'File not found', + 0x6A83: 'Record not found', + 0x6A84: 'Not enough memory', + 0x6A85: 'Lc inconsistent with TLV', + 0x6A86: 'Incorrect parameters P1-P2', + 0x6A87: 'Lc inconsistent with P1-P2', + 0x6A88: 'Referenced data not found', + 0x6B00: 'Wrong parameters P1-P2', + 0x6D00: 'Instruction not supported', + 0x6E00: 'Class not supported', + 0x6F00: 'Unknown error', + } + key = (sw1 << 8) | sw2 + if key in exact: + return exact[key] + if sw1 == 0x61: return f'Response bytes available: {sw2}' + if sw1 == 0x62: return f'Warning — no info change: {sw2:02X}' + if sw1 == 0x63: return f'Warning — state changed: {sw2:02X}' + if sw1 == 0x6C: return f'Wrong Le — use {sw2}' + if sw1 == 0x90: return 'OK' + if sw1 == 0x91: return 'Proprietary OK' + return '' + def _decode_14a_frame_col(data: bytes, szBits: int): """Return (description, colour) for a 14A frame.""" if not data: @@ -7731,6 +7551,18 @@ def _decode_14a_frame_col(data: bytes, szBits: int): return 'MANAGE CHANNEL', CC return f'APDU CLA={cla:02x} INS={ins:02x} P1={p1:02x} P2={p2:02x}', CY + # ISO 7816-4 status word — scan last 2 bytes (and last 4 if CRC present) + sw_label = '' + for sw_offset in (-2, -4): + if len(data) >= abs(sw_offset): + s1, s2 = data[sw_offset], data[sw_offset + 1] + lbl = _decode_sw(s1, s2) + if lbl: + sw_label = f'SW {s1:02X} {s2:02X} {lbl}' + break + if sw_label: + return sw_label, CY + # Unknown — show first byte return f'unknown (0x{b0:02x})', CC @@ -8633,11 +8465,13 @@ class EMVScan(DeviceRequiredUnit): print(f'') print(f' {CG}── Card Details ──────────────────────{C0}') - # App label - for v in tags.get(0x50, []): + # App label — show first unique label only + seen_labels = set() + for v in tags.get(0x50, []) + app_tags.get(0x50, []): try: lbl = v.decode('ascii', errors='replace').strip() - if lbl: + if lbl and lbl not in seen_labels: + seen_labels.add(lbl) print(f' {CG}App Label :{C0} {CY}{lbl}{C0}') except Exception: pass @@ -8706,15 +8540,12 @@ class EMVScan(DeviceRequiredUnit): print(f' {CG}Issuer Country:{C0} {CY}{country}{C0}') result.setdefault('Decoded', {})['IssuerCountry'] = country - # Application Preferred Name / Label + # Application Preferred Name (9F12) — only if different from label for v in tags[0x9F12]: try: - print(f' {CG}App Name :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}') - except Exception: - pass - for v in tags[0x50]: - try: - print(f' {CG}App Label :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}') + name = v.decode('ascii', errors='replace').strip() + if name and name not in seen_labels: + print(f' {CG}App Name :{C0} {CY}{name}{C0}') except Exception: pass @@ -9069,4 +8900,4 @@ class EMVApdu(DeviceRequiredUnit): print(f' {CR}Error sending response: {e}{C0}') break - print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') + print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') \ No newline at end of file From 76c961ed595e4b415ab12bb0641c61c512da5c01 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 14 Apr 2026 09:45:02 +0200 Subject: [PATCH 15/17] Added Ultra/Lite guard --- software/script/chameleon_cli_unit.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 75113c6..466aecb 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -6009,7 +6009,7 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit): print(f" - Viking ID(8H): {id_hex} write done.") -@lf_t55xx.command("clone") +@lf.command("clone") class LFT55xxClone(ReaderRequiredUnit): """ Clone a scanned or manually-specified LF card ID onto a blank T55xx tag. @@ -6105,6 +6105,10 @@ class LFT55xxClone(ReaderRequiredUnit): return parser def on_exec(self, args: argparse.Namespace): + # Clone requires LF writer — only available on Chameleon Ultra (not Lite) + if self.cmd.get_device_model() != 0: + print(f" - Error: LF clone requires Chameleon Ultra. Lite has no LF writer.") + return t = args.type if t in ("em410x", "electra"): @@ -8900,4 +8904,4 @@ class EMVApdu(DeviceRequiredUnit): print(f' {CR}Error sending response: {e}{C0}') break - print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') \ No newline at end of file + print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}') From 378c2b302fb97b71cdc0de92205956d0e4c6f272 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Wed, 15 Apr 2026 06:29:10 +0200 Subject: [PATCH 16/17] Various bug fixes --- firmware/application/src/app_cmd.c | 152 ++++++++++++------ .../application/src/rfid/nfctag/hf/nfc_14a.c | 53 +++++- .../application/src/rfid/nfctag/hf/nfc_14a.h | 13 ++ 3 files changed, 166 insertions(+), 52 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 322f928..ade7f54 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -21,6 +21,12 @@ #include "rc522.h" #endif #include "nfc_14a.h" +/* Forward declarations for functions added to nfc_14a.c/h in this PR. + * These are declared here to avoid build failure if nfc_14a.h is not yet + * updated on the build system. */ +extern void nfc_tag_14a_set_tx_sniff_cb(void (*cb)(const uint8_t *, uint16_t)); +extern void nfc_tag_14a_clear_tx_sniff_cb(void); +extern void nfc_tag_14a_set_sniff_passive(bool passive); #include "nfc_14a_4.h" #define NRF_LOG_MODULE_NAME app_cmd @@ -1860,18 +1866,30 @@ static uint16_t m_sniff_buf_len = 0; static bool m_sniff_active = false; static uint16_t m_sniff_cb_count = 0; /* debug: total callback invocations */ +/* Encode one frame into m_sniff_buf. + * Format: [szBits_be16][data...] + * Bit 15 of szBits: 0 = reader→card (RX), 1 = card→reader (TX). + * Real szBits always < 512 so bit15 is always free in genuine frames. + * Old parsers (bit15=0 for all frames) still work correctly. */ +static void hf14a_sniff_store(const uint8_t *data, uint16_t szBits, bool is_tx) { + uint16_t szBytes = (szBits + 7) / 8; + if (m_sniff_buf_len + 2 + szBytes > HF_SNIFF_BUF_SIZE) return; + uint16_t hdr = szBits | (is_tx ? 0x8000u : 0x0000u); + m_sniff_buf[m_sniff_buf_len++] = (hdr >> 8) & 0xFF; + m_sniff_buf[m_sniff_buf_len++] = hdr & 0xFF; + memcpy(&m_sniff_buf[m_sniff_buf_len], data, szBytes); + m_sniff_buf_len += szBytes; +} + static void hf14a_sniff_frame_cb(const uint8_t *data, uint16_t szBits) { m_sniff_cb_count++; /* count even if buffer full or inactive */ if (!m_sniff_active) return; - uint16_t szBytes = (szBits + 7) / 8; - /* Check space: 2 bytes header + data */ - if (m_sniff_buf_len + 2 + szBytes > HF_SNIFF_BUF_SIZE) return; - /* Write bit count big-endian */ - m_sniff_buf[m_sniff_buf_len++] = (szBits >> 8) & 0xFF; - m_sniff_buf[m_sniff_buf_len++] = szBits & 0xFF; - /* Write frame bytes */ - memcpy(&m_sniff_buf[m_sniff_buf_len], data, szBytes); - m_sniff_buf_len += szBytes; + hf14a_sniff_store(data, szBits, false); /* reader→card */ +} + +static void hf14a_sniff_tx_frame_cb(const uint8_t *data, uint16_t szBits) { + if (!m_sniff_active) return; + hf14a_sniff_store(data, szBits, true); /* card→reader */ } static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { @@ -1901,7 +1919,10 @@ static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, m_sniff_buf_len = 0; m_sniff_cb_count = 0; m_sniff_active = true; + /* passive mode intentionally disabled: CU acts as the card so it must + * respond normally to the reader (ATQA/UID/SAK). TX sniff captures responses. */ nfc_tag_14a_set_sniff_cb(hf14a_sniff_frame_cb); + nfc_tag_14a_set_tx_sniff_cb(hf14a_sniff_tx_frame_cb); /* Wait for duration, yielding each ms so USB stack stays alive. * Feed watchdog every iteration — WDT timeout is 5000ms and the @@ -1915,7 +1936,9 @@ static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, /* Remove callback and restore normal sense state */ m_sniff_active = false; + /* (passive mode was not enabled, nothing to restore) */ nfc_tag_14a_clear_sniff_cb(); + nfc_tag_14a_clear_tx_sniff_cb(); tag_emulation_sense_run(); /* restore slot-based sense state */ if (m_sniff_buf_len == 0) { @@ -2416,46 +2439,85 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st } if (flen_off + flen < 255) pi += flen_off + flen - 1; else break; } - /* ---- Step 5: GPO with PDOL retry -------------------------------- - * When the FCI is truncated (45b for long cards), we can't read the - * full PDOL. Try progressively larger PDOL sizes (all zeros) until the - * card accepts. pdol_len from FCI parsing is tried first. Common sizes - * cover most Mastercard/Visa variants. */ - static const uint8_t gpo_try_pl[] = {0, 4, 8, 12, 18, 22, 26, 29, 34, 38, 44}; - uint8_t gpo_buf[8 + 44]; /* PCB(1)+header(7)+max_PDOL(44)+Le(1)+CRC(2) fits abuf[64] */ + /* ---- Step 5: GPO ----------------------------------------------- + * Build GPO from parsed PDOL. pdol_len from FCI may be 0 if truncated. + * BUILD_GPO fills the PDOL data: TTQ = A0 00 00 00 for first 4 bytes + * (MSD+EMV contactless, offline, no DDA), rest zeros. + * TTQ=A0000000 is the lowest-security POS profile; Mastercard and Visa + * contactless cards respond to it even without a full terminal setup. */ + static uint8_t gpo_buf[8 + 44]; /* static: keep off stack */ + /* PDOL template: TTQ first 4 bytes, zeros after. + * TTQ A0000000: MSD+EMV contactless capable, offline, no CDA/DDA. */ + static const uint8_t gpo_pdol_template[44] = { + 0xA0, 0x00, 0x00, 0x00, /* TTQ (9F66): MSD+cEMV, offline, no DDA */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Amount Authorised (9F02) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Amount Other (9F03) */ + 0x02, 0x08, /* Country Code (9F1A): Norway */ + 0x00, 0x00, 0x00, 0x00, 0x00, /* TVR (95) */ + 0x09, 0x78, /* Currency (5F2A): EUR */ + 0x25, 0x01, 0x01, /* Date (9A) */ + 0x00, /* Transaction Type (9C) */ + 0x00, 0x00, 0x00, 0x00, /* Unpredictable Number (9F37) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* extra zeros */ + 0x00, 0x00, 0x00 + }; uint8_t gpo_len = 0; uint8_t *gpo_resp = NULL; uint16_t gpo_rlen = 0; - if (pdol_len > 44) pdol_len = 0; /* clamp to abuf-safe size */ - { - bool gpo_ok = false; - for (uint8_t ti = 0; ti <= sizeof(gpo_try_pl) && !gpo_ok; ti++) { - uint8_t pl = (ti == 0) ? pdol_len - : gpo_try_pl[ti - 1]; - /* skip sizes we already tried */ - bool dup = false; - for (uint8_t si = 0; si < ti && !dup; si++) - dup = ((si == 0 ? pdol_len : gpo_try_pl[si-1]) == pl); - if (dup) continue; - gpo_len = 0; - gpo_buf[gpo_len++]=0x80; gpo_buf[gpo_len++]=0xA8; - gpo_buf[gpo_len++]=0x00; gpo_buf[gpo_len++]=0x00; - gpo_buf[gpo_len++]=(uint8_t)(pl + 2); /* Lc */ - gpo_buf[gpo_len++]=0x83; - gpo_buf[gpo_len++]=pl; - memset(&gpo_buf[gpo_len], 0x00, pl); gpo_len += pl; - gpo_buf[gpo_len++]=0x00; /* Le */ - if (!SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen)) continue; - /* Accept if response template (0x77/0x80) or SW 9000 */ - if (gpo_rlen >= 2) { - uint8_t s1 = gpo_resp[gpo_rlen-2]; - uint8_t s2 = gpo_resp[gpo_rlen-1]; - if (gpo_resp[0] == 0x77 || gpo_resp[0] == 0x80 || - (s1 == 0x90 && s2 == 0x00)) - gpo_ok = true; - } + if (pdol_len > 44) pdol_len = 0; + #define BUILD_GPO(pl) do { \ + gpo_len = 0; \ + gpo_buf[gpo_len++]=0x80; gpo_buf[gpo_len++]=0xA8; \ + gpo_buf[gpo_len++]=0x00; gpo_buf[gpo_len++]=0x00; \ + gpo_buf[gpo_len++]=(uint8_t)((pl)+2); \ + gpo_buf[gpo_len++]=0x83; gpo_buf[gpo_len++]=(pl); \ + memcpy(&gpo_buf[gpo_len], gpo_pdol_template, \ + (pl) <= sizeof(gpo_pdol_template) ? (pl) : sizeof(gpo_pdol_template)); \ + if ((pl) > sizeof(gpo_pdol_template)) \ + memset(&gpo_buf[gpo_len + sizeof(gpo_pdol_template)], 0, \ + (pl) - sizeof(gpo_pdol_template)); \ + gpo_len += (pl); \ + gpo_buf[gpo_len++]=0x00; \ + } while(0) + #define GPO_OK(rp,rl) ((rl)>=2 && \ + ((rp)[0]==0x77||(rp)[0]==0x80|| \ + ((rp)[(rl)-2]==0x90&&(rp)[(rl)-1]==0x00))) + /* Attempt 1: use PDOL length from FCI (may be 0 if truncated) */ + BUILD_GPO(pdol_len); + if (!SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen) || + !GPO_OK(gpo_resp, gpo_rlen)) { + /* Attempt 2: try 4 bytes (TTQ only — some Visa/MC accept this) */ + if (pdol_len != 4) { + BUILD_GPO(4); + if (SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen) && + GPO_OK(gpo_resp, gpo_rlen)) goto gpo_done; } - if (!gpo_ok) goto done; + /* Attempt 3: try 29 bytes (common Mastercard/Visa PDOL size) */ + if (pdol_len != 29) { + BUILD_GPO(29); + if (SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen) && + GPO_OK(gpo_resp, gpo_rlen)) goto gpo_done; + } + /* Attempt 4: try 33 bytes (MC with Amount+Country+Currency fields) */ + if (pdol_len != 33) { + BUILD_GPO(33); + if (SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen) && + GPO_OK(gpo_resp, gpo_rlen)) goto gpo_done; + } + /* Attempts 5-7: additional MC sizes (+TermType, +DataAuth, +IssuerAppData) */ + { static const uint8_t extra_pl[] = {34, 36, 38}; + for (uint8_t ei = 0; ei < sizeof(extra_pl); ei++) { + uint8_t pl = extra_pl[ei]; + if (pl == pdol_len || pl == 4 || pl == 29 || pl == 33) continue; + BUILD_GPO(pl); + if (SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen) && + GPO_OK(gpo_resp, gpo_rlen)) goto gpo_done; + } + } + goto done; /* all attempts failed */ } + gpo_done: + #undef BUILD_GPO + #undef GPO_OK APPEND_PAIR(gpo_buf, gpo_len, gpo_resp, gpo_rlen); num_apdus++; uint8_t *afl = NULL; uint8_t afl_len = 0; diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c index 50aa32b..c7b9ca6 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c @@ -70,6 +70,25 @@ void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb) { void nfc_tag_14a_clear_sniff_cb(void) { m_sniff_cb = NULL; } + +/* TX sniff: captures card→reader frames at TX_FRAMESTART */ +static nfc_tag_14a_tx_sniff_cb_t m_tx_sniff_cb = NULL; + +void nfc_tag_14a_set_tx_sniff_cb(nfc_tag_14a_tx_sniff_cb_t cb) { + m_tx_sniff_cb = cb; +} + +void nfc_tag_14a_clear_tx_sniff_cb(void) { + m_tx_sniff_cb = NULL; +} + +/* Passive sniff mode: suppress all tag TX responses so the CU does not + * participate in anticollision and avoids colliding with the real card. */ +static bool m_sniff_passive = false; + +void nfc_tag_14a_set_sniff_passive(bool passive) { + m_sniff_passive = passive; +} static uint8_t m_nfc_tx_buffer[MAX_NFC_TX_BUFFER_SIZE] = { 0x00 }; // The N -secondary connection needs to use SAK, when the "third 'bit' in SAK is 1 is 1, the logo UID is incomplete static uint8_t m_uid_incomplete_sak[] = { 0x04, 0xda, 0x17 }; @@ -366,9 +385,11 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { if (auto_coll_res != NULL) { // The status machine is set to the preparation state, and the next operation is to enter the card selection link m_tag_state_14a = NFC_TAG_STATE_14A_READY; - // After receiving the WUPA or REQA instruction, we need to reply to ATQA - nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false); - // NRF_LOG_INFO("ATQA reply: %02x%02x", auto_coll_res->atqa[0], auto_coll_res->atqa[1]); + if (!m_sniff_passive) { + // After receiving the WUPA or REQA instruction, we need to reply to ATQA + nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false); + // NRF_LOG_INFO("ATQA reply: %02x%02x", auto_coll_res->atqa[0], auto_coll_res->atqa[1]); + } } else { m_tag_state_14a = NFC_TAG_STATE_14A_IDLE; NRF_LOG_INFO("Auto anti-collision resource no exists."); @@ -484,7 +505,9 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { } // Incoming SELECT ALL for any cascade level if (szDataBits == 16 && p_data[1] == 0x20) { - nfc_tag_14a_tx_bytes(uid, 5, false); + if (!m_sniff_passive) { + nfc_tag_14a_tx_bytes(uid, 5, false); + } // NRF_LOG_INFO("[MFEMUL_SELECT] SEL Reply."); break; } @@ -498,10 +521,14 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { if (cl_finished) { // NRF_LOG_INFO("[MFEMUL_SELECT] m_tag_state_14a = MFEMUL_WORK"); m_tag_state_14a = NFC_TAG_STATE_14A_ACTIVE; - nfc_tag_14a_tx_bytes(auto_coll_res->sak, 1, true); + if (!m_sniff_passive) { + nfc_tag_14a_tx_bytes(auto_coll_res->sak, 1, true); + } } else { // It is necessary to continue the level, so we need to respond to a data that marks the incomplete UID in SAK - nfc_tag_14a_tx_bytes(m_uid_incomplete_sak, 3, false); + if (!m_sniff_passive) { + nfc_tag_14a_tx_bytes(m_uid_incomplete_sak, 3, false); + } } } else { // IDLE, not our UID @@ -651,7 +678,19 @@ void nfc_tag_14a_event_callback(nrfx_nfct_evt_t const *p_event) { } case NRFX_NFCT_EVT_TX_FRAMESTART: { // NRF_LOG_INFO("TX start.\n"); - // NRF_LOG_INFO("TX config is %d.\n", nrf_nfct_tx_frame_config_get(NRF_NFCT)); + if (m_tx_sniff_cb != NULL) { + uint32_t amt = NRF_NFCT->TXD.AMOUNT; + uint16_t tx_bytes = (amt >> NFCT_TXD_AMOUNT_TXDATABYTES_Pos) + & (NFCT_TXD_AMOUNT_TXDATABYTES_Msk >> NFCT_TXD_AMOUNT_TXDATABYTES_Pos); + uint16_t tx_bits_rem = (amt >> NFCT_TXD_AMOUNT_TXDATABITS_Pos) + & (NFCT_TXD_AMOUNT_TXDATABITS_Msk >> NFCT_TXD_AMOUNT_TXDATABITS_Pos); + uint16_t tx_bits = (tx_bits_rem > 0) + ? ((tx_bytes - 1) * 8 + tx_bits_rem) + : (tx_bytes * 8); + if (tx_bits > 0 && tx_bytes <= MAX_NFC_TX_BUFFER_SIZE) { + m_tx_sniff_cb(m_nfc_tx_buffer, tx_bits); + } + } break; } case NRFX_NFCT_EVT_TX_FRAMEEND: { diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h index cd1702b..7bbf7fb 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h @@ -90,6 +90,19 @@ typedef void (*nfc_tag_14a_sniff_cb_t)(const uint8_t *data, uint16_t szBits); void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb); void nfc_tag_14a_clear_sniff_cb(void); + +/* TX sniff callback — fires at TX_FRAMESTART with the frame the tag is about + * to send (card→reader direction). Same signature as the RX sniff callback. + * Install alongside nfc_tag_14a_set_sniff_cb() to capture both directions. */ +typedef void (*nfc_tag_14a_tx_sniff_cb_t)(const uint8_t *data, uint16_t szBits); + +void nfc_tag_14a_set_tx_sniff_cb(nfc_tag_14a_tx_sniff_cb_t cb); +void nfc_tag_14a_clear_tx_sniff_cb(void); + +/* Passive sniff mode: when true, suppresses all CU anticollision responses + * (ATQA, UID, SAK) so the CU does not collide with real cards in the field. + * Enable before starting a sniff session, disable on completion. */ +void nfc_tag_14a_set_sniff_passive(bool passive); typedef void (*nfc_tag_14a_state_handler_t)(uint8_t *data, uint16_t szBits); typedef nfc_tag_14a_coll_res_reference_t *(*nfc_tag_14a_coll_handler_t)(void); From 4406788aef3fda1b909bd030bcbe908171d5af15 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Wed, 15 Apr 2026 14:45:41 +0200 Subject: [PATCH 17/17] BUG: reverted bug that was reintroduced --- software/script/chameleon_cli_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 466aecb..f2a2db3 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -6154,7 +6154,7 @@ class LFT55xxClone(ReaderRequiredUnit): elif t == "ioprox": ver = args.ver if args.ver is not None else 1 - fc = int(args.fc, 0) if args.fc is not None else 0 + fc = int(args.fc) if args.fc is not None else 0 cn = args.cn if args.cn is not None else 0 if args.raw8 is not None: raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8)