From 78cb9e52fd391a10e791c673c7c4b97d81b73b5c Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Tue, 25 Aug 2026 23:46:40 +0800 Subject: [PATCH 1/2] USB SAM Reader (ACR39U-style passthrough) Seader can present the attached HID SAM to a host PC as a **USB CCID contact smart-card reader**, so any PC/SC application can drive the SAM directly (e.g. the PM3 SAM host tools). The SAM appears as the card in slot 0. --- application.fam | 1 + readme.md | 37 ++ sam_reader.c | 414 +++++++++++++++++++++ sam_reader.h | 85 +++++ scenes/seader_scene_config.h | 3 + scenes/seader_scene_reader.c | 89 +++++ scenes/seader_scene_reader_manuf.c | 54 +++ scenes/seader_scene_reader_name.c | 75 ++++ scenes/seader_scene_sam_present.c | 30 +- seader.c | 3 + seader_i.h | 10 + seader_worker.c | 7 + seader_worker.h | 2 + usb_ccid_reader.c | 571 +++++++++++++++++++++++++++++ usb_ccid_reader.h | 49 +++ 15 files changed, 1428 insertions(+), 2 deletions(-) create mode 100644 sam_reader.c create mode 100644 sam_reader.h create mode 100644 scenes/seader_scene_reader.c create mode 100644 scenes/seader_scene_reader_manuf.c create mode 100644 scenes/seader_scene_reader_name.c create mode 100644 usb_ccid_reader.c create mode 100644 usb_ccid_reader.h diff --git a/application.fam b/application.fam index 699b005..6f18c13 100644 --- a/application.fam +++ b/application.fam @@ -12,6 +12,7 @@ App( "ASN_DISABLE_XER_SUPPORT", "ASN_DISABLE_RANDOM_FILL", "ASN_DISABLE_PRINT_SUPPORT", + "SEADER_ENABLE_TRACE_LOG", ], requires=[ "gui", "storage", "nfc", diff --git a/readme.md b/readme.md index fe66538..a92098c 100644 --- a/readme.md +++ b/readme.md @@ -72,3 +72,40 @@ Optionally 3d print a [case designed by sean](https://www.printables.com/model/5 - `arm-none-eabi-readelf ~/.ufbt/build/seader.fap -t` - `ufbt cli` -> `free_blocks` + +## ๐Ÿ”Œ USB SAM Reader (ACR39U-style passthrough) + +Seader can present the attached HID SAM to a host PC as a **USB CCID contact +smart-card reader**, so any PC/SC application can drive the SAM directly (e.g. +the PM3 SAM host tools). The SAM appears as the card in slot 0. + +Menu: with a SAM detected, choose **USB SAM Reader**. Connect the Flipper to a +PC over USB; a PC/SC reader appears (default identity: ACS / "ACR39U ICC +Reader"). Press **Back** to stop and restore the normal USB console. + +- Host APDUs (`PC_TO_RDR_XfrBlock`) are relayed to the SAM over Seader's + existing T=1 / CCID path; the response is returned to the host verbatim. +- The reader name is **user-editable in the app**: SAM detected โ†’ **USB Reader + Name** โ†’ set the **manufacturer**, then the **product** string. Both persist + (`usb_reader.conf`) and the USB PID auto-bumps on any change so Windows + re-enumerates a fresh device node instead of showing the cached old name. + Windows builds the PC/SC reader name as ` `, + which is what name-matching host tools key on. Keep at least the manufacturer + non-empty: with no manufacturer string (`iManufacturer = 0`) Windows names the + reader from the generic driver description and PC/SC apps stop finding it. + Compile-time defaults / VID live in `sam_reader.h` (`SEADER_READER_DEFAULT_*`). + Neutral VID `0x1209` keeps the inbox CCID driver bound (avoid a real vendor's + VID like ACS `0x072F`, which loads the vendor driver and refuses a non-genuine + device). +- Implementation: `usb_ccid_reader.{c,h}` (self-contained USB CCID gadget) + + `sam_reader.{c,h}` (SAM relay bridge) + `scenes/seader_scene_reader.c`, worker + state `SeaderWorkerStateReaderEmulation`. + +**Why a custom USB gadget:** the firmware's stock `usb_ccid` interface +advertises only 2 endpoints (no interrupt IN) and `dwProtocols = T=0` only. +Windows' WUDF usbccid driver refuses to start it ("This device cannot start. +Code 10"), and it can't present a T=1 card. `usb_ccid_reader.c` therefore +defines its own `FuriHalUsbInterface` with a 3-endpoint descriptor (bulk +in/out + interrupt in) and `dwProtocols = T=0|T=1`, then bridges host APDUs to +the SAM. Built against the f7 SDK's `libusb_stm32` (`usb_ccid.h`, `usb_std.h`, +`usbd_core.h`) + `furi_hal_usb.h`. diff --git a/sam_reader.c b/sam_reader.c new file mode 100644 index 0000000..a65b0c8 --- /dev/null +++ b/sam_reader.c @@ -0,0 +1,414 @@ +#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); +} diff --git a/sam_reader.h b/sam_reader.h new file mode 100644 index 0000000..9e5d617 --- /dev/null +++ b/sam_reader.h @@ -0,0 +1,85 @@ +#pragma once + +// SAM Reader emulation: presents the attached HID SAM to a USB host as a +// CCID contact smart-card reader (ACR39U-style), relaying host APDUs to the +// SAM over Seader's existing T=1 / CCID-XfrBlock path. +// +// Topology: +// USB host (PC/SC, e.g. PM3_SAM_App / Bridge tools) +// | USB CCID (this module presents the reader) +// Flipper (Seader) +// | UART -> reader chip / raw ISO7816 +// HID SAM (appears as the card in slot 0) + +#include +#include +#include +#include + +typedef struct Seader Seader; + +// Buffers sized to Seader's existing SAM frame ceiling. +#define SEADER_READER_MAX_APDU 272 +#define SEADER_READER_TIMEOUT_MS 4000U +#define SEADER_READER_NAME_MAX 32 + +// Default identity presented on the USB bus. Edit these to change how the +// Flipper advertises itself. The product string is what surfaces (with a slot +// suffix) as the PC/SC reader name that host tools match against. +// +// Neutral VID/PID: keep these OFF a real vendor's IDs so Windows binds the +// inbox Microsoft CCID class driver (on USB\Class_0B) instead of a vendor +// driver. The inbox driver then uses the iProduct string below as the PC/SC +// reader name, which is what name-matching host tools key on. +// +// Do NOT use ACS's real VID (0x072F): Windows loads the ACS driver, renames +// the device generically ("ACS CCID USB Reader"), and refuses to initialize a +// non-genuine ACS device -- host apps then cannot connect. +// +// The manufacturer + product strings are chosen so the PC/SC reader name +// matches a genuine ACR39U ("ACS ACR39U ICC Reader 0"). If your host tool +// matches a different exact name, edit the product string to suit. +#define SEADER_READER_DEFAULT_VID 0x1209U // pid.codes generic +#define SEADER_READER_DEFAULT_PID 0x5346U +#define SEADER_READER_DEFAULT_MANUF "ACS" +#define SEADER_READER_DEFAULT_PRODUCT "ACR39U ICC Reader" + +typedef struct SeaderReader SeaderReader; + +// Runtime-configurable identity. +typedef struct { + uint16_t vid; + uint16_t pid; + char manufacturer[SEADER_READER_NAME_MAX]; + char product[SEADER_READER_NAME_MAX]; +} SeaderReaderConfig; + +// Fill cfg with the compiled-in defaults. +void seader_reader_config_default(SeaderReaderConfig* cfg); + +// Load/save the persisted, user-editable reader identity (product name + PID) +// into seader->reader_product / seader->reader_pid. Load applies compiled-in +// defaults when no config file exists. +void seader_reader_settings_load(Seader* seader); +void seader_reader_settings_save(Seader* seader); + +// Worker-thread entry: brings up the USB CCID reader, then blocks relaying +// APDUs until the worker leaves SeaderWorkerStateReaderEmulation. Tears the +// USB reader down and restores the previous USB config on exit. +void seader_reader_run(Seader* seader); + +// Called from the UART-RX path (seader_worker_process_sam_message) when a SAM +// response arrives while in reader-emulation mode. Hands the response back to +// the waiting USB callback. Returns true if consumed. +bool seader_reader_sam_response(Seader* seader, uint8_t* apdu, uint32_t len); + +// Number of APDUs relayed since the current session started (for the UI). +uint32_t seader_reader_apdu_count(Seader* seader); + +// Product/reader name currently advertised (for the UI). +const char* seader_reader_active_name(Seader* seader); + +// Diagnostics for the UI: total CCID commands seen + last command name. +uint32_t seader_reader_ccid_count(void); +const char* seader_reader_ccid_last_name(void); +void seader_reader_ccid_debug(uint16_t* atr_len, int32_t* tx_last); diff --git a/scenes/seader_scene_config.h b/scenes/seader_scene_config.h index 0b93c48..8400690 100644 --- a/scenes/seader_scene_config.h +++ b/scenes/seader_scene_config.h @@ -19,3 +19,6 @@ ADD_SCENE(seader, sam_info, SamInfo) ADD_SCENE(seader, virtual_credential, VirtualCredential) ADD_SCENE(seader, formats, Formats) ADD_SCENE(seader, apdu_runner, APDURunner) +ADD_SCENE(seader, reader, Reader) +ADD_SCENE(seader, reader_manuf, ReaderManuf) +ADD_SCENE(seader, reader_name, ReaderName) diff --git a/scenes/seader_scene_reader.c b/scenes/seader_scene_reader.c new file mode 100644 index 0000000..625b85a --- /dev/null +++ b/scenes/seader_scene_reader.c @@ -0,0 +1,89 @@ +#include "../seader_i.h" + +#define TAG "Seader:Scene:Reader" + +static void seader_scene_reader_render(Seader* seader) { + Widget* widget = seader_get_widget(seader); + if(!widget) { + return; + } + widget_reset(widget); + + widget_add_string_element( + widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "USB SAM Reader"); + + char line[48]; + + /* Live CCID command indicator: shows where a host connect stops. */ + snprintf( + line, + sizeof(line), + "CCID: %s (%lu)", + seader_reader_ccid_last_name(), + (unsigned long)seader_reader_ccid_count()); + widget_add_string_element(widget, 0, 14, AlignLeft, AlignTop, FontSecondary, line); + + /* PowerOn diagnostics: ATR length + last bulk-IN write result. */ + uint16_t atr_len = 0; + int32_t tx_last = 0; + seader_reader_ccid_debug(&atr_len, &tx_last); + snprintf(line, sizeof(line), "atr=%u tx=%ld", (unsigned)atr_len, (long)tx_last); + widget_add_string_element(widget, 0, 26, AlignLeft, AlignTop, FontSecondary, line); + + snprintf( + line, sizeof(line), "Relayed: %lu APDUs", (unsigned long)seader_reader_apdu_count(seader)); + widget_add_string_element(widget, 0, 38, AlignLeft, AlignTop, FontSecondary, line); + + widget_add_string_element( + widget, 0, 52, AlignLeft, AlignTop, FontSecondary, "USB PC/SC ยท Back to stop"); + + view_dispatcher_switch_to_view(seader->view_dispatcher, SeaderViewWidget); +} + +static void seader_scene_reader_worker_callback(uint32_t event, void* context) { + Seader* seader = context; + view_dispatcher_send_custom_event(seader->view_dispatcher, event); +} + +void seader_scene_reader_on_enter(void* context) { + Seader* seader = context; + seader_worker_acquire(seader); + + seader_scene_reader_render(seader); + + seader_worker_start( + seader->worker, + SeaderWorkerStateReaderEmulation, + seader->uart, + seader_scene_reader_worker_callback, + seader); +} + +bool seader_scene_reader_on_event(void* context, SceneManagerEvent event) { + Seader* seader = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SeaderWorkerEventReaderUpdate) { + seader_scene_reader_render(seader); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + // Stop the reader worker and restore USB before leaving. + seader_worker_release(seader); + scene_manager_search_and_switch_to_previous_scene( + seader->scene_manager, SeaderSceneSamPresent); + consumed = true; + } + + return consumed; +} + +void seader_scene_reader_on_exit(void* context) { + Seader* seader = context; + // Idempotent: release again in case exit came from a route other than Back. + seader_worker_release(seader); + if(seader->widget) { + widget_reset(seader->widget); + } +} diff --git a/scenes/seader_scene_reader_manuf.c b/scenes/seader_scene_reader_manuf.c new file mode 100644 index 0000000..07395a7 --- /dev/null +++ b/scenes/seader_scene_reader_manuf.c @@ -0,0 +1,54 @@ +#include "../seader_i.h" + +#define TAG "Seader:Scene:ReaderManuf" + +void seader_scene_reader_manuf_text_input_callback(void* context) { + Seader* seader = context; + view_dispatcher_send_custom_event(seader->view_dispatcher, SeaderCustomEventTextInputDone); +} + +void seader_scene_reader_manuf_on_enter(void* context) { + Seader* seader = context; + + TextInput* text_input = seader_get_text_input(seader); + if(!text_input) { + FURI_LOG_E(TAG, "Text input view unavailable"); + return; + } + + strlcpy(seader->reader_manuf_edit, seader->reader_manufacturer, SEADER_READER_NAME_MAX); + + text_input_set_header_text(text_input, "Manufacturer (may be empty)"); + text_input_set_result_callback( + text_input, + seader_scene_reader_manuf_text_input_callback, + seader, + seader->reader_manuf_edit, + SEADER_READER_NAME_MAX, + false); // prefilled with the current manufacturer + + view_dispatcher_switch_to_view(seader->view_dispatcher, SeaderViewTextInput); +} + +bool seader_scene_reader_manuf_on_event(void* context, SceneManagerEvent event) { + Seader* seader = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SeaderCustomEventTextInputDone) { + // Step 1 of 2: manufacturer captured in reader_manuf_edit; now the + // product name. The final save (with PID bump) happens there. + scene_manager_next_scene(seader->scene_manager, SeaderSceneReaderName); + consumed = true; + } + } + + return consumed; +} + +void seader_scene_reader_manuf_on_exit(void* context) { + Seader* seader = context; + if(seader->text_input) { + text_input_reset(seader->text_input); + } +} diff --git a/scenes/seader_scene_reader_name.c b/scenes/seader_scene_reader_name.c new file mode 100644 index 0000000..8585be6 --- /dev/null +++ b/scenes/seader_scene_reader_name.c @@ -0,0 +1,75 @@ +#include "../seader_i.h" + +#define TAG "Seader:Scene:ReaderName" + +void seader_scene_reader_name_text_input_callback(void* context) { + Seader* seader = context; + view_dispatcher_send_custom_event(seader->view_dispatcher, SeaderCustomEventTextInputDone); +} + +void seader_scene_reader_name_on_enter(void* context) { + Seader* seader = context; + + TextInput* text_input = seader_get_text_input(seader); + if(!text_input) { + FURI_LOG_E(TAG, "Text input view unavailable"); + return; + } + + strlcpy(seader->reader_name_edit, seader->reader_product, SEADER_READER_NAME_MAX); + + text_input_set_header_text(text_input, "Product name"); + text_input_set_result_callback( + text_input, + seader_scene_reader_name_text_input_callback, + seader, + seader->reader_name_edit, + SEADER_READER_NAME_MAX, + false); // prefilled with the current name + + view_dispatcher_switch_to_view(seader->view_dispatcher, SeaderViewTextInput); +} + +bool seader_scene_reader_name_on_event(void* context, SceneManagerEvent event) { + Seader* seader = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SeaderCustomEventTextInputDone) { + // Step 2 of 2: commit both manufacturer + product. Bump the USB PID + // if either changed so Windows re-enumerates a fresh device node + // instead of showing the cached old name. + bool changed = + strncmp( + seader->reader_manuf_edit, + seader->reader_manufacturer, + SEADER_READER_NAME_MAX) != 0 || + strncmp(seader->reader_name_edit, seader->reader_product, SEADER_READER_NAME_MAX) != + 0; + if(changed) { + seader->reader_pid++; + if(seader->reader_pid == 0) { + seader->reader_pid = SEADER_READER_DEFAULT_PID; + } + } + strlcpy( + seader->reader_manufacturer, + seader->reader_manuf_edit, + SEADER_READER_NAME_MAX); + strlcpy(seader->reader_product, seader->reader_name_edit, SEADER_READER_NAME_MAX); + seader_reader_settings_save(seader); + scene_manager_search_and_switch_to_previous_scene( + seader->scene_manager, SeaderSceneSamPresent); + consumed = true; + } + } + + return consumed; +} + +void seader_scene_reader_name_on_exit(void* context) { + Seader* seader = context; + if(seader->text_input) { + text_input_reset(seader->text_input); + } +} diff --git a/scenes/seader_scene_sam_present.c b/scenes/seader_scene_sam_present.c index fa362dc..e233b79 100644 --- a/scenes/seader_scene_sam_present.c +++ b/scenes/seader_scene_sam_present.c @@ -2,6 +2,8 @@ enum SubmenuIndex { SubmenuIndexRead, SubmenuIndexSaved, + SubmenuIndexReaderEmulation, + SubmenuIndexReaderName, SubmenuIndexAPDURunner, SubmenuIndexReadConfigCard, SubmenuIndexSamInfo, @@ -23,6 +25,19 @@ static void seader_scene_sam_present_rebuild_menu(Seader* seader, uint32_t selec submenu_add_item( submenu, "Saved", SubmenuIndexSaved, seader_scene_sam_present_submenu_callback, seader); + submenu_add_item( + submenu, + "USB SAM Reader", + SubmenuIndexReaderEmulation, + seader_scene_sam_present_submenu_callback, + seader); + submenu_add_item( + submenu, + "USB Reader Name", + SubmenuIndexReaderName, + seader_scene_sam_present_submenu_callback, + seader); + if(seader->is_debug_enabled) { submenu_add_item( submenu, @@ -77,8 +92,9 @@ bool seader_scene_sam_present_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(seader->sam_present_menu_guard_active && (event.event == SubmenuIndexRead || event.event == SubmenuIndexSaved || - event.event == SubmenuIndexAPDURunner || event.event == SubmenuIndexReadConfigCard || - event.event == SubmenuIndexSamInfo)) { + event.event == SubmenuIndexReaderEmulation || + event.event == SubmenuIndexReaderName || event.event == SubmenuIndexAPDURunner || + event.event == SubmenuIndexReadConfigCard || event.event == SubmenuIndexSamInfo)) { seader->sam_present_menu_guard_active = false; consumed = true; } else if(event.event == SubmenuIndexRead) { @@ -101,6 +117,16 @@ bool seader_scene_sam_present_on_event(void* context, SceneManagerEvent event) { seader->scene_manager, SeaderSceneSamPresent, event.event); scene_manager_next_scene(seader->scene_manager, SeaderSceneFileSelect); consumed = true; + } else if(event.event == SubmenuIndexReaderEmulation) { + scene_manager_set_scene_state( + seader->scene_manager, SeaderSceneSamPresent, event.event); + scene_manager_next_scene(seader->scene_manager, SeaderSceneReader); + consumed = true; + } else if(event.event == SubmenuIndexReaderName) { + scene_manager_set_scene_state( + seader->scene_manager, SeaderSceneSamPresent, event.event); + scene_manager_next_scene(seader->scene_manager, SeaderSceneReaderManuf); + consumed = true; } else if(event.event == SeaderWorkerEventSamMissing) { seader->board_status = seader_board_status_on_sam_missing(seader->board_status); scene_manager_next_scene(seader->scene_manager, SeaderSceneSamMissing); diff --git a/seader.c b/seader.c index c2dbf1f..a4b0d13 100644 --- a/seader.c +++ b/seader.c @@ -1172,6 +1172,9 @@ Seader* seader_alloc() { memset(&seader->hf_mode_ctx, 0, sizeof(seader->hf_mode_ctx)); seader->hf_mode_active = false; + seader->reader = NULL; + seader_reader_settings_load(seader); + seader->worker = seader_worker_alloc(); seader->view_dispatcher = view_dispatcher_alloc(); seader->scene_manager = scene_manager_alloc(&seader_scene_handlers, seader); diff --git a/seader_i.h b/seader_i.h index 763e7ca..18f1932 100644 --- a/seader_i.h +++ b/seader_i.h @@ -68,6 +68,7 @@ #include "uhf_snmp_probe.h" #include "uhf_status_label.h" #include "hf_buffer_pool.h" +#include "sam_reader.h" #define WORKER_ALL_RX_EVENTS \ (WorkerEvtStop | WorkerEvtRxDone | WorkerEvtCfgChange | WorkerEvtLineCfgSet | \ @@ -221,6 +222,15 @@ struct Seader { APDULog* apdu_log; SeaderAPDURunnerContext apdu_runner_ctx; + + // USB CCID reader-emulation session (NULL unless in reader mode) + SeaderReader* reader; + // Persisted, user-editable USB reader identity + char reader_manufacturer[SEADER_READER_NAME_MAX]; + char reader_product[SEADER_READER_NAME_MAX]; + char reader_manuf_edit[SEADER_READER_NAME_MAX]; + char reader_name_edit[SEADER_READER_NAME_MAX]; + uint16_t reader_pid; }; struct SeaderPollerContainer { diff --git a/seader_worker.c b/seader_worker.c index df0ce17..b218d0d 100644 --- a/seader_worker.c +++ b/seader_worker.c @@ -423,6 +423,10 @@ bool seader_worker_process_sam_message(Seader* seader, uint8_t* apdu, uint32_t l return seader_apdu_runner_response(seader, apdu, len); } + if(seader_worker->state == SeaderWorkerStateReaderEmulation) { + return seader_reader_sam_response(seader, apdu, len); + } + SEADER_VERBOSE_HEX(FuriLogLevelInfo, TAG, "APDU", apdu, len); seader_trace( TAG, @@ -552,6 +556,9 @@ int32_t seader_worker_task(void* context) { } else if(seader_worker->state == SeaderWorkerStateReading) { SEADER_VERBOSE_D(TAG, "Reading mode started"); seader_worker_reading(seader); + } else if(seader_worker->state == SeaderWorkerStateReaderEmulation) { + SEADER_VERBOSE_I(TAG, "USB reader emulation started"); + seader_reader_run(seader); } seader_worker_change_state(seader_worker, SeaderWorkerStateReady); diff --git a/seader_worker.h b/seader_worker.h index 5aa8220..2c2ab1d 100644 --- a/seader_worker.h +++ b/seader_worker.h @@ -21,6 +21,7 @@ typedef enum { SeaderWorkerStateAPDURunner, SeaderWorkerStateReading, SeaderWorkerStateHfTeardown, + SeaderWorkerStateReaderEmulation, // Transition SeaderWorkerStateStop, } SeaderWorkerState; @@ -42,6 +43,7 @@ typedef enum { SeaderWorkerEventAPDURunnerSuccess, SeaderWorkerEventAPDURunnerError, SeaderWorkerEventHfTeardownComplete, + SeaderWorkerEventReaderUpdate, } SeaderWorkerEvent; typedef enum { diff --git a/usb_ccid_reader.c b/usb_ccid_reader.c new file mode 100644 index 0000000..2d2f5fe --- /dev/null +++ b/usb_ccid_reader.c @@ -0,0 +1,571 @@ +#include "usb_ccid_reader.h" + +#include +#include +#include +#include +#include + +#define TAG "SeaderUsbCcid" + +/* IMPORTANT: on the Flipper, USB_LP_IRQHandler calls usbd_poll directly, so all + * usbd endpoint/event callbacks run in ISR context. The ISR callbacks here must + * NEVER block and must NOT use large stack buffers. All CCID message processing + * (including the blocking SAM relay) happens on the ccid worker thread; the ISR + * only reads packets into a stream buffer and signals the thread. */ + +/* Endpoints (index low nibble must be unique across in/out). */ +#define CCID_EP_OUT 0x01 /* bulk OUT (host -> reader) */ +#define CCID_EP_IN 0x82 /* bulk IN (reader -> host) */ +#define CCID_EP_INT 0x83 /* interrupt IN (slot change) */ +#define CCID_BULK_EPSIZE 64 +#define CCID_INT_EPSIZE 8 + +#define STR_MAX_CHARS 31 +#define STR_BUF_SIZE (2u + 2u * STR_MAX_CHARS) + +#define CCID_RX_STREAM_SIZE (SEADER_CCID_MSG_MAX + 128u) + +#define CCID_FLAG_RX (1u << 0) +#define CCID_FLAG_TX_DONE (1u << 1) +#define CCID_FLAG_STOP (1u << 2) +#define CCID_FLAG_ALL (CCID_FLAG_RX | CCID_FLAG_TX_DONE | CCID_FLAG_STOP) + +#define CCID_TX_TIMEOUT_MS 2000u + +/* Full config descriptor: config + interface(3 EP) + CCID functional + 3 EPs. */ +struct CcidConfigDescriptor { + struct usb_config_descriptor config; + struct usb_interface_descriptor intf; + struct usb_ccid_descriptor ccid; + struct usb_endpoint_descriptor ep_bulk_in; + struct usb_endpoint_descriptor ep_bulk_out; + struct usb_endpoint_descriptor ep_int_in; +} __attribute__((packed)); + +static const struct CcidConfigDescriptor ccid_cfg_desc = { + .config = + { + .bLength = sizeof(struct usb_config_descriptor), + .bDescriptorType = USB_DTYPE_CONFIGURATION, + .wTotalLength = sizeof(struct CcidConfigDescriptor), + .bNumInterfaces = 1, + .bConfigurationValue = 1, + .iConfiguration = 0, + .bmAttributes = USB_CFG_ATTR_RESERVED | USB_CFG_ATTR_SELFPOWERED, + .bMaxPower = 0xFA, /* 500 mA */ + }, + .intf = + { + .bLength = sizeof(struct usb_interface_descriptor), + .bDescriptorType = USB_DTYPE_INTERFACE, + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 3, + .bInterfaceClass = USB_CLASS_CCID, + .bInterfaceSubClass = USB_CCID_SUBCLASS, + .bInterfaceProtocol = USB_CCID_PROTO_CCID, + .iInterface = 0, + }, + .ccid = + { + .bLength = sizeof(struct usb_ccid_descriptor), + .bDescriptorType = USB_DTYPE_CCID_FUNCTIONAL, + .bcdCCID = CCID_CURRENT_SPEC_RELEASE_NUMBER, + .bMaxSlotIndex = 0x00, + .bVoltageSupport = 0x07, /* 5V | 3V | 1.8V */ + .dwProtocols = 0x00000003, /* T=0 | T=1 (spec bit0=T0, bit1=T1) */ + /* Clock/data-rate matched to a genuine ACR39U. dwDataRate must be + consistent with dwDefaultClock at the initial ETU (clock/372). */ + .dwDefaultClock = 0x000012C0, /* 4.8 MHz */ + .dwMaximumClock = 0x000012C0, + .bNumClockSupported = 0, + .dwDataRate = 0x00003267, /* 12903 bps */ + .dwMaxDataRate = 0x000C9A90, /* 826000 bps */ + .bNumDataRatesSupported = 0, + .dwMaxIFSD = 0x000000F7, /* 247 */ + .dwSynchProtocols = 0, + .dwMechanical = 0, + /* Genuine ACR39U dwFeatures is 0x000107BA (TPDU level). We use the + short+extended APDU level (0x40000): at short-APDU level (0x20000) + WUDF caps the R-APDU buffer at 258 bytes, so key-scan replies of + 264 bytes overflow it (SCardTransmit 0x0000050B). Extended level + gives WUDF an extended-sized response buffer. WUDF still corrupts + the class byte of EXTENDED APDUs (A0->00) and re-encodes their + length to short at every exchange level on some Windows builds; + sam_reader.c rebuilds the extended PUT DATA commands to + compensate. */ + .dwFeatures = 0x000407BA, + .dwMaxCCIDMessageLength = SEADER_CCID_MSG_MAX, + .bClassGetResponse = 0xFF, + .bClassEnvelope = 0xFF, + .wLcdLayout = 0x0000, + .bPINSupport = 0x00, + .bMaxCCIDBusySlots = 0x01, + }, + .ep_bulk_in = + { + .bLength = sizeof(struct usb_endpoint_descriptor), + .bDescriptorType = USB_DTYPE_ENDPOINT, + .bEndpointAddress = CCID_EP_IN, + .bmAttributes = USB_EPTYPE_BULK, + .wMaxPacketSize = CCID_BULK_EPSIZE, + .bInterval = 0, + }, + .ep_bulk_out = + { + .bLength = sizeof(struct usb_endpoint_descriptor), + .bDescriptorType = USB_DTYPE_ENDPOINT, + .bEndpointAddress = CCID_EP_OUT, + .bmAttributes = USB_EPTYPE_BULK, + .wMaxPacketSize = CCID_BULK_EPSIZE, + .bInterval = 0, + }, + .ep_int_in = + { + .bLength = sizeof(struct usb_endpoint_descriptor), + .bDescriptorType = USB_DTYPE_ENDPOINT, + .bEndpointAddress = CCID_EP_INT, + .bmAttributes = USB_EPTYPE_INTERRUPT, + .wMaxPacketSize = CCID_INT_EPSIZE, + .bInterval = 16, + }, +}; + +typedef struct { + usbd_device* dev; + SeaderCcidReaderConfig cfg; + FuriHalUsbInterface* prev; + + struct usb_device_descriptor dev_descr; + uint8_t str_manuf[STR_BUF_SIZE]; + uint8_t str_prod[STR_BUF_SIZE]; + uint8_t str_serial[STR_BUF_SIZE]; + FuriHalUsbInterface iface; + + /* Worker thread + ISR->thread plumbing. */ + FuriThread* thread; + FuriThreadId thread_id; + FuriStreamBuffer* rx_stream; + volatile bool running; + volatile bool configured; + + /* All owned by the worker thread. */ + uint8_t rx_msg[SEADER_CCID_MSG_MAX]; /* reassembled CCID message */ + uint16_t rx_msg_len; + uint8_t tx_buf[10 + SEADER_CCID_MAX_RESP]; /* response being sent */ + uint8_t xfr_resp[SEADER_CCID_MAX_RESP]; /* SAM response scratch */ + + /* Diagnostics (read cross-thread by the UI). */ + volatile uint8_t last_cmd; + volatile uint32_t cmd_count; + volatile uint16_t dbg_atr_len; + volatile int32_t dbg_tx_last; +} UsbCcid; + +static UsbCcid* g_ccid = NULL; + +/* -------------------------------------------------------------------------- */ + +static void ccid_build_string(uint8_t* buf, const char* s) { + size_t n = strlen(s); + if(n > STR_MAX_CHARS) n = STR_MAX_CHARS; + buf[0] = (uint8_t)(2u + 2u * n); + buf[1] = USB_DTYPE_STRING; + for(size_t i = 0; i < n; i++) { + buf[2 + 2 * i] = (uint8_t)s[i]; + buf[2 + 2 * i + 1] = 0x00; + } +} + +/* -------- TX from the worker thread (blocking-safe, packetized) -------- */ + +static bool ccid_send(const uint8_t* data, uint16_t len) { + uint16_t pos = 0; + do { + uint16_t chunk = len - pos; + if(chunk > CCID_BULK_EPSIZE) chunk = CCID_BULK_EPSIZE; + furi_thread_flags_clear(CCID_FLAG_TX_DONE); + int32_t w = usbd_ep_write(g_ccid->dev, CCID_EP_IN, data + pos, chunk); + g_ccid->dbg_tx_last = w; + if(w < 0) { + /* EP momentarily busy: wait for the in-flight packet to drain. */ + uint32_t f = furi_thread_flags_wait( + CCID_FLAG_TX_DONE | CCID_FLAG_STOP, FuriFlagWaitAny, CCID_TX_TIMEOUT_MS); + if((f & FuriFlagError) || (f & CCID_FLAG_STOP)) return false; + continue; + } + pos += (uint16_t)w; + uint32_t f = furi_thread_flags_wait( + CCID_FLAG_TX_DONE | CCID_FLAG_STOP, FuriFlagWaitAny, CCID_TX_TIMEOUT_MS); + if((f & FuriFlagError) || (f & CCID_FLAG_STOP)) return false; + } while(pos < len); + + /* A transfer that is a whole multiple of the packet size needs a ZLP so the + host knows it ended. */ + if(len > 0 && (len % CCID_BULK_EPSIZE) == 0) { + furi_thread_flags_clear(CCID_FLAG_TX_DONE); + usbd_ep_write(g_ccid->dev, CCID_EP_IN, NULL, 0); + furi_thread_flags_wait( + CCID_FLAG_TX_DONE | CCID_FLAG_STOP, FuriFlagWaitAny, CCID_TX_TIMEOUT_MS); + } + return true; +} + +static void ccid_send_data_block(uint8_t seq, const uint8_t* data, uint16_t len) { + if(len > SEADER_CCID_MAX_RESP) len = SEADER_CCID_MAX_RESP; + uint8_t* p = g_ccid->tx_buf; + p[0] = RDR_TO_PC_DATABLOCK; /* 0x80 */ + p[1] = (uint8_t)(len & 0xFF); + p[2] = (uint8_t)((len >> 8) & 0xFF); + p[3] = 0; + p[4] = 0; + p[5] = 0; /* bSlot */ + p[6] = seq; + p[7] = 0x00; /* bStatus: ICC present+active, cmd OK */ + p[8] = 0x00; /* bError */ + p[9] = 0x00; /* bChainParameter */ + if(len) memcpy(p + 10, data, len); + ccid_send(p, (uint16_t)(10 + len)); +} + +static void ccid_send_slot_status(uint8_t seq) { + uint8_t* p = g_ccid->tx_buf; + p[0] = RDR_TO_PC_SLOTSTATUS; /* 0x81 */ + memset(p + 1, 0, 9); + p[6] = seq; + ccid_send(p, 10); +} + +static void ccid_send_parameters(uint8_t seq, uint8_t proto, const uint8_t* params, uint8_t nparams) { + if(nparams > 7) nparams = 7; + uint8_t* p = g_ccid->tx_buf; + p[0] = RDR_TO_PC_PARAMETERS; /* 0x82 */ + p[1] = nparams; + p[2] = 0; + p[3] = 0; + p[4] = 0; + p[5] = 0; + p[6] = seq; + p[7] = 0x00; /* bStatus */ + p[8] = 0x00; /* bError */ + p[9] = proto ? proto : 0x01; /* bProtocolNum (default T=1) */ + if(nparams) { + memcpy(p + 10, params, nparams); + } else { + /* Default T=1 parameter block. */ + static const uint8_t t1[7] = {0x11, 0x10, 0x00, 0x45, 0x00, 0xFE, 0x00}; + memcpy(p + 10, t1, 7); + p[1] = 7; + p[9] = 0x01; + nparams = 7; + } + ccid_send(p, (uint16_t)(10 + nparams)); +} + +/* -------- Process one complete CCID message (worker thread) -------- */ + +static void ccid_process_message(const uint8_t* msg, uint16_t total) { + uint8_t type = msg[0]; + uint32_t dwLength = (uint32_t)msg[1] | ((uint32_t)msg[2] << 8) | ((uint32_t)msg[3] << 16) | + ((uint32_t)msg[4] << 24); + uint8_t seq = msg[6]; + UNUSED(total); + + g_ccid->last_cmd = type; + g_ccid->cmd_count++; + + switch(type) { + case PC_TO_RDR_ICCPOWERON: { + uint16_t atr_len = 0; + /* Reuse tx_buf tail as scratch for the ATR, then send. */ + uint8_t* atr = g_ccid->xfr_resp; + if(g_ccid->cfg.get_atr) g_ccid->cfg.get_atr(g_ccid->cfg.ctx, atr, &atr_len); + g_ccid->dbg_atr_len = atr_len; + ccid_send_data_block(seq, atr, atr_len); + break; + } + case PC_TO_RDR_ICCPOWEROFF: + case PC_TO_RDR_GETSLOTSTATUS: + ccid_send_slot_status(seq); + break; + case PC_TO_RDR_XFRBLOCK: { + const uint8_t* apdu = msg + 10; + uint16_t apdu_len = (uint16_t)dwLength; + uint16_t resp_len = 0; + if(g_ccid->cfg.xfr && + g_ccid->cfg.xfr(g_ccid->cfg.ctx, apdu, apdu_len, g_ccid->xfr_resp, &resp_len)) { + ccid_send_data_block(seq, g_ccid->xfr_resp, resp_len); + } else { + uint8_t sw[2] = {0x6F, 0x00}; + ccid_send_data_block(seq, sw, 2); + } + break; + } + case PC_TO_RDR_SETPARAMETERS: + ccid_send_parameters(seq, msg[7], msg + 10, (uint8_t)dwLength); + break; + case PC_TO_RDR_GETPARAMETERS: + case PC_TO_RDR_RESETPARAMETERS: + ccid_send_parameters(seq, 0x01, NULL, 0); + break; + default: + ccid_send_slot_status(seq); + break; + } +} + +static void ccid_drain_and_process(void) { + for(;;) { + size_t space = sizeof(g_ccid->rx_msg) - g_ccid->rx_msg_len; + size_t got = 0; + if(space > 0) { + got = furi_stream_buffer_receive( + g_ccid->rx_stream, g_ccid->rx_msg + g_ccid->rx_msg_len, space, 0); + g_ccid->rx_msg_len += (uint16_t)got; + } + + bool progressed = false; + while(g_ccid->rx_msg_len >= 10) { + uint32_t dwLength = (uint32_t)g_ccid->rx_msg[1] | ((uint32_t)g_ccid->rx_msg[2] << 8) | + ((uint32_t)g_ccid->rx_msg[3] << 16) | + ((uint32_t)g_ccid->rx_msg[4] << 24); + uint32_t expected = 10u + dwLength; + if(expected > sizeof(g_ccid->rx_msg)) { + g_ccid->rx_msg_len = 0; /* oversized/garbage: resync */ + break; + } + if(g_ccid->rx_msg_len < expected) break; + + ccid_process_message(g_ccid->rx_msg, (uint16_t)expected); + uint16_t remain = g_ccid->rx_msg_len - (uint16_t)expected; + if(remain) memmove(g_ccid->rx_msg, g_ccid->rx_msg + expected, remain); + g_ccid->rx_msg_len = remain; + progressed = true; + } + + if(got == 0 && !progressed) break; + } +} + +static int32_t ccid_worker(void* context) { + UNUSED(context); + while(g_ccid->running) { + uint32_t flags = furi_thread_flags_wait( + CCID_FLAG_RX | CCID_FLAG_STOP, FuriFlagWaitAny, FuriWaitForever); + if(flags & FuriFlagError) continue; + if(flags & CCID_FLAG_STOP) break; + if(flags & CCID_FLAG_RX) ccid_drain_and_process(); + } + return 0; +} + +/* -------- ISR endpoint/event callbacks (minimal, non-blocking) -------- */ + +static void ccid_rx_isr(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(ep); + if(event != usbd_evt_eprx) return; + uint8_t buf[CCID_BULK_EPSIZE]; + int32_t len = usbd_ep_read(dev, CCID_EP_OUT, buf, sizeof(buf)); + if(len > 0 && g_ccid && g_ccid->rx_stream) { + furi_stream_buffer_send(g_ccid->rx_stream, buf, (size_t)len, 0); + if(g_ccid->thread_id) furi_thread_flags_set(g_ccid->thread_id, CCID_FLAG_RX); + } +} + +static void ccid_tx_isr(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(dev); + UNUSED(ep); + if(event != usbd_evt_eptx) return; + if(g_ccid && g_ccid->thread_id) furi_thread_flags_set(g_ccid->thread_id, CCID_FLAG_TX_DONE); +} + +static void ccid_int_isr(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(dev); + UNUSED(event); + UNUSED(ep); +} + +static void ccid_notify_slot_change(usbd_device* dev) { + uint8_t buf[2] = {RDR_TO_PC_NOTIFYSLOTCHANGE, 0x03}; /* slot 0: present + changed */ + usbd_ep_write(dev, CCID_EP_INT, buf, sizeof(buf)); +} + +static usbd_respond ccid_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) { + UNUSED(dev); + UNUSED(callback); + if((req->bmRequestType & USB_REQ_TYPE) == USB_REQ_CLASS && + (req->bmRequestType & USB_REQ_RECIPIENT) == USB_REQ_INTERFACE) { + if(req->bRequest == CCID_ABORT) return usbd_ack; + } + return usbd_fail; +} + +static usbd_respond ccid_ep_config(usbd_device* dev, uint8_t cfg) { + switch(cfg) { + case 0: + usbd_ep_deconfig(dev, CCID_EP_IN); + usbd_ep_deconfig(dev, CCID_EP_OUT); + usbd_ep_deconfig(dev, CCID_EP_INT); + usbd_reg_endpoint(dev, CCID_EP_IN, NULL); + usbd_reg_endpoint(dev, CCID_EP_OUT, NULL); + usbd_reg_endpoint(dev, CCID_EP_INT, NULL); + g_ccid->configured = false; + return usbd_ack; + case 1: + usbd_ep_config(dev, CCID_EP_IN, USB_EPTYPE_BULK, CCID_BULK_EPSIZE); + usbd_ep_config(dev, CCID_EP_OUT, USB_EPTYPE_BULK, CCID_BULK_EPSIZE); + usbd_ep_config(dev, CCID_EP_INT, USB_EPTYPE_INTERRUPT, CCID_INT_EPSIZE); + usbd_reg_endpoint(dev, CCID_EP_IN, ccid_tx_isr); + usbd_reg_endpoint(dev, CCID_EP_OUT, ccid_rx_isr); + usbd_reg_endpoint(dev, CCID_EP_INT, ccid_int_isr); + g_ccid->rx_msg_len = 0; + g_ccid->configured = true; + ccid_notify_slot_change(dev); + return usbd_ack; + } + return usbd_fail; +} + +static void ccid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { + UNUSED(intf); + UNUSED(ctx); + if(!g_ccid) return; + g_ccid->dev = dev; + usbd_reg_config(dev, ccid_ep_config); + usbd_reg_control(dev, ccid_control); + usbd_connect(dev, true); +} + +static void ccid_deinit(usbd_device* dev) { + usbd_reg_config(dev, NULL); + usbd_reg_control(dev, NULL); +} + +static void ccid_on_wakeup(usbd_device* dev) { + UNUSED(dev); +} + +static void ccid_on_suspend(usbd_device* dev) { + UNUSED(dev); +} + +/* -------------------------------------------------------------------------- */ + +void seader_usb_ccid_reader_start(const SeaderCcidReaderConfig* cfg) { + furi_check(cfg); + if(g_ccid) seader_usb_ccid_reader_stop(); + + g_ccid = malloc(sizeof(UsbCcid)); + memset(g_ccid, 0, sizeof(UsbCcid)); + g_ccid->cfg = *cfg; + g_ccid->rx_stream = furi_stream_buffer_alloc(CCID_RX_STREAM_SIZE, 1); + g_ccid->running = true; + + g_ccid->thread = furi_thread_alloc_ex("SeaderCcid", 2048, ccid_worker, NULL); + furi_thread_start(g_ccid->thread); + g_ccid->thread_id = furi_thread_get_id(g_ccid->thread); + + g_ccid->dev_descr = (struct usb_device_descriptor){ + .bLength = sizeof(struct usb_device_descriptor), + .bDescriptorType = USB_DTYPE_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 8, + .idVendor = cfg->vid, + .idProduct = cfg->pid, + .bcdDevice = 0x0100, + .iManufacturer = 1, /* manufacturer string present (PC/SC name = manuf + product) */ + .iProduct = 2, + .iSerialNumber = 3, + .bNumConfigurations = 1, + }; + + ccid_build_string(g_ccid->str_manuf, cfg->manuf ? cfg->manuf : "Seader"); + ccid_build_string(g_ccid->str_prod, cfg->product ? cfg->product : "SAM Reader"); + ccid_build_string(g_ccid->str_serial, "SEADER-SAM-1"); + + g_ccid->iface = (FuriHalUsbInterface){ + .init = ccid_init, + .deinit = ccid_deinit, + .wakeup = ccid_on_wakeup, + .suspend = ccid_on_suspend, + .dev_descr = &g_ccid->dev_descr, + .str_manuf_descr = g_ccid->str_manuf, + .str_prod_descr = g_ccid->str_prod, + .str_serial_descr = g_ccid->str_serial, + .cfg_descr = (void*)&ccid_cfg_desc, + }; + + g_ccid->prev = furi_hal_usb_get_config(); + furi_hal_usb_unlock(); + if(!furi_hal_usb_set_config(&g_ccid->iface, NULL)) { + FURI_LOG_E(TAG, "furi_hal_usb_set_config failed"); + } + FURI_LOG_I(TAG, "CCID reader up: '%s' %04x:%04x", g_ccid->str_prod + 2, cfg->vid, cfg->pid); +} + +void seader_usb_ccid_reader_stop(void) { + if(!g_ccid) return; + + /* Restore USB first so no more ISR callbacks reference our state. */ + FuriHalUsbInterface* prev = g_ccid->prev; + furi_hal_usb_unlock(); + furi_hal_usb_set_config(prev, NULL); + + /* Tear down the worker thread. */ + g_ccid->running = false; + if(g_ccid->thread_id) furi_thread_flags_set(g_ccid->thread_id, CCID_FLAG_STOP); + furi_thread_join(g_ccid->thread); + furi_thread_free(g_ccid->thread); + + furi_stream_buffer_free(g_ccid->rx_stream); + free(g_ccid); + g_ccid = NULL; + FURI_LOG_I(TAG, "CCID reader down"); +} + +void seader_usb_ccid_reader_stats(uint8_t* last_cmd, uint32_t* count) { + if(g_ccid) { + if(last_cmd) *last_cmd = g_ccid->last_cmd; + if(count) *count = g_ccid->cmd_count; + } else { + if(last_cmd) *last_cmd = 0; + if(count) *count = 0; + } +} + +void seader_usb_ccid_reader_debug(uint16_t* atr_len, int32_t* tx_last) { + if(g_ccid) { + if(atr_len) *atr_len = g_ccid->dbg_atr_len; + if(tx_last) *tx_last = g_ccid->dbg_tx_last; + } else { + if(atr_len) *atr_len = 0; + if(tx_last) *tx_last = 0; + } +} + +const char* seader_usb_ccid_cmd_name(uint8_t type) { + switch(type) { + case 0x00: + return "-"; + case PC_TO_RDR_ICCPOWERON: + return "PowerOn"; + case PC_TO_RDR_ICCPOWEROFF: + return "PowerOff"; + case PC_TO_RDR_GETSLOTSTATUS: + return "SlotStatus"; + case PC_TO_RDR_XFRBLOCK: + return "XfrBlock"; + case PC_TO_RDR_GETPARAMETERS: + return "GetParams"; + case PC_TO_RDR_SETPARAMETERS: + return "SetParams"; + case PC_TO_RDR_RESETPARAMETERS: + return "RstParams"; + default: + return "?"; + } +} diff --git a/usb_ccid_reader.h b/usb_ccid_reader.h new file mode 100644 index 0000000..fb4d0e6 --- /dev/null +++ b/usb_ccid_reader.h @@ -0,0 +1,49 @@ +#pragma once + +// Self-contained USB CCID smart-card reader gadget (WUDF-compliant). +// +// The firmware's stock `usb_ccid` interface advertises only 2 endpoints (no +// interrupt IN) and dwProtocols = T=0 only. Windows' WUDF usbccid driver +// refuses to start it ("Code 10"), and it can't present a T=1 card. This +// module defines its own FuriHalUsbInterface with the interrupt endpoint added +// and dwProtocols = T=0|T=1, then bridges host APDUs to the SAM via callbacks. + +#include +#include +#include + +// Largest card response we return. Sized to the SAM's full frame (Seader's +// UART buffer) so large key-scan replies (>261 B) aren't truncated. +#define SEADER_CCID_MAX_RESP 272u +// Max CCID message length (advertised as dwMaxCCIDMessageLength), both +// directions = 10-byte header + response body. +#define SEADER_CCID_MSG_MAX (10u + SEADER_CCID_MAX_RESP) + +typedef struct { + uint16_t vid; + uint16_t pid; + const char* manuf; + const char* product; + + // Fill `atr` (<= 33 bytes) and set *atr_len. Called on ICC power-on. + void (*get_atr)(void* ctx, uint8_t* atr, uint16_t* atr_len); + + // Relay one APDU to the SAM. Must always fill `resp`/`*resp_len` (e.g. 6F00 + // on failure) and may block until the SAM answers. Returns true on success. + bool (*xfr)(void* ctx, const uint8_t* apdu, uint16_t apdu_len, uint8_t* resp, uint16_t* resp_len); + + void* ctx; +} SeaderCcidReaderConfig; + +// Bring the USB CCID reader up (saves + replaces the current USB config) and +// tear it down (restores the previous USB config). Not re-entrant; one at a time. +void seader_usb_ccid_reader_start(const SeaderCcidReaderConfig* cfg); +void seader_usb_ccid_reader_stop(void); + +// Diagnostics: last CCID command type received + total command count. +void seader_usb_ccid_reader_stats(uint8_t* last_cmd, uint32_t* count); +const char* seader_usb_ccid_cmd_name(uint8_t type); + +// Deeper diagnostics: ATR length returned on last PowerOn, and the return value +// of the last bulk-IN write (>=0 bytes written, <0 = write failed). +void seader_usb_ccid_reader_debug(uint16_t* atr_len, int32_t* tx_last); From 2d23f39f5f15220a1b21eded82c0663ccfd7dee6 Mon Sep 17 00:00:00 2001 From: Eric Betts Date: Tue, 25 Aug 2026 18:53:11 -0700 Subject: [PATCH 2/2] style: apply ufbt formatting Claude-Session: https://claude.ai/code/session_017aeoATqoJv92Xiq5vnqaN8 --- sam_reader.c | 47 +++++++++++++++++++++++-------- scenes/seader_scene_reader.c | 3 +- scenes/seader_scene_reader_name.c | 8 ++---- scenes/seader_scene_sam_present.c | 6 ++-- usb_ccid_reader.c | 3 +- usb_ccid_reader.h | 5 ++-- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/sam_reader.c b/sam_reader.c index a65b0c8..ecc7128 100644 --- a/sam_reader.c +++ b/sam_reader.c @@ -26,8 +26,8 @@ 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}; +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; @@ -72,7 +72,8 @@ void seader_reader_settings_load(Seader* seader) { 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); + 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); @@ -96,8 +97,7 @@ void seader_reader_settings_save(Seader* seader) { 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)) + 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; @@ -111,9 +111,26 @@ void seader_reader_settings_save(Seader* seader) { /* 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 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; @@ -228,7 +245,8 @@ static bool seader_reader_relay_once( *out_len = n; ok = true; } else { - seader_trace("Reader", "SAM RX none/short st=%d len=%lu", st, (unsigned long)reader->resp_len); + seader_trace( + "Reader", "SAM RX none/short st=%d len=%lu", st, (unsigned long)reader->resp_len); } furi_mutex_release(reader->lock); return ok; @@ -260,7 +278,13 @@ static bool seader_reader_xfr( uint16_t total = 0; if(!seader_reader_relay_once( - seader, reader, apdu, apdu_len, resp, SEADER_CCID_MAX_RESP, &total, + 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; @@ -275,7 +299,8 @@ static bool seader_reader_xfr( 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; + 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; diff --git a/scenes/seader_scene_reader.c b/scenes/seader_scene_reader.c index 625b85a..e94f677 100644 --- a/scenes/seader_scene_reader.c +++ b/scenes/seader_scene_reader.c @@ -9,8 +9,7 @@ static void seader_scene_reader_render(Seader* seader) { } widget_reset(widget); - widget_add_string_element( - widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "USB SAM Reader"); + widget_add_string_element(widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "USB SAM Reader"); char line[48]; diff --git a/scenes/seader_scene_reader_name.c b/scenes/seader_scene_reader_name.c index 8585be6..8c191cc 100644 --- a/scenes/seader_scene_reader_name.c +++ b/scenes/seader_scene_reader_name.c @@ -44,8 +44,8 @@ bool seader_scene_reader_name_on_event(void* context, SceneManagerEvent event) { seader->reader_manuf_edit, seader->reader_manufacturer, SEADER_READER_NAME_MAX) != 0 || - strncmp(seader->reader_name_edit, seader->reader_product, SEADER_READER_NAME_MAX) != - 0; + strncmp( + seader->reader_name_edit, seader->reader_product, SEADER_READER_NAME_MAX) != 0; if(changed) { seader->reader_pid++; if(seader->reader_pid == 0) { @@ -53,9 +53,7 @@ bool seader_scene_reader_name_on_event(void* context, SceneManagerEvent event) { } } strlcpy( - seader->reader_manufacturer, - seader->reader_manuf_edit, - SEADER_READER_NAME_MAX); + seader->reader_manufacturer, seader->reader_manuf_edit, SEADER_READER_NAME_MAX); strlcpy(seader->reader_product, seader->reader_name_edit, SEADER_READER_NAME_MAX); seader_reader_settings_save(seader); scene_manager_search_and_switch_to_previous_scene( diff --git a/scenes/seader_scene_sam_present.c b/scenes/seader_scene_sam_present.c index e233b79..ff1ae2f 100644 --- a/scenes/seader_scene_sam_present.c +++ b/scenes/seader_scene_sam_present.c @@ -92,9 +92,9 @@ bool seader_scene_sam_present_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(seader->sam_present_menu_guard_active && (event.event == SubmenuIndexRead || event.event == SubmenuIndexSaved || - event.event == SubmenuIndexReaderEmulation || - event.event == SubmenuIndexReaderName || event.event == SubmenuIndexAPDURunner || - event.event == SubmenuIndexReadConfigCard || event.event == SubmenuIndexSamInfo)) { + event.event == SubmenuIndexReaderEmulation || event.event == SubmenuIndexReaderName || + event.event == SubmenuIndexAPDURunner || event.event == SubmenuIndexReadConfigCard || + event.event == SubmenuIndexSamInfo)) { seader->sam_present_menu_guard_active = false; consumed = true; } else if(event.event == SubmenuIndexRead) { diff --git a/usb_ccid_reader.c b/usb_ccid_reader.c index 2d2f5fe..6077213 100644 --- a/usb_ccid_reader.c +++ b/usb_ccid_reader.c @@ -237,7 +237,8 @@ static void ccid_send_slot_status(uint8_t seq) { ccid_send(p, 10); } -static void ccid_send_parameters(uint8_t seq, uint8_t proto, const uint8_t* params, uint8_t nparams) { +static void + ccid_send_parameters(uint8_t seq, uint8_t proto, const uint8_t* params, uint8_t nparams) { if(nparams > 7) nparams = 7; uint8_t* p = g_ccid->tx_buf; p[0] = RDR_TO_PC_PARAMETERS; /* 0x82 */ diff --git a/usb_ccid_reader.h b/usb_ccid_reader.h index fb4d0e6..a414579 100644 --- a/usb_ccid_reader.h +++ b/usb_ccid_reader.h @@ -17,7 +17,7 @@ #define SEADER_CCID_MAX_RESP 272u // Max CCID message length (advertised as dwMaxCCIDMessageLength), both // directions = 10-byte header + response body. -#define SEADER_CCID_MSG_MAX (10u + SEADER_CCID_MAX_RESP) +#define SEADER_CCID_MSG_MAX (10u + SEADER_CCID_MAX_RESP) typedef struct { uint16_t vid; @@ -30,7 +30,8 @@ typedef struct { // Relay one APDU to the SAM. Must always fill `resp`/`*resp_len` (e.g. 6F00 // on failure) and may block until the SAM answers. Returns true on success. - bool (*xfr)(void* ctx, const uint8_t* apdu, uint16_t apdu_len, uint8_t* resp, uint16_t* resp_len); + bool ( + *xfr)(void* ctx, const uint8_t* apdu, uint16_t apdu_len, uint8_t* resp, uint16_t* resp_len); void* ctx; } SeaderCcidReaderConfig;