Skip to content

Commit 12284d5

Browse files
committed
Fix: emv scan truncation
1 parent e4dca3f commit 12284d5

File tree

2 files changed

+297
-10
lines changed

2 files changed

+297
-10
lines changed

firmware/application/src/app_cmd.c

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,7 +2110,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t
21102110

21112111
/* ISO14443-4 chaining: PCB bit5 (0x20) set means more blocks follow */
21122112
while (resp_pcb & 0x20) {
2113-
uint8_t rack = 0xA2 | (blk_num & 0x01); /* R(ACK) */
2113+
uint8_t rack = 0xA2 | (resp_pcb & 0x01); /* R(ACK) block_num matches received I-block */
21142114
uint8_t rack_frame[3];
21152115
rack_frame[0] = rack;
21162116
crc_14a_append(rack_frame, 1);
@@ -2135,6 +2135,81 @@ static data_frame_tx_t *cmd_processor_hf14a_4_reader_apdu(uint16_t cmd, uint16_t
21352135
return data_frame_make(cmd, STATUS_HF_TAG_OK, resp_chain_len, resp_chain);
21362136
}
21372137

2138+
/* -----------------------------------------------------------------------
2139+
* tcl_apdu_: ISO 14443-4 APDU helper used by cmd_processor_hf14a_4_emv_scan.
2140+
* Sends one I-block, receives full response handling card-side chaining.
2141+
* ----------------------------------------------------------------------- */
2142+
static bool tcl_apdu_(
2143+
const uint8_t *apdu, uint8_t apdu_sz,
2144+
uint8_t **rdata_ptr, uint16_t *rlen_ptr,
2145+
uint8_t *abuf, uint8_t *rbuf, uint8_t *chain_buf,
2146+
uint16_t *rbits_p, uint8_t *blk_p)
2147+
{
2148+
/* Build I-block: PCB + APDU + CRC */
2149+
abuf[0] = 0x02 | (*blk_p & 0x01);
2150+
memcpy(&abuf[1], apdu, apdu_sz);
2151+
crc_14a_append(abuf, apdu_sz + 1);
2152+
uint8_t frame_len = apdu_sz + 3; /* PCB + APDU + CRC */
2153+
2154+
/* Clear stale RxIRq before transmit.
2155+
* bytes_transfer only clears ComIrqReg bit7 (Set1).
2156+
* RxIRq (bit4) stays set from the previous receive and causes the
2157+
* wait-loop to exit instantly, returning garbage from FIFO. */
2158+
write_register_single(ComIrqReg, 0x7F);
2159+
pcd_14a_reader_timeout_set(600);
2160+
uint16_t rbits = 0;
2161+
uint8_t st = pcd_14a_reader_bytes_transfer(
2162+
PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 70u * 8u);
2163+
if (st != STATUS_HF_TAG_OK || rbits < 24u) return false;
2164+
2165+
uint16_t rb = rbits / 8u;
2166+
uint8_t crc[2];
2167+
crc_14a_calculate(rbuf, rb - 2u, crc);
2168+
if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) return false;
2169+
2170+
*blk_p ^= 1;
2171+
uint8_t resp_pcb = rbuf[0];
2172+
uint16_t chain_len = 0;
2173+
uint8_t dlen = (uint8_t)(rb - 3u);
2174+
if (dlen > 0 && dlen < 512u) {
2175+
memcpy(chain_buf, &rbuf[1], dlen);
2176+
chain_len = dlen;
2177+
}
2178+
2179+
/* Handle card-side chaining ---------------------------------------- */
2180+
while (resp_pcb & 0x20u) {
2181+
if ((resp_pcb & 0xC0u) != 0x00u) break; /* not an I-block */
2182+
2183+
/* R(ACK) block_num must match the received I-block's block_num */
2184+
uint8_t rf[3];
2185+
rf[0] = 0xA2u | (resp_pcb & 0x01u);
2186+
crc_14a_append(rf, 1);
2187+
2188+
/* Use bytes_transfer for chain R(ACK) — clear stale RxIRq first */
2189+
write_register_single(ComIrqReg, 0x7F);
2190+
pcd_14a_reader_timeout_set(600);
2191+
uint16_t chain_rbits = 0;
2192+
uint8_t chain_st = pcd_14a_reader_bytes_transfer(
2193+
PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 70u * 8u);
2194+
if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break;
2195+
rb = chain_rbits / 8u; /* bytes_transfer returns BIT count */
2196+
2197+
crc_14a_calculate(rbuf, rb - 2u, crc);
2198+
if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) break;
2199+
2200+
resp_pcb = rbuf[0];
2201+
dlen = (uint8_t)(rb - 3u);
2202+
if (dlen > 0u && chain_len + dlen < 512u) {
2203+
memcpy(&chain_buf[chain_len], &rbuf[1], dlen);
2204+
chain_len += dlen;
2205+
}
2206+
}
2207+
2208+
*rdata_ptr = chain_buf;
2209+
*rlen_ptr = chain_len;
2210+
return chain_len > 0u;
2211+
}
2212+
21382213
/**
21392214
* HF14A-4 EMV scan — complete EMV card read in a single firmware call.
21402215
*
@@ -2157,16 +2232,15 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st
21572232

21582233
/* ---- helpers -------------------------------------------------- */
21592234
static uint8_t abuf[64]; /* TX frame: PCB + APDU + CRC */
2160-
static uint8_t rbuf[64]; /* single-frame receive buffer (RC522 FIFO = 64 bytes) */
2235+
static uint8_t rbuf[70]; /* single-frame receive buffer: FIFO(64) + PCB(1) + CRC(2) + slack */
21612236
static uint8_t chain_buf[512]; /* reassembled chained response */
21622237
uint16_t rbits;
21632238
uint8_t blk = 0; /* alternating block number */
21642239

2165-
/* Send one I-block APDU, handling ISO14443-4 response chaining.
2166-
* The RC522 FIFO is 64 bytes. If the card chains its response
2167-
* (PCB bit4=1), we send R(ACK) blocks and reassemble here.
2168-
* Returns pointer into chain_buf, sets *rlen_ptr to total length. */
2169-
#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; })
2240+
/* SEND_APDU: thin wrapper that calls the static tcl_apdu_ helper. */
2241+
#define SEND_APDU(ap, asz, rd, rl) tcl_apdu_((ap),(asz),(rd),(rl),abuf,rbuf,chain_buf,&rbits,&blk)
2242+
2243+
21702244

21712245
/* Append a cmd+resp pair to out buffer */
21722246
#define APPEND_PAIR(cmd_ptr, cmd_sz, resp_ptr, resp_sz) do { if (out_len + 1 + (cmd_sz) + 2 + (resp_sz) < NETDATA_MAX_DATA_LENGTH) { out[out_len++] = (uint8_t)(cmd_sz); memcpy(&out[out_len], (cmd_ptr), (cmd_sz)); out_len += (cmd_sz); out[out_len++] = (uint8_t)((resp_sz) & 0xFF); out[out_len++] = (uint8_t)((resp_sz) >> 8); memcpy(&out[out_len], (resp_ptr), (resp_sz)); out_len += (resp_sz); } } while(0)
@@ -2243,6 +2317,23 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st
22432317
APPEND_PAIR(ppse_cmd, sizeof(ppse_cmd), ppse_resp, ppse_rlen);
22442318
num_apdus++;
22452319

2320+
/* ---- Re-establish T=CL after PPSE ----------------------------
2321+
* The PPSE exchange leaves the RC522 in an unknown internal state.
2322+
* Rather than trying to clear it piecemeal, do a full reset:
2323+
* turn the field off briefly, rescan the card, re-run RATS.
2324+
* This guarantees a clean RC522 state before SELECT AID.
2325+
* blk resets to 0 because a new T=CL session starts after RATS. */
2326+
pcd_14a_reader_antenna_off();
2327+
bsp_delay_ms(10);
2328+
{
2329+
picc_14a_tag_t tag2;
2330+
pcd_14a_reader_reset();
2331+
pcd_14a_reader_antenna_on();
2332+
bsp_delay_ms(8);
2333+
if (pcd_14a_reader_scan_auto(&tag2) != STATUS_HF_TAG_OK) goto done;
2334+
}
2335+
blk = 0; /* new T=CL session: block number restarts at 0 */
2336+
22462337
/* ---- Extract first AID from PPSE ----------------------------- */
22472338
uint8_t aid[16]; uint8_t aid_len = 0;
22482339
for (uint8_t i = 0; i + 1 < ppse_rlen; i++) {

software/script/chameleon_cli_unit.py

Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8172,7 +8172,7 @@ def on_exec(self, args: argparse.Namespace):
81728172
except Exception:
81738173
time.sleep(0.3)
81748174

8175-
print(f' {CY}Scanning... (place card on antenna){C0}')
8175+
print(f' {CY}Scanning... (place card on antenna) [fw-canary:v5]{C0}')
81768176

81778177
# Single firmware call — full EMV sequence without USB round-trips
81788178
resp = cmd.hf14a_4_emv_scan()
@@ -8284,6 +8284,204 @@ def find_tag(data, tag):
82848284
'Offline': '01', 'Data': tlv_to_dict(r_body)})
82858285
result['Application']['Records'] = records
82868286

8287+
# ---- Decode and display key card fields from EMV records --------
8288+
def _pan_luhn(pan: str) -> bool:
8289+
digits = [int(c) for c in pan if c.isdigit()]
8290+
digits.reverse()
8291+
total = sum(d if i % 2 == 0 else (d * 2 - 9 if d * 2 > 9 else d * 2)
8292+
for i, d in enumerate(digits))
8293+
return total % 10 == 0
8294+
8295+
def _find_tag_all(data: bytes, *tags: int):
8296+
"""Recursively find all values for any of the given tags."""
8297+
results = {}
8298+
for t in tags:
8299+
results[t] = []
8300+
i = 0
8301+
while i < len(data) - 1:
8302+
tl = 2 if (data[i] & 0x1F) == 0x1F else 1
8303+
if i + tl > len(data):
8304+
break
8305+
cur_tag = int.from_bytes(data[i:i+tl], 'big')
8306+
i += tl
8307+
if i >= len(data):
8308+
break
8309+
if data[i] & 0x80:
8310+
nb = data[i] & 0x7F; i += 1
8311+
vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb
8312+
else:
8313+
vlen = data[i]; i += 1
8314+
val = data[i:i+vlen]; i += vlen
8315+
if cur_tag in results:
8316+
results[cur_tag].append(val)
8317+
# recurse into constructed TLV
8318+
if data[i - vlen - (1 if vlen < 128 else 2)] & 0x20 if False else (data[i - vlen - 1] & 0x20 if vlen < 128 else False):
8319+
sub = _find_tag_all(val, *tags)
8320+
for t in tags:
8321+
results[t].extend(sub[t])
8322+
return results
8323+
8324+
# Simpler recursive TLV walker
8325+
def tlv_find(data: bytes, *want_tags: int) -> dict:
8326+
found = {t: [] for t in want_tags}
8327+
i = 0
8328+
while i < len(data):
8329+
if i + 1 >= len(data):
8330+
break
8331+
b0 = data[i]
8332+
tl = 2 if (b0 & 0x1F) == 0x1F else 1
8333+
if i + tl > len(data):
8334+
break
8335+
tag = int.from_bytes(data[i:i+tl], 'big')
8336+
i += tl
8337+
if i >= len(data):
8338+
break
8339+
constructed = bool(b0 & 0x20)
8340+
if data[i] & 0x80:
8341+
nb = data[i] & 0x7F; i += 1
8342+
if i + nb > len(data):
8343+
break
8344+
vlen = int.from_bytes(data[i:i+nb], 'big'); i += nb
8345+
else:
8346+
vlen = data[i]; i += 1
8347+
# For truncated TLV: read whatever bytes are available and
8348+
# continue parsing — don't break, so we can find tags inside
8349+
# truncated constructed TLV (e.g. 6F/A5 larger than received data)
8350+
truncated = (i + vlen > len(data))
8351+
val = data[i:i+vlen] if not truncated else data[i:]
8352+
i = (i + vlen) if not truncated else len(data)
8353+
if tag in found and not truncated:
8354+
found[tag].append(val)
8355+
if constructed:
8356+
sub = tlv_find(val, *want_tags)
8357+
for t in want_tags:
8358+
found[t].extend(sub[t])
8359+
return found
8360+
8361+
# Collect all response bodies for tag search.
8362+
# tlv_to_dict stores the VALUE (content) of the outermost tag —
8363+
# so rec['Data']['value'] is already the unwrapped inner bytes.
8364+
all_record_data = b''
8365+
for rec in result.get('Application', {}).get('Records', []):
8366+
raw_hex = rec.get('Data', {}).get('value', '')
8367+
try:
8368+
all_record_data += bytes.fromhex(raw_hex.replace(' ', ''))
8369+
except Exception:
8370+
pass
8371+
# Also include GPO and SELECT AID FCI values for label/name tags
8372+
extra_data = b''
8373+
for key in ('GPO', 'FCITemplate'):
8374+
v = result.get('Application', {}).get(key, {})
8375+
if isinstance(v, dict):
8376+
try:
8377+
extra_data += bytes.fromhex(v.get('value', '').replace(' ', ''))
8378+
except Exception:
8379+
pass
8380+
all_search_data = all_record_data + extra_data
8381+
8382+
# EMV tag definitions:
8383+
# 0x5A = PAN
8384+
# 0x5F24 = Expiry Date (YYMMDD)
8385+
# 0x5F20 = Cardholder Name
8386+
# 0x5F28 = Issuer Country Code
8387+
# 0x8C / 0x8D = CDOL — skip
8388+
# 0x9F12 = Application Preferred Name
8389+
# 0x50 = Application Label
8390+
tags = tlv_find(all_record_data, 0x5A, 0x57, 0x5F24, 0x5F20, 0x5F28)
8391+
app_tags = tlv_find(all_search_data, 0x9F12, 0x50)
8392+
tags[0x9F12] = app_tags[0x9F12]
8393+
tags[0x50] = app_tags[0x50]
8394+
8395+
print(f'')
8396+
print(f' {CG}── Card Details ──────────────────────{C0}')
8397+
8398+
# App label
8399+
for v in tags.get(0x50, []):
8400+
try:
8401+
lbl = v.decode('ascii', errors='replace').strip()
8402+
if lbl:
8403+
print(f' {CG}App Label :{C0} {CY}{lbl}{C0}')
8404+
except Exception:
8405+
pass
8406+
8407+
# PAN — prefer Track2 D-separator (authoritative, no padding ambiguity)
8408+
pan_hex = None
8409+
for v in tags.get(0x57, []):
8410+
t2 = v.hex().upper()
8411+
sep = t2.find('D')
8412+
if sep > 0:
8413+
pan_hex = t2[:sep]
8414+
break
8415+
if not pan_hex:
8416+
for v in tags.get(0x5A, []):
8417+
raw = v.hex().upper()
8418+
pan_hex = raw.rstrip('F') if raw.endswith('F') else raw
8419+
break
8420+
if pan_hex:
8421+
pan_fmt = ' '.join(pan_hex[i:i+4] for i in range(0, len(pan_hex), 4))
8422+
luhn_ok = _pan_luhn(pan_hex)
8423+
luhn_str = f'{CG}{C0}' if luhn_ok else f'{CR}{C0}'
8424+
print(f' {CG}PAN :{C0} {CY}{pan_fmt}{C0} Luhn: {luhn_str}')
8425+
result.setdefault('Decoded', {})['PAN'] = pan_hex
8426+
else:
8427+
print(f' {CR}PAN : not found{C0}')
8428+
8429+
# Expiry — 5F24 is 3 bytes BCD: YYMMDD
8430+
expiry_found = False
8431+
for v in tags.get(0x5F24, []):
8432+
if len(v) == 3:
8433+
exp = v.hex().upper()
8434+
exp_fmt = f'20{exp[0:2]}/{exp[2:4]}'
8435+
print(f' {CG}Expiry :{C0} {CY}{exp_fmt}{C0}')
8436+
result.setdefault('Decoded', {})['Expiry'] = exp_fmt
8437+
expiry_found = True
8438+
# Fallback: extract expiry from Track2 after D separator (YYMM)
8439+
if not expiry_found and pan_hex:
8440+
for v in tags.get(0x57, []):
8441+
t2 = v.hex().upper()
8442+
sep = t2.find('D')
8443+
if sep > 0 and len(t2) >= sep + 5:
8444+
yymm = t2[sep+1:sep+5]
8445+
if yymm.isdigit():
8446+
exp_fmt = f'20{yymm[0:2]}/{yymm[2:4]}'
8447+
print(f' {CG}Expiry :{C0} {CY}{exp_fmt}{C0} (from Track2)')
8448+
result.setdefault('Decoded', {})['Expiry'] = exp_fmt
8449+
expiry_found = True
8450+
break
8451+
if not expiry_found:
8452+
print(f' {CR}Expiry : not found{C0}')
8453+
8454+
# Cardholder Name (tag 5F20: printable ASCII only)
8455+
for v in tags[0x5F20]:
8456+
try:
8457+
if v and all(0x20 <= b <= 0x7E for b in v):
8458+
name = v.decode('ascii').strip()
8459+
if name:
8460+
print(f' {CG}Cardholder :{C0} {CY}{name}{C0}')
8461+
result.setdefault('Decoded', {})['CardholderName'] = name
8462+
except Exception:
8463+
pass
8464+
8465+
# Issuer Country Code (ISO 3166-1 numeric, BCD)
8466+
for v in tags[0x5F28]:
8467+
country = v.hex().upper().lstrip('0') or '0'
8468+
print(f' {CG}Issuer Country:{C0} {CY}{country}{C0}')
8469+
result.setdefault('Decoded', {})['IssuerCountry'] = country
8470+
8471+
# Application Preferred Name / Label
8472+
for v in tags[0x9F12]:
8473+
try:
8474+
print(f' {CG}App Name :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}')
8475+
except Exception:
8476+
pass
8477+
for v in tags[0x50]:
8478+
try:
8479+
print(f' {CG}App Label :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}')
8480+
except Exception:
8481+
pass
8482+
8483+
print(f' {CG}──────────────────────────────────────{C0}')
8484+
82878485
json_str = jsonlib.dumps(result, indent=2)
82888486
if args.file:
82898487
try:
@@ -8634,5 +8832,3 @@ def on_exec(self, args: argparse.Namespace):
86348832
break
86358833

86368834
print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}')
8637-
8638-

0 commit comments

Comments
 (0)