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):