mirror of
https://github.com/bettse/seader.git
synced 2026-09-01 20:08:26 +00:00
440 lines
14 KiB
C
440 lines
14 KiB
C
#include "sam_reader.h"
|
|
|
|
#include "seader_i.h"
|
|
#include "seader_worker_i.h"
|
|
#include "t_1.h"
|
|
#include "usb_ccid_reader.h"
|
|
#include "trace_log.h"
|
|
|
|
#include <storage/storage.h>
|
|
#include <flipper_format/flipper_format.h>
|
|
|
|
static bool seader_reader_relay_once(
|
|
Seader* seader,
|
|
SeaderReader* reader,
|
|
const uint8_t* apdu,
|
|
uint16_t apdu_len,
|
|
uint8_t* out,
|
|
uint16_t out_cap,
|
|
uint16_t* out_len,
|
|
uint32_t timeout_ms);
|
|
|
|
#define TAG "SeaderReader"
|
|
|
|
#define SEADER_READER_CFG_PATH STORAGE_APP_DATA_PATH_PREFIX "/usb_reader.conf"
|
|
static const char* seader_reader_cfg_header = "Seader USB Reader";
|
|
static const uint32_t seader_reader_cfg_version = 1;
|
|
|
|
/* Grace SAM ATR used only if the live ATR was not captured. */
|
|
static const uint8_t SEADER_READER_FALLBACK_ATR[] =
|
|
{0x3b, 0x95, 0x96, 0x80, 0xb1, 0xfe, 0x55, 0x1f, 0xc7, 0x47, 0x72, 0x61, 0x63, 0x65, 0x13};
|
|
|
|
struct SeaderReader {
|
|
SeaderReaderConfig cfg;
|
|
|
|
FuriMutex* lock;
|
|
FuriSemaphore* resp_ready;
|
|
|
|
bool waiting;
|
|
uint32_t resp_len;
|
|
uint8_t resp_buf[SEADER_READER_MAX_APDU];
|
|
uint8_t tx_buf[SEADER_READER_MAX_APDU];
|
|
|
|
uint32_t apdu_count;
|
|
};
|
|
|
|
void seader_reader_config_default(SeaderReaderConfig* cfg) {
|
|
furi_check(cfg);
|
|
memset(cfg, 0, sizeof(*cfg));
|
|
cfg->vid = SEADER_READER_DEFAULT_VID;
|
|
cfg->pid = SEADER_READER_DEFAULT_PID;
|
|
strncpy(cfg->manufacturer, SEADER_READER_DEFAULT_MANUF, SEADER_READER_NAME_MAX - 1);
|
|
strncpy(cfg->product, SEADER_READER_DEFAULT_PRODUCT, SEADER_READER_NAME_MAX - 1);
|
|
}
|
|
|
|
/* -------------------- Persisted identity (name + PID) -------------------- */
|
|
|
|
void seader_reader_settings_load(Seader* seader) {
|
|
furi_check(seader);
|
|
/* Defaults first. */
|
|
strlcpy(seader->reader_manufacturer, SEADER_READER_DEFAULT_MANUF, SEADER_READER_NAME_MAX);
|
|
strlcpy(seader->reader_product, SEADER_READER_DEFAULT_PRODUCT, SEADER_READER_NAME_MAX);
|
|
seader->reader_pid = SEADER_READER_DEFAULT_PID;
|
|
|
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
FlipperFormat* file = flipper_format_file_alloc(storage);
|
|
FuriString* tmp = furi_string_alloc();
|
|
uint32_t version = 0;
|
|
do {
|
|
if(!flipper_format_file_open_existing(file, SEADER_READER_CFG_PATH)) break;
|
|
if(!flipper_format_read_header(file, tmp, &version)) break;
|
|
if(furi_string_cmp_str(tmp, seader_reader_cfg_header) ||
|
|
version != seader_reader_cfg_version)
|
|
break;
|
|
if(flipper_format_read_string(file, "Manufacturer", tmp)) {
|
|
strlcpy(
|
|
seader->reader_manufacturer, furi_string_get_cstr(tmp), SEADER_READER_NAME_MAX);
|
|
}
|
|
if(flipper_format_read_string(file, "Product", tmp)) {
|
|
strlcpy(seader->reader_product, furi_string_get_cstr(tmp), SEADER_READER_NAME_MAX);
|
|
}
|
|
uint32_t pid = 0;
|
|
if(flipper_format_read_uint32(file, "PID", &pid, 1) && pid != 0 && pid <= 0xFFFF) {
|
|
seader->reader_pid = (uint16_t)pid;
|
|
}
|
|
} while(false);
|
|
furi_string_free(tmp);
|
|
flipper_format_free(file);
|
|
furi_record_close(RECORD_STORAGE);
|
|
}
|
|
|
|
void seader_reader_settings_save(Seader* seader) {
|
|
furi_check(seader);
|
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
FlipperFormat* file = flipper_format_file_alloc(storage);
|
|
do {
|
|
if(!flipper_format_file_open_always(file, SEADER_READER_CFG_PATH)) break;
|
|
if(!flipper_format_write_header_cstr(
|
|
file, seader_reader_cfg_header, seader_reader_cfg_version))
|
|
break;
|
|
if(!flipper_format_write_string_cstr(file, "Manufacturer", seader->reader_manufacturer))
|
|
break;
|
|
if(!flipper_format_write_string_cstr(file, "Product", seader->reader_product)) break;
|
|
uint32_t pid = seader->reader_pid;
|
|
if(!flipper_format_write_uint32(file, "PID", &pid, 1)) break;
|
|
} while(false);
|
|
flipper_format_free(file);
|
|
furi_record_close(RECORD_STORAGE);
|
|
}
|
|
|
|
/* -------------------- CCID gadget callbacks -------------------- */
|
|
|
|
/* Called from the USB gadget on ICC power-on (USB thread). */
|
|
/* Grace getSamVersion single-shot, used as a sacrificial warm-up. */
|
|
static const uint8_t SEADER_READER_WARMUP[] = {
|
|
0xA0,
|
|
0xDA,
|
|
0x02,
|
|
0x63,
|
|
0x00,
|
|
0x00,
|
|
0x0A,
|
|
0x44,
|
|
0x0A,
|
|
0x44,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0xA0,
|
|
0x02,
|
|
0x82,
|
|
0x00,
|
|
0x00,
|
|
0x00};
|
|
|
|
static void seader_reader_get_atr(void* ctx, uint8_t* atr, uint16_t* atr_len) {
|
|
Seader* seader = ctx;
|
|
SeaderReader* reader = seader->reader;
|
|
|
|
/* Warm up the SAM link on power-on: the first exchange after connect can be
|
|
dropped/rejected (SamKeyScan resets+retries for the same reason), so send
|
|
a sacrificial getSamVersion and discard the result before the host's real
|
|
commands arrive. */
|
|
if(reader) {
|
|
uint8_t scratch[64];
|
|
uint16_t slen = 0;
|
|
seader_reader_relay_once(
|
|
seader,
|
|
reader,
|
|
SEADER_READER_WARMUP,
|
|
sizeof(SEADER_READER_WARMUP),
|
|
scratch,
|
|
sizeof(scratch),
|
|
&slen,
|
|
1000); /* short timeout so a mute SAM doesn't stall SCardConnect */
|
|
}
|
|
|
|
if(seader->ATR_len > 0 && seader->ATR_len <= SEADER_MAX_ATR_SIZE) {
|
|
memcpy(atr, seader->ATR, seader->ATR_len);
|
|
*atr_len = (uint16_t)seader->ATR_len;
|
|
} else {
|
|
memcpy(atr, SEADER_READER_FALLBACK_ATR, sizeof(SEADER_READER_FALLBACK_ATR));
|
|
*atr_len = (uint16_t)sizeof(SEADER_READER_FALLBACK_ATR);
|
|
}
|
|
FURI_LOG_I(TAG, "ICC power on, ATR len=%u", (unsigned)*atr_len);
|
|
}
|
|
|
|
/* Undo WUDF's corruption of the extended Grace single-shot: on some Windows
|
|
builds WUDF changes the class byte (A0->00) and re-encodes the extended
|
|
length as short, so the SAM's Grace PUT DATA (INS=DA, P1P2=0263) fails (6E00
|
|
for the class byte, 6700 for the short length). We restore CLA=A0 and rebuild
|
|
the extended encoding the SAM expects, writing the result into dst and
|
|
returning its new length. Everything else is copied verbatim: non-DA
|
|
commands, already-extended commands, and the SnmpLoader forms (P1=80 /
|
|
P1P2=0000) whose short encoding the SAM accepts. */
|
|
static uint16_t seader_reader_fix_command(uint8_t* dst, const uint8_t* apdu, uint16_t apdu_len) {
|
|
memcpy(dst, apdu, apdu_len);
|
|
if(apdu_len < 5 || dst[1] != 0xDA) return apdu_len;
|
|
|
|
if(dst[0] == 0x00) dst[0] = 0xA0; /* restore class byte */
|
|
|
|
if(dst[4] == 0x00) return apdu_len; /* already extended */
|
|
/* Only the Grace single-shot (P1P2=0263) needs the extended encoding
|
|
rebuilt. SnmpLoader (P1=80 / P1P2=0000) works in its short form, and
|
|
reconstructing it would desync SamKeyScan's chunked read fallback. */
|
|
if(!(dst[2] == 0x02 && dst[3] == 0x63)) return apdu_len;
|
|
|
|
uint8_t lc = dst[4];
|
|
if((uint16_t)(5 + lc) > apdu_len) return apdu_len; /* malformed short APDU */
|
|
bool case4 = apdu_len > (uint16_t)(5 + lc);
|
|
uint16_t new_len = (uint16_t)(7 + lc + (case4 ? 2 : 0));
|
|
if(new_len > SEADER_READER_MAX_APDU) return apdu_len; /* no room */
|
|
|
|
/* CLA INS P1 P2 | 00 <Lc hi> <Lc lo> | data | 00 00 (extended Le, case 4). */
|
|
memmove(dst + 7, dst + 5, lc);
|
|
dst[4] = 0x00;
|
|
dst[5] = 0x00;
|
|
dst[6] = lc;
|
|
if(case4) {
|
|
dst[7 + lc] = 0x00;
|
|
dst[7 + lc + 1] = 0x00;
|
|
}
|
|
return new_len;
|
|
}
|
|
|
|
/* Relay one APDU to the SAM and block for its response. Copies up to out_cap
|
|
bytes into out and sets *out_len. Runs on the ccid worker thread; the UART-RX
|
|
thread wakes us via seader_reader_sam_response. Returns true on success. */
|
|
static bool seader_reader_relay_once(
|
|
Seader* seader,
|
|
SeaderReader* reader,
|
|
const uint8_t* apdu,
|
|
uint16_t apdu_len,
|
|
uint8_t* out,
|
|
uint16_t out_cap,
|
|
uint16_t* out_len,
|
|
uint32_t timeout_ms) {
|
|
if(apdu_len == 0 || apdu_len > SEADER_READER_MAX_APDU) return false;
|
|
|
|
furi_semaphore_acquire(reader->resp_ready, 0); /* drain stale token */
|
|
furi_mutex_acquire(reader->lock, FuriWaitForever);
|
|
reader->resp_len = 0;
|
|
reader->waiting = true;
|
|
uint16_t tx_len = seader_reader_fix_command(reader->tx_buf, apdu, apdu_len);
|
|
furi_mutex_release(reader->lock);
|
|
|
|
seader_trace_hex("Reader", "SAM TX", reader->tx_buf, tx_len);
|
|
|
|
SeaderUartBridge* uart = seader->uart;
|
|
if(uart->T == 1) {
|
|
seader_send_t1(uart, reader->tx_buf, tx_len);
|
|
} else {
|
|
seader_ccid_XfrBlock(uart, reader->tx_buf, tx_len);
|
|
}
|
|
|
|
FuriStatus st = furi_semaphore_acquire(reader->resp_ready, furi_ms_to_ticks(timeout_ms));
|
|
|
|
bool ok = false;
|
|
furi_mutex_acquire(reader->lock, FuriWaitForever);
|
|
reader->waiting = false;
|
|
if(st == FuriStatusOk && reader->resp_len >= 2) {
|
|
uint16_t n = (uint16_t)reader->resp_len;
|
|
seader_trace_hex("Reader", "SAM RX", reader->resp_buf, reader->resp_len);
|
|
if(n > out_cap) n = out_cap;
|
|
memcpy(out, reader->resp_buf, n);
|
|
*out_len = n;
|
|
ok = true;
|
|
} else {
|
|
seader_trace(
|
|
"Reader", "SAM RX none/short st=%d len=%lu", st, (unsigned long)reader->resp_len);
|
|
}
|
|
furi_mutex_release(reader->lock);
|
|
return ok;
|
|
}
|
|
|
|
/* Called from the CCID worker thread for each host APDU. Relays to the SAM and,
|
|
like a real ACR39U at short-APDU level, handles T=0 GET RESPONSE (SW1=0x61)
|
|
reader-side so the host sees one complete response. Always fills resp. */
|
|
static bool seader_reader_xfr(
|
|
void* ctx,
|
|
const uint8_t* apdu,
|
|
uint16_t apdu_len,
|
|
uint8_t* resp,
|
|
uint16_t* resp_len) {
|
|
Seader* seader = ctx;
|
|
SeaderReader* reader = seader->reader;
|
|
if(!reader) {
|
|
*resp_len = 0;
|
|
return false;
|
|
}
|
|
|
|
if(apdu_len == 0 || apdu_len > SEADER_READER_MAX_APDU) {
|
|
FURI_LOG_W(TAG, "Rejecting APDU len=%u", (unsigned)apdu_len);
|
|
resp[0] = 0x6F;
|
|
resp[1] = 0x00;
|
|
*resp_len = 2;
|
|
return true;
|
|
}
|
|
|
|
uint16_t total = 0;
|
|
if(!seader_reader_relay_once(
|
|
seader,
|
|
reader,
|
|
apdu,
|
|
apdu_len,
|
|
resp,
|
|
SEADER_CCID_MAX_RESP,
|
|
&total,
|
|
SEADER_READER_TIMEOUT_MS)) {
|
|
FURI_LOG_W(TAG, "SAM relay timeout/short");
|
|
resp[0] = 0x6F;
|
|
resp[1] = 0x00;
|
|
*resp_len = 2;
|
|
reader->apdu_count++;
|
|
return true;
|
|
}
|
|
|
|
/* Reader-side GET RESPONSE loop for SW1=0x61. */
|
|
uint8_t guard = 0;
|
|
while(total >= 2 && resp[total - 2] == 0x61 && guard++ < 16) {
|
|
uint8_t le = resp[total - 1];
|
|
total -= 2; /* strip the 61xx SW; keep any leading data */
|
|
uint16_t cap = (total < SEADER_CCID_MAX_RESP) ? (uint16_t)(SEADER_CCID_MAX_RESP - total) :
|
|
0;
|
|
if(cap < 2) break;
|
|
uint8_t get_response[5] = {0x00, 0xC0, 0x00, 0x00, le};
|
|
uint16_t got = 0;
|
|
if(!seader_reader_relay_once(
|
|
seader, reader, get_response, 5, resp + total, cap, &got, SEADER_READER_TIMEOUT_MS)) {
|
|
break;
|
|
}
|
|
total += got;
|
|
}
|
|
|
|
*resp_len = total;
|
|
reader->apdu_count++;
|
|
return true;
|
|
}
|
|
|
|
/* -------------------- SAM response (UART-RX thread context) --------------- */
|
|
|
|
bool seader_reader_sam_response(Seader* seader, uint8_t* apdu, uint32_t len) {
|
|
furi_check(seader);
|
|
SeaderReader* reader = seader->reader;
|
|
if(!reader) {
|
|
return false;
|
|
}
|
|
|
|
furi_mutex_acquire(reader->lock, FuriWaitForever);
|
|
if(reader->waiting) {
|
|
uint32_t n = len;
|
|
if(n > sizeof(reader->resp_buf)) {
|
|
n = sizeof(reader->resp_buf);
|
|
}
|
|
memcpy(reader->resp_buf, apdu, n);
|
|
reader->resp_len = n;
|
|
furi_semaphore_release(reader->resp_ready);
|
|
} else {
|
|
FURI_LOG_D(TAG, "Dropping unsolicited SAM response len=%lu", (unsigned long)len);
|
|
}
|
|
furi_mutex_release(reader->lock);
|
|
return true;
|
|
}
|
|
|
|
uint32_t seader_reader_apdu_count(Seader* seader) {
|
|
if(!seader || !seader->reader) {
|
|
return 0;
|
|
}
|
|
return seader->reader->apdu_count;
|
|
}
|
|
|
|
const char* seader_reader_active_name(Seader* seader) {
|
|
if(!seader || !seader->reader) {
|
|
return SEADER_READER_DEFAULT_PRODUCT;
|
|
}
|
|
return seader->reader->cfg.product;
|
|
}
|
|
|
|
uint32_t seader_reader_ccid_count(void) {
|
|
uint32_t count = 0;
|
|
seader_usb_ccid_reader_stats(NULL, &count);
|
|
return count;
|
|
}
|
|
|
|
const char* seader_reader_ccid_last_name(void) {
|
|
uint8_t last = 0;
|
|
seader_usb_ccid_reader_stats(&last, NULL);
|
|
return seader_usb_ccid_cmd_name(last);
|
|
}
|
|
|
|
void seader_reader_ccid_debug(uint16_t* atr_len, int32_t* tx_last) {
|
|
seader_usb_ccid_reader_debug(atr_len, tx_last);
|
|
}
|
|
|
|
/* -------------------- Lifecycle (worker thread) --------------------------- */
|
|
|
|
static SeaderReader* seader_reader_alloc(void) {
|
|
SeaderReader* reader = malloc(sizeof(SeaderReader));
|
|
memset(reader, 0, sizeof(SeaderReader));
|
|
seader_reader_config_default(&reader->cfg);
|
|
reader->lock = furi_mutex_alloc(FuriMutexTypeNormal);
|
|
reader->resp_ready = furi_semaphore_alloc(1, 0);
|
|
return reader;
|
|
}
|
|
|
|
static void seader_reader_free(SeaderReader* reader) {
|
|
if(!reader) {
|
|
return;
|
|
}
|
|
furi_semaphore_free(reader->resp_ready);
|
|
furi_mutex_free(reader->lock);
|
|
free(reader);
|
|
}
|
|
|
|
void seader_reader_run(Seader* seader) {
|
|
furi_check(seader);
|
|
SeaderWorker* worker = seader->worker;
|
|
furi_check(worker);
|
|
|
|
SeaderReader* reader = seader_reader_alloc();
|
|
seader->reader = reader;
|
|
|
|
/* Apply the persisted, user-editable identity (falls back to defaults). */
|
|
strlcpy(reader->cfg.manufacturer, seader->reader_manufacturer, SEADER_READER_NAME_MAX);
|
|
if(seader->reader_product[0]) {
|
|
strlcpy(reader->cfg.product, seader->reader_product, SEADER_READER_NAME_MAX);
|
|
}
|
|
if(seader->reader_pid) {
|
|
reader->cfg.pid = seader->reader_pid;
|
|
}
|
|
|
|
SeaderCcidReaderConfig ccfg = {
|
|
.vid = reader->cfg.vid,
|
|
.pid = reader->cfg.pid,
|
|
.manuf = reader->cfg.manufacturer,
|
|
.product = reader->cfg.product,
|
|
.get_atr = seader_reader_get_atr,
|
|
.xfr = seader_reader_xfr,
|
|
.ctx = seader,
|
|
};
|
|
seader_usb_ccid_reader_start(&ccfg);
|
|
|
|
uint32_t last_ccid = UINT32_MAX;
|
|
while(seader_worker_get_state(worker) == SeaderWorkerStateReaderEmulation) {
|
|
furi_delay_ms(50);
|
|
/* Refresh the UI on any CCID activity (PowerOn/SlotStatus/XfrBlock/...),
|
|
not just relayed APDUs, so the on-screen command indicator is live. */
|
|
uint32_t ccid = seader_reader_ccid_count();
|
|
if(ccid != last_ccid) {
|
|
last_ccid = ccid;
|
|
if(worker->callback) {
|
|
worker->callback(SeaderWorkerEventReaderUpdate, worker->context);
|
|
}
|
|
}
|
|
}
|
|
|
|
seader_usb_ccid_reader_stop();
|
|
seader->reader = NULL;
|
|
seader_reader_free(reader);
|
|
}
|