#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 #include 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 | 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); }