mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-10-23 04:21:52 +00:00
protocol ISO 14443-4 and emv scan, loading json file from PM3rdv4
This commit is contained in:
1 parent
93c1e150ab
commit
efa2ea2c7b
13 files changed
+1593
-9
No files matched your search
@@ -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 \
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <string.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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='<path>',
|
||||
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='<path>',
|
||||
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='<hex>',
|
||||
help='Command APDU prefix to match (hex)')
|
||||
parser.add_argument('--resp', default='', metavar='<hex>',
|
||||
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='<ms>',
|
||||
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}')
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user