Keycard firmware branch updates#375
Open
ethermachine wants to merge 121 commits into
Open
Conversation
…mode Keycard / Ethereum - Centralise DEFAULT_ETH_PATH in helpers/ethereum/__init__.py. - Extract path/pubkey/PIN helpers from keycard_views.py into the new helpers/keycard/ui_helpers.py (keycard_views shrinks 747 -> 670 lines; underscore-prefixed aliases retained for backward compatibility). - AGENTS.md gains a full "Ethereum + Keycard integration" section (module layout, protocol parameters, pairing storage, threat model, extension points, hardware verification recipe). - Add tests/test_keycard_address.py (EIP-55 vectors, 8 cases) and tests/test_keycard_ui_helpers.py (path/TLV/wipe/session, 14 cases). Stealth boot mode - Two HIDDEN settings: STEALTH_BOOT (default Disabled) and STEALTH_UNLOCK_SEQUENCE (default 5xKEY_UP, 3..12 keys validated). - New seedsigner.stealth package: UnlockBuffer (suffix-match) and SnakeGame (resolution-aware, threaded input, panic exit on KEY1+KEY2+KEY3 held 10 s). Stealth modules MUST NOT import seed/ Keycard code; documented in AGENTS.md. - Controller.start() runs the game before OpeningSplashView when the setting is enabled; firmware boots normally once the unlock combo matches. - Tools menu gains "Stealth boot" -> ToolsStealthBootView (toggle + edit-sequence record/confirm). - Tests: test_stealth_unlock.py (16), test_stealth_snake.py (7), test_stealth_boot_integration.py (5). 168 ETH/Keycard/Stealth tests pass; core seed/settings/PSBT/tools-flow tests remain green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Storage (helpers/keycard/pairing_storage.py) - Per-UID encrypted blobs at <microSD>/keycard_pairing_<fp>.bin where fp is the first 16 hex chars of SHA-256(instance_uid). - save() writes to per-UID path by default and removes the legacy single-card file once a per-UID copy is on disk. - load() takes instance_uid, tries the per-UID path first then falls back to the legacy file (only if the embedded UID matches), so users with an existing keycard_pairing.bin keep working through the transition. - New list_pairings(), remove(instance_uid=...), remove_all(), fingerprint_for_uid(). Cache (controller.py) - controller.keycard_pairings: dict[bytes, PairingInfo] indexed by instance_uid for the boot. New helpers get_pairing_for / set_pairing_for / forget_pairing_for / forget_all_pairings. - last_keycard_uid tracks the most-recently-seen card. - Back-compat property keycard_pairing returns the pairing for the last-seen UID (so views still upgrading keep working). Auto-switch (helpers/keycard/ui_helpers.py) - open_unlocked_session now SELECTs first to discover which card is in the reader, then looks up its pairing in the controller's cache. Raises new KeycardCardChangedError(instance_uid) if the inserted card has no pairing for this boot. Sign/Export/Generate/Unpair views catch it and route to ToolsKeycardPairView. - New identify_inserted_card() helper for views that need only the UID (e.g. Forget current). Views (views/keycard_views.py) - ToolsKeycardPairView: SELECT first; if a pairing for the inserted card is already cached this boot, skip the password prompt entirely. - ToolsKeycardForgetSavedPairingView: now a list (Forget current / Forget all / per-fingerprint entry) backed by list_pairings(). Two new helper views handle the destructive paths with proper warning screens. - Sign/Export/Generate/Unpair: catch KeycardCardChangedError and redirect to PairView; Unpair also drops the cache entry and per-UID storage file on success. Tests - tests/test_keycard_pairing_storage.py: +13 cases covering fingerprint determinism, two-card save/load isolation, list/remove/remove_all, and legacy fallback + migration. - tests/test_keycard_session_auto_switch.py (new): 4 cases covering auto-switch happy path, card-not-paired raises, and back-to-back card-swap with cached pairings. - tests/test_keycard_ui_helpers.py: TestOpenUnlockedSession rewritten for the SELECT-first flow plus a new TestIdentifyInsertedCard. 186 keycard/eth/stealth tests pass; core seed/settings/PSBT/tools-flow tests remain green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B - Factory reset
- New INS_FACTORY_RESET (0xFD) with magic P1=0xAA P2=0x55 in
helpers/keycard/commands.py + factory_reset() builder + matching
KeycardClient.factory_reset(). Cleartext APDU (no secure channel
required); the magic in P1/P2 is the on-card sanity check.
- New ToolsKeycardFactoryResetView under Tools > Keycard with a Dire
warning, executes the wipe, then drops the device's local pairing
blobs (pairing_storage.remove_all() + controller.forget_all_pairings)
so nothing on the device references the now-blank card.
- Graceful handling of older applets: SW=0x6Dxx surfaces a clear
"Update applet, or use keycard-shell on PC" message.
- tests/test_keycard_factory_reset.py: APDU layout, magic constants
pinned, happy-path transmit, unsupported-applet error path.
Phase C - Import seedphrase to card (LOAD_KEY P1=0x02)
- New LOAD_KEY_P1_BIP39_SEED (0x02) constant + load_bip39_seed(seed64)
builder in commands.py + KeycardClient.load_bip39_seed() that routes
through the secure channel (PIN-protected).
- New ToolsKeycardImportSeedView monolithic flow (try/finally
guarantees wipe of every intermediate buffer):
1. Dire warning that the seed leaves the device once.
2. Source picker: Scan SeedQR / Type 12 words / Type 24 words.
3. Mnemonic captured into freshly-allocated strings via
"".join(word) so wipes never touch the BIP-39 wordlist
(CLAUDE.md guidance).
4. Optional BIP-39 passphrase prompt.
5. Master fingerprint shown for user verification (root.my_fingerprint).
6. PIN prompt, _open_unlocked_session (auto-switch aware),
client.load_bip39_seed.
7. Cleanup: wipe each word, passphrase bytearray, 64-byte seed,
PIN bytearray on every exit path.
- The import flow is isolated from controller.in_memory_seeds; the
seed never lands in the standard SeedSigner seed-management state.
- tests/test_keycard_load_key.py: APDU layout, length validation,
protected-channel routing, BIP-39 reference vector master
fingerprint pinned (abandon...about => 73c5da0a).
196 ETH+Keycard+Stealth tests pass; core seed/settings/PSBT/tools-flow
tests remain green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New module helpers/keycard/global_platform.py
- ISD AIDs (Visa + Mastercard candidates), default ISD keys (404142...4F).
- SCP02 (i='15') secure channel with C-MAC security level (0x01):
- INITIALIZE UPDATE / EXTERNAL AUTHENTICATE handshake.
- Session key derivation via 3DES-CBC over standardised constants
(0182 ENC, 0101 MAC, 0181 DEK).
- Cryptogram + retail MAC (ISO 9797-1 alg 3, padding method 2).
- Per-command MAC chaining: next ICV = single-DES-encrypt(prev MAC,
K1_MAC) per the GP 2.1.1 spec.
- High-level operations:
- list_instances(channel) via GET STATUS P1=0x40, TLV parser for
E3/4F/9F70/C5 entries.
- install_for_install(channel, package, applet, instance, ...).
- delete_aid(channel, aid, with_related=True).
- C-MAC only (no C-ENC): INSTALL/DELETE/GET STATUS carry no secrets.
Controller wiring (controller.py)
- New active_keycard_aid attribute (defaults to Status published AID).
- All client.select() callers in keycard_views.py + ui_helpers.py now
pass aid=controller.active_keycard_aid so multi-instance setups can
target a specific applet without further code change.
Manage instances UI (views/keycard_views.py)
- New menu entry "Manage instances" -> ToolsKeycardInstancesMenuView
with four sub-flows:
- List instances: read GET STATUS, show AIDs.
- Switch active: pick a Keycard applet AID and set
controller.active_keycard_aid for the session.
- Create instance: INSTALL [for install] under a free AID
(auto-suggested by bumping the last byte from existing).
- Delete instance: pick an instance, DELETE with related; if
deleted AID was active, fall back to the default.
Tests (tests/test_keycard_global_platform.py)
- ISO 9797 padding, session-key derivation determinism + arg
validation, cryptogram order-sensitivity, retail MAC length + ICV
chaining, ICV-update determinism.
- INITIALIZE UPDATE response parser (success + length rejection).
- Full SCP02 handshake against a self-consistent _MockCard:
- Successful open() yields three distinct 16-byte session keys.
- Card cryptogram mismatch raises GpProtocolError.
- First authenticated command after open carries CLA=0x84 + 8-byte
CMAC suffix.
- INSTALL [for install] APDU layout + short-AID rejection.
- DELETE APDU layout (P2=0x80 with related).
- GET STATUS TLV parser handles two-instance payload.
AGENTS.md
- New "Multi-instance management (GlobalPlatform / SCP02)" section
covering pre-conditions, protocol parameters, AID conventions,
active-instance semantics, and the threat model added by relying
on default ISD keys.
214 ETH+Keycard+Stealth tests pass (18 new in test_keycard_global_platform.py);
core seed/settings/PSBT/tools-flow tests remain green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ent-based reader - _error_destination now skips the failing view so OK on the error screen returns to the menu instead of looping back into the same wait_for_card timeout (the user got stuck and had to power-cycle). - SeedAddPassphraseScreen / SeedEncryptedQRMnemonicIDScreen treat KEY_LEFT on the back-arrow as "go back" -- previously ignored, only KEY_PRESS exited. - prompt_for_pin opens directly on the digits keyboard. - reader.py uses CardRequest.waitforcard() (PC/SC SCardGetStatusChange, same primitive as keycard-cli/keycard-shell) for native multi-slot detection; default timeout 5s -> 15s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ButtonListScreen has no status_headline field; three call sites (stealth menu, stealth save, sign ETH overview) were passing it and crashing with TypeError. Stealth menu drops the kwarg; the other two switch to LargeIconStatusScreen so headline + body actually render. - Sign ETH overview was computing a kind/chain/path/address body and silently discarding it; now passed as text. - Keycard reader follows the established multi-source pattern from helpers.seedkeeper_utils: release_other_smartcard_holders disconnects any held Satochip_Connector, kicks/relaunches gpg scdaemon, and re-arms ifdnfc when the PN532 interface is selected. Wait_for_card retries connect() across the full timeout budget so transient PC/SC contention no longer surfaces as the raw "Card not connected" text. - Sign ETH error path clears eth_sign_request / eth_signature in both except branches and routes the user to MainMenu via clear_history, ending the loop where pressing OK on the error popped back to the Overview with stale state. - Adds reader retry/release tests and a screen-kwargs validation test that asserts ButtonListScreen does not own status_headline (would have caught all three TypeError sites at unit-test time). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
release_other_smartcard_holders() was killing scdaemon and immediately
relaunching it, recreating the very PC/SC lock contention it was trying
to clear -- subsequent connect() calls failed with
CardConnectionException("card not connected") whether or not a card was
inserted, surfacing as the generic "Card not reachable" error. Drop the
relaunch (gpg-agent re-spawns scdaemon on demand) so wait_for_card()
can grab the exclusive handle.
Also disconnect half-open PC/SC connections inside the wait_for_card()
retry loop, otherwise we leak handles for the full 5s budget and poison
later attempts in the same session. And route KeycardErrorView's OK
explicitly to ToolsKeycardMenuView with clear_history=True so error
recovery is independent of any back-stack state set up by the failing
sub-flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After confirming the unlock-sequence recording the user landed back on ToolsStealthBootView via skip_current_view, but every screen pushed during the recording flow was still on the back stack -- pressing Back from the menu re-entered the Record screen. Drop everything pushed since (and including) the previous ToolsStealthBootView so Back from the menu correctly returns to Tools. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wait_for_card() returned service.connection from CardRequest.waitforcard().
That connection is bound to the CardRequest's PC/SC hcontext, which
PCSCCardRequest.__del__ releases via SCardReleaseContext as soon as the
local `request`/`service` fall out of scope on return. The hcard the
caller received was therefore invalid by the time it called transmit(),
and pyscard raised CardConnectionException("Card not connected") -- the
exact message the user kept seeing whenever a card was actually inserted.
Detect the active reader from the waitforcard service, then build our
own connection via Reader.createConnection() so the hcontext is owned
by the caller. Add per-retry warning logs and tighten the half-open
handle cleanup to cover both the service connection and the new one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues: (1) After Save, leaving the stealth menu required two Done presses. The cleanup loop removed Record/Confirm from the back stack, but the return missed skip_current_view=True so the controller left a duplicate ToolsStealthBootView entry; the first Done popped the duplicate and landed back on the same menu. Pop with skip_current_view=True. (2) Add a "View current" button so users can review the recorded sequence without having to re-record. Also distinguish "(none)" from "(invalid)" instead of conflating both as "(invalid)". (3) Stealth could be enabled while the unlock sequence was still the trivial 5xKEY_UP default. Change the default to "" and refuse the toggle ON when no sequence is recorded -- the firmware boots into a game and we don't want a coincidence to lock a user out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… reachable" Add `classify_card_error()` to helpers/keycard/ui_helpers.py and route every card-side catch block through it. Three views (Status, Pair, Forget current) used to title every exception "Card not reachable" and dump str(exc) into the body, so a card present at a non-default AID (SELECT -> SW=0x6A82) was indistinguishable from no-card-in-reader. The classifier maps NoReaderError, NoCardError, KeycardCardChangedError, SecureChannelError, PairingStorageError, and APDUError (with branches for 0x6A82 / 0x6982 / 0x6985 / 0x6Dxx / 0x63Cx) onto short, accurate (title, body) pairs that fit ~60 chars per line per CLAUDE.md. Operation-specific catches (Generate / Sign / Export / Push / UNPAIR / INIT / PAIR / Reset / GP-list/switch/create/delete) keep their titles when the cause is unknown but switch to a more diagnostic title (e.g. "Wrong PIN", "Applet not found") when the classifier recognises the SW. 14 new unit tests cover every mapping row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop for Rabby / MetaMask Mobile QR-Hardware-Wallet pairing and fixes three bugs uncovered while testing on a freshly-initialised keycard-shell card: * Sign-ETH crashed at the final QR display because PSBTSignedQRDisplayScreen doesn't exist; switched to QRDisplayScreen (same screen used by PSBT). * Card status / Factory reset / Pair / Init failed with "Applet not found / try Manage instances" when the inserted card's instance AID suffix wasn't 0x0101. select_with_autodetect now probes 0x0101..0x010F on 6A82 and updates active_keycard_aid transparently. * PAIR with wrong password used to surface as "Cannot open secure channel"; classify_card_error now returns "Wrong password" for the cryptogram-mismatch SecureChannelError specifically. Behaviour additions: * ToolsKeycardPairView tries "KeycardDefaultPairing" silently before prompting (matches keycard-shell / keycard-cli defaults). PAIR failures don't consume slots, so this is free for cards with custom passwords. Success screen indicates default vs custom. * New ToolsKeycardPairWalletView (replaces ToolsKeycardExportPubkeyView, with backwards-compat alias). Within one PIN-verified session, derives master/parent/account/first-receive, computes parent and source fingerprints (HASH160 of compressed pubkeys), encodes the BIP-44 ETH HD account at m/44'/60'/0' as a crypto-hdkey UR with chain code, origin and children=0/* — what Rabby and MetaMask Mobile (Keystone SDK) ingest to import a QR hardware wallet. The textual checksum address at m/44'/60'/0'/0/0 is shown as a secondary screen for visual verification. * New EthHDKeyQrEncoder in encode_qr.py and extract_extended_pubkey helper in ui_helpers.py (parses tag 0x82 chain code alongside 0x80 pubkey from the EXPORT KEY extended template). * Wipe path through "".join(pwd) avoids interning hazard when the default-password fast path normalises a module-level constant. Tests: 16 new in test_keycard_eth_pair_wallet.py covering the encoder round-trip via urtypes.crypto.HDKey.from_cbor, the TLV parser for the extended export, select_with_autodetect's fall-through and update of active_keycard_aid, and the new wrong-password classification. All 130 existing keycard/ethereum tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Status applet appends the applet-level SW1/SW2 to every protected response BEFORE padding + AES-CBC encryption. unwrap_response was returning the full decrypted plaintext, so the inner SW was silently ignored: VERIFY_PIN with a wrong PIN "succeeded" and only the next operation surfaced the failure as the cryptic "Parse fail resp=6985". Now we read the trailing two bytes as the inner SW and raise APDUError(sw) when != 0x9000. The IV chain still advances on error so subsequent commands stay in lockstep with the card. User-visible effect: - Wrong PIN fails on submission with "Wrong PIN - N tries left". - Operations that hit SW=6985/63CX/6A82/... are now routed through classify_card_error instead of falling through to pubkey-parse code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a background CardMonitor observer that wipes the cached PIN of the active card the instant it is removed from the reader. This closes the "yank-and-sniff" window where a session was still authenticated even though no card was present. - helpers/card_monitor.py: pyscard CardMonitor observer, daemon thread owned by pyscard. Wipes Keycard PIN cache + Satochip/SeedKeeper session on removal. ATR/reader_name deliberately not logged. - controller.py: per-UID keycard_pins (bytearray) + keycard_wallets_data cache. Independent copies so wiping caller's PIN does not destroy the cache; set_pin_for wipes the previous bytearray in place. Started CardMonitor at boot. - settings_definition.py: new SETTING__AUTO_PIN_ON_INSERT (Advanced, default Disabled) - toast prompts for PIN on card insert. Default for SETTING__CACHE_SCARD_PIN flipped to Enabled (Disabled = re-prompt on every Home navigation). - platform defaults flipped: PERSISTENT_SETTINGS Disabled -> Enabled, CAMERA_ROTATION 180 -> 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-level Keycard menu reduced from 10 entries to 5 daily-use ops +
two submenus (Setup, Manage):
Keycard
+-- Sign ETH (was: "Sign ETH transaction")
+-- View wallets
+-- Export xpub (was: "Export xpub to wallet")
+-- Setup > [new: Initialise card / Generate key / Import seed]
+-- Manage > [new: Status / Change PIN / Instances / Advanced]
+-- Advanced > [shortened labels]
ToolsKeycardPinManagementMenuView removed (was a one-entry indirection
to Change PIN; Change PIN now hangs directly off Manage). Error bodies
in classify_card_error / ToolsKeycardPairView updated to reference the
new menu paths ("Run Setup > Initialise card first", "Try Manage >
Instances to switch active AID").
Other Keycard work bundled in this snapshot:
- Change PIN flow (ToolsKeycardChangePinView): captures the new PIN
twice via the digits keyboard, runs CHANGE_PIN through the secure
channel, drops the cached PIN so the next op re-prompts. Tests in
test_keycard_change_pin.py.
- ui_helpers.py: prompt_for_pin / prompt_for_text / select_with_auto-
detect / open_unlocked_session / classify_card_error consolidated
out of keycard_views.py. PIN-prompt cancel propagates as
KeycardPinPromptCancelled.
- crypto.py / commands.py / responses.py / client.py: misc hardening
- init_encrypted APDU (one-shot ECDH+AES-CBC INIT for blank cards)
- factory_reset client method (INS=0xFD, magic P1/P2)
- bigger TLV parser for SELECT (8F/80/02/02/8E/8D fields)
- ECDSA DER signature normalisation (32-byte left-pad, sign-byte
strip)
- global_platform.py: SCP02 i=15 with C-MAC against the default ISD
keys for multi-instance management. INITIALIZE_UPDATE / EXTERNAL_
AUTHENTICATE / INSTALL[for install] / DELETE / GET_STATUS. 18 unit
tests in test_keycard_global_platform.py.
Tests:
- test_keycard.py: 4 new unwrap_response cases (SW strip on success,
wrong-PIN raise, 6985 raise, IV advance on error) + 4 PAIR /
init_encrypted / factory_reset cases.
- test_keycard_views.py: routing smoke tests for the 4 new menus,
plus an assertion that the stale PinManagementMenuView class is gone.
- test_keycard_ui_helpers.py: 6A82 error body now asserts the new
"Manage > Instances" copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- gui/screens/seed_screens.py: SeedAddPassphraseScreen and SeedEncryptedQRMnemonicIDScreen now honour ``initial_keyboard`` in _run() (digits / symbols / ABC). The dispatch loop matched the rendered keyboard only by coincidence before; opening with the digits keyboard (PIN entry) and pressing toggle would jump to the wrong layout. - gui/toast.py + gui/components.py: ID_CARD FontAwesome glyph and toast plumbing for the "card inserted" prompt driven by the new CardMonitor. - views/view.py: CardsMenuView entry point so the smartcard flows are reachable from Home without going through Tools (mirrors ToolsSmartcardMenuView). Initialise blank card routes to CardManagementView. - views/tools_views.py: Smartcard Tools menu now exposes Keycard, Common, SeedKeeper, Satochip in a consistent order. - hardware/buttons.py + hardware/displays/desktop_display.py: minor fixes for the desktop emulator (button mapping consistency, display config logging). - tests/test_controller.py: keycard_pins lifecycle tests (set/get/forget/forget_all + wipe-after-set + per-UID isolation). - tests/test_flows_seed.py + tests/test_flows_tools.py: small fixture adjustments for the new CardsMenuView routing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single entry point under Cards (SeedKeeper / Satochip / Keycard); Tools > Smartcard Tools and CardManagementView dropped. Each app View now runs a card_probe gate that routes to CardWaitScreen when no card is present and to the matching setup wizard when the applet is uninstantiated. Card insert/remove fan out through new Controller listeners; a CardRemovedToast mirrors CardInsertedToast. Adds a user-tunable Screensaver timeout setting (Off + minute presets) via a Controller property so changes take effect without restart. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Probe SELECT-probes Keycard/Satochip/SeedKeeper AIDs to render a check/empty-box on each Cards entry, so the user sees at a glance what is installed. When the per-app gate finds the applet missing, it now offers "Install applet" instead of dead-ending: a kind-aware CardsInstallAppletView shells `seedkeeper_utils.run_globalplatform` with the proven `--install`/`--load`+`--create` recipes against the .cap files baked at <microsd>/javacard-cap/. Each app's Advanced submenu gains an "Uninstall applet" entry. Keycard uses the native GP secure channel + delete_aid for atomic package removal; Satochip and SeedKeeper shell gp.jar. A new top-level "Factory reset card" entry tries the GP-authenticated wipe first and falls back to Keycard's cleartext FACTORY_RESET when ISD keys are rotated, with a clear notice that Satochip/SeedKeeper need a PC for that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test_bip85_pgp_cli: extend the Brainpool curve `group_order` shim to P-384 and P-512 (pgpy ships them as abstract); RFC 5639 values. * test_io_config_profiles: desktop runtime profile default is desktop_320x240, update the assertion to match the source of truth in `Settings.get_platform_default_display_config`. * test_settings::test_reset_settings: previously asserted DISABLED against PERSISTENT_SETTINGS, which now defaults to ENABLED — pick a setting (SLIP39_SEEDS) whose default differs from the value we set, and assert post-reset matches `SettingsDefinition.default_value` so the test cannot drift again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keycard: probe_installed_applets now iterates instance suffixes 0x01..0x0F (preferring controller.active_keycard_aid first), so keycard-shell cards on a non-default suffix render the install checkmark on first visit rather than only after entering the menu. SeedKeeper: writes on v0.2 cards now use type=BIP39 mnemonic subtype 0 instead of Masterseed subtype 1. The Seedkeeper-iOS app only parses bip39Mnemonic/password types, so revealing entries written by our device previously crashed in its parser. Read path keeps the Masterseed parser for backward compat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…→setup init_satochip() used to prompt for the existing PIN before calling card_get_status(), so a fresh applet showed a meaningless "Card PIN" screen (any value accepted) before the real "Card Uninitialised — set a device PIN" + "New Card PIN" flow. Defer the PIN prompt until the status check confirms setup_done=True, and unify cache-miss/card-changed handling in one place. Also route CardsInstallAppletView directly into the per-applet setup view (ToolsKeycardInitView / ToolsSatochipSetupView / ToolsSeedkeeperSetupView) with a "Set up" button on the success screen, instead of going to the kind-specific menu and letting the gate re-probe and re-redirect. Keycard flow unchanged — its gate already routes uninitialised cards straight to ToolsKeycardInitView with no PIN prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the install-success screen ("Set a device PIN to continue") routes
straight into the setup view, init_satochip() showed a second
"Card Uninitialised — Set a device PIN to complete Card Setup" warning
before the "New Card PIN" keyboard. Two screens carrying the same
message, back-to-back. Drop the warning; the keyboard's "New Card PIN"
title is enough context for both the install-flow and the direct-entry
case (Cards → Satochip/SeedKeeper on an uninstantiated card).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Cards fixes, all SeedKeeper-flavoured: 1. SECRET_TYPE_PASSWORD writes now include the trailing `login_size(0)` and `url_size(0)` bytes required by the applet's Specifications.md. Without them, the iOS Satochip app's Password parser reads past the buffer and crashes on Reveal — and on the secret-list refresh it does right after change-PIN succeeds, which is why change-PIN appeared to crash too. Fixes both write sites (Tools → SeedKeeper → Save Password to Card, and the SLIP39-share-as-Password path in the seed save flow). Existing entries written by previous firmware remain malformed; wipe + re-save them. 2. CardsInstallAppletView now calls disconnect_smartcard_connections() between the successful gp.jar install and the routing to the setup view. The pre-refactor flow bounced through the kind-specific menu which re-probed via run_card_gate (releasing readers, killing scdaemon); c80aea5a's direct-to-setup hop skipped that, leaving a stale connector and held scdaemon — symptom was "Set up" doing nothing or hanging after a factory-reset+reinstall. 3. Restore the SeedKeeper storage chooser (4/8/16/32/64 KB → 0FFF / 1FFF / 3FFF / 7FFF / FFFF) that PR 3rdIteration#189 added to the pre-dfca575f DIY-tools install view and that the Cards refactor dropped. Default highlighted is 8 KB (1FFF), matching the prior implicit default. Also folds in two pre-existing working-tree tweaks in tools_views.py: the dead PACKAGE_AID constant in ToolsSatochipUninstallAppletView is removed, and the SeedKeeper --delete AID typo `5365656b6565706572` ("Seekeeper") is fixed to `536565644b6565706572` ("SeedKeeper"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old _attempt_globalplatform_wipe ran the whole reset through the in-tree Python SCP02 stack and called gp.delete_aid() per enumerated AID. That issues a single DELETE APDU and bubbles the card's SW as an exception — fine for most applets, but SeedKeeper rejects the one-shot DELETE. The exception was caught silently and surfaced as "1 skipped" in the factory-reset report, leaving the applet in place. Replicate what the per-applet ToolsSeedkeeperUninstallAppletView does: keep the SCP02 channel as an auth probe (so rotated ISD keys still fall back to the soft-reset path), keep gp.list_instances for enumeration, but disconnect the Python channel before deleting and shell out to `gp.jar --delete <AID> -force` per AID. -force is what makes gp.jar paper over the SWs the Python stack treats as fatal, and the per-applet Uninstall flow already proves it works for SeedKeeper, Satochip and Keycard alike. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…files
(A) Factory reset now drives entirely off probe_installed_applets +
gp.jar — no Python SCP02 channel. The previous flow opened SCP02 via
the in-tree Python stack purely to enumerate applets, then disconnected
with pyscard's default LEAVE disposition and called gp.jar per AID.
The card retained ISD-selected + session-counter state from the Python
side; gp.jar's subsequent SCP02 handshake produced cards that rejected
DELETE with SW=0x6985 ("Could not delete X. Some app still active?").
Same gp.jar command from a cold start (Tools → SeedKeeper → Advanced →
Uninstall) always worked — the only difference was the prior Python
SCP02 priming. Switching the enumerate step to cleartext SELECT via
probe_installed_applets removes the priming entirely. For Keycard we
pass the 7-byte package AID A0000008040001 so a single --delete -force
wipes the multi-instance package atomically.
(B) Earlier factory-reset builds (before this change) deleted Keycard
instances individually but never the package, leaving the load file as
an orphan. probe_installed_applets's SELECT to instance AIDs then
reports Keycard as "not installed", but gp.jar --load on a fresh install
hits "Applet loading not allowed". Add -force to every install command
template (satochip --install, seedkeeper --install, keycard --load) so
a stale load file is wiped before the new one is loaded. Idempotent
when there's nothing to override.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits Controller.on_card_removed into wipe_card_session_secrets (runs immediately, never debounced — leaking secrets across cards is far worse than an extra PIN re-entry) and dispatch_card_removed_event (toast + listener fan-out, debounced in CardMonitor to absorb the spurious removed/inserted bounces common on contactless and loose USB readers). CardsMenuView now registers as a card-event listener and re-probes / re-renders when a card is inserted or removed while the menu is on screen, so the checkmark state stays in sync without a back-out round trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ory reset The probe only SELECTs against instance AIDs, so an orphan 7-byte Keycard package (load file with no instances) is invisible to both the Cards menu and the factory-reset target picker. Install then hits "Applet loading not allowed" because gp.jar's --load -force fails to clean up the stale package on some cards. Add an unconditional gp.jar --delete A0000008040001 -force before --load (silenced via a new suppress_failure_dialog kwarg on run_globalplatform so a clean card doesn't show a spurious "Failed" toast) and always include Keycard in the factory-reset target list so the orphan is reachable from the existing wipe path too. Per-step error trailer added to install failures so a future regression is diagnosable without peeking at stderr. Also fixes two stale assertions in test_controller's defaults check (PERSISTENT_SETTINGS and CAMERA_ROTATION defaults changed upstream). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``RET_CODE__BACK_BUTTON`` and ``HardwareButtonsConstants.OVERRIDE`` are both numerically ``1000``, so the previous check intercepted every back press as a card-event refresh, dropped the user back into the same menu, and the next ENTER landed on SeedKeeper. Discriminate via the listener flag instead: only ``trigger_override`` sets ``refresh_requested[0]``, so checking it first cleanly separates a real card event from a back press. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Keycard-3.2.cap ships three applets (signing, NDEF, Cash) and our install recipe was creating all three. The NDEF instance uses the ISO/IEC 14443 Type-4 standard AID (D2760000850101), which iOS Core NFC auto-SELECTs on tag discovery. On a card that also has SeedKeeper loaded, the Seedkeeper-iOS app then talks to the wrong applet on the secret-reveal path and the app crashes (the label is still readable because it comes from an independent code path that hits SeedKeeper directly). Our Python firmware was unaffected because select_with_ autodetect always SELECTs the signing AID by name. Reduce both install paths (CardsInstallAppletView and the legacy ToolsDIYInstallAppletView) to create only the signing instance (A0000008040001010101). The Cash applet is dropped at the same time — it's only used by Status Pay and no wallet or tool we target uses it. No migration UI needed: the unconditional `gp.jar --delete A0000008040001 -force` we already run before --load cascade-deletes the prior signing + NDEF + Cash instances on the same package, so an affected card is cleaned up by simply reinstalling. Tests in test_cards_install_and_wipe.py updated to expect the new 3-step recipe (pre-delete + --load + 1 × --create). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controller.VERSION -> '0.1.1'; CHANGELOG: this release is 0.1.1 and the prior Keycard-Edition-B1 entry is renamed 0.1.0, matching the GitHub release names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Controller.VERSION carries the brand + version on the opening splash (release/tag stay 0.1.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap handling hardening across multi-card / multi-instance sessions: - open_unlocked_session now detects a card/instance swap synchronously at SELECT (compares the freshly-SELECTed instance_uid against the new Controller.last_authenticated_keycard_uid marker) and scrubs the previous card's session secrets before touching the new card. This is the reader-independent backstop for the unreliable PC/SC 'removed' event (notably NFC/PN532 via ifdnfc). The marker is distinct from last_keycard_uid (which enumeration/name-resolution mutate) and is cleared by forget_all_pins(). - KeycardCardChangedError in the ETH/BTC sign/export finalize handlers now follows the no-card retry-without-rescan principle: keep the scanned request / PSBT / message and route via _pair_then_resume(...) -> ToolsKeycardPairView(next_destination=...), which resumes the originating step after a successful pair (both hops skip_current_view so the back stack ends up as if the pairing detour never happened). - SETTING__CACHE_SCARD_PIN default flipped Disabled -> Enabled: the PIN is reused across Home for fewer prompts, dropped only on swap/removal/Lock card/instance-switch/wipe-timer (all ungated). Help text + 9-locale catalogs updated to match. Cover: test_keycard_session_auto_switch.py, test_controller.py, test_keycard_views.py. Docs: AGENTS.md threat-model + no-card sections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToolsKeycardInstancesCreateView now sets active_keycard_aid to the freshly-installed AID after a successful INSTALL. The new instance is uninitialised, so this makes the announced "Run Init next" actually target it: select_with_autodetect first-tries the active AID, so without the switch the Init wizard would re-SELECT the previous (already-initialised) instance and bail with "Already initialised", forcing the user through Switch instance first. Symmetric with the delete path, which falls the active AID back to the default when the active instance is removed. Cover: test_create_auto_switches_active_instance. Docs: AGENTS.md menu ordering + "Active instance for the session" notes corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…byte tools - calldata_decoder: thread the tx `value` into pages_for_calldata so the decoded view can reflect native-value transfers alongside the call - keycard_views (ETH sign): render a non-UTF8 personal_sign payload as 0x-prefixed hex instead of raw hex that looks like text - function_registry: extend known selectors; add tests - scripts/: fetch_4byte_signatures.py + probe_eth_signature_coverage.py + fetch_eth_signatures.md to refresh/measure 4byte selector coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the single hard-coded Snake boot game with a pluggable "games console": boot shows a game-select menu and dispatches to a mini-game, returning to the menu on game-over. The configured unlock sequence still exits the whole console into the firmware from anywhere (menu or any game), and the KEY1+KEY2+KEY3 10s panic exit still disables stealth boot. - stealth/base.py: BaseStealthGame contract (reset/tick_ms/step/ handle_key/draw_frame); shared render + render_game_over + grid layout - stealth/console.py: StealthConsole owns the input thread, UnlockBuffer (fed every key, everywhere), panic, game registry and play loop; KEY1 returns to the menu mid-game - stealth/snake.py: reparented onto BaseStealthGame; walls now WRAP (modulo) instead of killing — self-collision is the only death - stealth/game_2048.py, tetris.py, dino.py: three new games - controller.start(): launch StealthConsole().run() instead of SnakeGame - Tools > Stealth boot: inline description via new DescriptionButtonListScreen; translated into all 9 catalogs (.po/.mo) - tests: wrap-around, 2048/tetris/dino mechanics, console menu/unlock/ KEY1/panic; boot-integration updated to StealthConsole Import isolation preserved (test_stealth_import_guard scans the package). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The top menu probed instance count via _probe_keycard_instance_count,
which deliberately returned None for 0/1 instances (>=2 only); the
authoritative _count_keycard_instances meant to cache an exact 1 is dead
code. So a single-instance card never reached count==1: "Switch instance"
stayed visible and titles kept the noisy "· Inst N" suffix.
The fast probe already SELECTs every valid slot (1..MAX, both AID forms),
which this firmware/keycard-cli are the only minters of, so it reliably
distinguishes "exactly 1". Make it return the exact count (len(slots) or
None) — still no ISD handshake on menu entry.
Add _instance_title_suffix(): a user-assigned name always shows; the auto
"Inst N" is dropped only when count==1; unknown count keeps the label.
Apply to all 6 instance-scoped menu titles plus the Wallets/Addresses
list tags. Destructive "Wipe {}?" keeps a concrete identifier.
l10n: add bare Pairing/Manage Inst/Wallets/Addresses to all 9 catalogs
(derived from the existing compound strings); recompile .mo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nces) The blank-card install minted the first signing instance at the 10-byte legacy AID A0000008040001010101 (pinned to commands.APPLET_AID), while "Create instance" (_next_free_instance_aid) mints the 9-byte canonical form A000000804000101+slot that real Status cards / keycard-cli use. So the first instance ended ...010101 and the second ...0102 — inconsistent. Unify on the 9-byte canonical form: APPLET_AID and the blank-card --create both become A00000080400010101 (slot 1). select_with_autodetect still probes the 10-byte legacy form, so cards already in the field keep resolving via one extra 0x6A82+probe. The smoke test's bare client.select() now probes both forms on 0x6A82 too (no Controller needed). Tests: pin APPLET_AID to the 9-byte value; slot-1 mint now equals APPLET_AID (old "must differ from 10-byte default" invariant dropped); blank-card create assertion updated. Legacy-card autodetect already covered by test_probes_legacy_10byte_form. Hardware verification still required (smoke test on 9-byte + legacy cards, blank-card install, create 2nd instance) before relying on this on-device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump Controller.VERSION to Keycard-Edition-0.1.2 (shown on the opening splash via views/screensaver.py) and add the 0.1.2 CHANGELOG entry covering the work since 0.1.1: 9-byte AID unification, instance auto-activate, single-instance menu cleanup, reader-independent card-swap detection + retry-without-rescan, the stealth games console, and the ETH calldata decoding improvements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…menu entry The PC/SC `removed` event is unreliable on contactless readers (NFC/PN532 via ifdnfc), and the synchronous swap detection only lived inside open_unlocked_session() — which the View wallets / View addresses flows SKIP when the address cache is warm (they serve keycard_wallets_data straight from the per-AID cache without touching the card). Result: after swapping cards, the previous card's addresses were still shown. Fix: extend the synchronous, reader-independent identity check to the choke-points that display/reuse cached card data, instead of trying to fix the unreliable async event. - ui_helpers: extract detect_card_swap(controller, instance_uid) from open_unlocked_session (behaviour-preserving); it wipes session secrets on a UID mismatch and deliberately does NOT claim an authenticated session (never sets last_authenticated_keycard_uid). Add verify_active_card_unchanged() — a cheap SELECT-only guard built on identify_inserted_card + detect_card_swap. - card_probe.run_card_gate: run detect_card_swap on every Keycard menu entry, reusing the SELECT the card probe already did (zero extra I/O), so re-entering the menus wipes stale state for a swapped card. - keycard_views: guard the warm-cache branch of ToolsKeycardWalletsListView (ETH) and ToolsKeycardBtcAddressesListView (BTC); on a detected swap the cache is wiped and the view re-runs (skip_current_view) to re-derive for the new card; no-card routes via _no_card_toast_or_error. - Tests: new TestCardSwapDetection (10 cases) covering detect_card_swap, the warm-cache guard (swap / same-card / no-card, ETH+BTC) and the menu gate; fix test_card_probe _make_view to init last_authenticated_keycard_uid to None (real controller default) so the gate's swap check doesn't trip. - AGENTS.md: document the three swap-detection choke-points. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iOS coexistence warning: replace the blocking DireWarningScreen with a
subtle non-blocking InfoToast (_toast_ios_coexistence) on both install
paths (CardsInstallAppletView + install_seedkeeper_applet). The old gate
was too invasive — it fires correctly even for an uninitialised Keycard
instance, since the iOS crash depends only on the Keycard package being
present, not on init state.
Low-space check: stop false-positiving on a card that already holds the
Keycard package. required_nv_for_install now estimates the real on-card
footprint (real_nv_footprint = cap_size * CAP_NV_FOOTPRINT_RATIO 0.70)
instead of the raw .cap size, which over-estimates by ~1.3-1.5x (it
bundles Debug/Descriptor/Export components stripped at load). Add a
LOW_SPACE_MARGIN_BYTES (2048) tolerance so borderline fits don't warn.
The same ratio feeds estimated_used_nv (Storage occupation bar).
Show the card's free memory as 'Free: X KB' on the SeedKeeper Select
Storage chooser (DescriptionButtonListScreen), reading CardMemory once
and threading it into _warn_if_low_space(..., mem=) to avoid a 2nd query.
l10n: add 'Free: {} KB' and 'iOS Seedkeeper may crash' to all 9 catalogs
(+recompile .mo). Update tests and AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ooser - ETH derivation-scheme chooser (Standard m/44'/60'/0'/0/i vs Ledger Live m/44'/60'/i'/0/0) for View wallets + Connect software wallet, with per-scheme address-cache namespacing and a Ledger Live account picker. - Import hex (NGRAVE) Type-hex path gains a 24/12-word length chooser that caps input via the new KeyboardScreen.max_input_length + GroupedHexEntryDisplay. - Seedkeeper backup at creation simplified to the single iOS-compatible 'BIP39 mnemonic' format (drop the non-round-tripping Password option). - l10n: new strings propagated across all 9 catalogs. Tests run in Docker/CI (local env lacks embit/Cryptodome). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Held buttons no longer 'run on': the input loop now drains the whole queue
each frame and throttles per-key game/menu actions to ~10/s, so a held key
stops the instant it is released. Previously a background thread dumped every
HardwareButtons.wait_for() repeat into an unbounded queue while the game loop
consumed one per frame, so a held button built a backlog that kept acting for
seconds after release (most visible as Tetris soft-drop overshoot). The unlock
combo still sees every raw key, so repeated-key combos keep matching.
Menu redesigned ('arcade cards'): real OpenSans typography via a cached
stealth_font() helper, per-game accent swatches, highlighted selected card,
header + footer hints. All decorations are drawn as shapes (OpenSans has no
geometric-shape glyphs); the styled path falls back to a plain render if a
font ever fails, so the boot screen can't crash.
Full visual refresh: game-over screen and every in-game HUD label use the
font; 2048 tile numbers are sized to the cell.
Tests: de-flood/no-backlog, distinct-keys, raw-unlock regression, menu render
(57 stealth tests pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…icker Reword the derivation-scheme chooser (shared by View wallets and Connect software wallet): "Default (this device)" -> "Standard (BIP-44)", title "Wallet type" -> "Account layout", and replace the awkward description with "Wallets number accounts differently. Match your app." Make the Ledger Live export picker paginated and show each account's address so the user can verify the account before exporting. Drop the static, 10-cap ToolsKeycardEthLedgerAccountView and add a mode="export" to ToolsKeycardWalletsListView, which already derives m/44'/60'/N'/0/0 under the Ledger Live scheme with per-AID caching, swap detection and "Next 10" paging. Rows render as "N:0x..addr.." and selecting one exports that account's xpub. Update the routing/derivation tests and the two warm-cache swap tests to thread the new mode. Replace the three changed msgids across all 9 catalogs (+ messages.pot) with translations and recompile the .mo files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded 4-instance cap with a memory-aware limit. The card reports only free NV (GET DATA 0xFF21), so per-instance cost is estimated and refined by a self-calibrated free-NV delta around INSTALL. - MAX_KEYCARD_INSTANCES 4 -> 16 (firmware ceiling, not hardware). - view.py: KEYCARD_INSTANCE_NV_ESTIMATE_BYTES (3 KB, calibratable) + keycard_instance_nv_estimate() + estimate_remaining_instances(). - Controller.keycard_measured_instance_nv (card-specific, wiped on swap). - Create flow: soft "Create anyway" warning when no room + "~N more fit" on confirm; self-calibrates from the INSTALL free-NV delta. - Storage view/screen: "~N more instances fit" line. - Fast menu probe made bounded (canonical-only, early-exit at 2) so the raised ceiling doesn't re-introduce the menu-entry stall; exact census stays in the management flows (_probe_keycard_instance_count_exact). - l10n: 4 new strings across all 9 catalogs (+ recompiled .mo). - Tests + AGENTS.md docs updated. NOTE: KEYCARD_INSTANCE_NV_ESTIMATE_BYTES is a conservative placeholder; calibrate on hardware (free-NV before/after a real create+init). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erwrite guard This instance now holds only safe per-instance management (Change PIN, Unblock PIN, Rename, Lock) plus a "Set up / reset ›" pointer. The four destructive ops (Generate key, Import seed, Initialise instance, Factory reset) move one level down into the new ToolsKeycardSetupResetMenuView, so the routine path can't fat-finger a key overwrite. Generate key / Import seed now warn explicitly before replacing an existing key: _instance_key_present does a SELECT-only read of the unauthenticated key_uid and the warning becomes "Replace key?" when a key is loaded. The probe is advisory — any failure (incl. no card) falls back to the generic warning and lets the downstream flow surface real card errors, so the entry views gain no new no-card branch. Added select_inserted_card (factored from identify_inserted_card) to expose the full SelectResult without a second round-trip. Tests: This-instance routing (5/4 counts), no-destructive-or-pairing guard, new setup-reset routing + overwrite-guard tests; setup-chain reorg-reachability now routes through the submenu; unblock-pin index fix. Docs: AGENTS.md menu-organisation section. l10n: 5 new strings added to all 9 catalogs and .mo recompiled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…& MAC compares Audit of the Keycard/SeedKeeper firmware confirmed no secret material reaches persistent storage. Findings were all in-RAM hygiene + small hardening: - F1: the seed import/generate/backup flows wiped their byte buffers and word lists but left the *joined* plaintext mnemonic string and the *decoded* passphrase string (each holding the whole secret in one immutable allocation) unwiped until GC. Wipe both via wipe_string() in a finally on every exit path in ToolsKeycardImportSeedView, ToolsKeycardImportSeedkeeperPushView, ToolsKeycardGenerateSeedLoadView, and ToolsKeycardSeedkeeperSaveRunView (the last gained a try/finally; its int-list copies are overwrite-then-clear'd, best-effort). Controller pending_* copies stay under the existing error-vs-user-exit rule, so the Seedkeeper "Both" flow is unaffected. - F2: decode_seedkeeper_seed_secret() read length-prefixed fields off an untrusted card with no bounds checks (IndexError on a malformed/hostile secret) and accepted any entropy length. Bounds-check every field and reject non-canonical BIP-39 entropy lengths, returning None instead of raising. - F3: use hmac.compare_digest for the secure-channel response MAC check and the KEF encrypted-QR auth tag (constant-time). Tests: new tests/test_keycard_secret_wipe.py (4 flows + wordlist integrity) and malformed-payload cases in test_keycard_seedkeeper_import.py. Full suite green (1195 passed, 3 skipped). AGENTS.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump Controller.VERSION to Keycard-Edition-0.1.3 (shown on the opening splash via views/screensaver.py) and add the 0.1.3 CHANGELOG entry covering the work since 0.1.2: in-RAM secret-wipe hardening + Seedkeeper decode bounds-checks + constant-time MAC compares, the destructive ops moved into a Set up / reset submenu with a "Replace key?" overwrite guard, the memory-aware instance limit (cap 16 + free-space estimate), the ETH account-layout chooser + paginated Ledger Live picker, the NGRAVE hex length chooser, the stealth held-button fix + arcade menu, the non-blocking iOS toast + recalibrated low-space check, and card-swap detection extended to the warm address-cache views and menu entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, step back a word) Republish 0.1.3 with two changes on top of the previous (broken) build: - Create instance regression: read the card's free-NV in the clear BEFORE the SCP02 secure channel opens. The prior build issued an un-MAC'd GET DATA over the open channel, which desynced it so the next INSTALL was rejected with 0x6982 "Security status not satisfied". _open_isd_channel now takes read_free_nv= and returns (channel, instances, connection, free_nv); the post-INSTALL self-calibration (also a read over the open channel) is dropped in favour of the conservative per-instance estimate. - Seed word entry (Import seed > Type words): D-pad navigation skips greyed-out letters and the cursor jumps to the next still-available key (opt-in Keyboard.skip_inactive_keys); after locking a letter the cursor moves off a now-greyed key onto the first available letter. The back arrow steps to the previous word, re-opened pre-filled for editing, instead of discarding all progress (only backs out of the flow on word 3rdIteration#1). Tests: new tests/test_keyboard_skip_inactive.py; extended tests/test_keycard_import_entry.py (back-one-word) and tests/test_keycard_views.py (free-NV read before open). Full suite: 1204 passed, 3 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… uninitialised-instance loop Import seed (Tools > Keycard … > Import seed) UX fixes plus a real bug fix: - Reorder source menu so "Scan SeedQR" is last; manual entry first. - Rename "Type words" → "BIP39 seed phrase". - Word-entry keyboard: stop auto-skipping greyed keys (skip_inactive_keys = False). Greying stays as a visual hint but the cursor lands on every key, so letters are reachable. - Fix the uninitialised-instance import loop: creating an instance auto- activates a fresh (no-PIN) instance; going straight to Import > From SeedKeeper reached LOAD_KEY on an uninitialised card, which raised KeycardNotInitialisedError and the SeedKeeper handler looped back to the re-insert step. Now both Import seed and Generate key probe init state up front (_instance_initialised) and route to the Init wizard via a "Set up first" explainer. The SeedKeeper push also catches KeycardNotInitialised- Error explicitly (wipes pending seed, clear error) as a backstop so it can never loop. Tests: redirect-to-Init for both entry views, back-button behaviour, no redirect on probe failure, and a no-loop regression for the SeedKeeper push. Updated source-index assumptions in two existing import tests. New UI strings translated into all 9 catalogs + .mo recompiled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "≈N more instances fit" estimate over-promised: it read only free EEPROM, but transient RAM is the binding resource. A field card refused a 5th instance (SW=0x6F00) with ~45 KB EEPROM free; an on-device GET DATA 0xFF21 read confirmed free volatile RAM = 303 B (exhausted), NV = 46596 B, 7 applets. Our 0xFF21 parser was correct — we were just reading the wrong resource and never learning from the failure. - estimate_remaining_instances now gates on free RAM (tag 0x83) too: min(slots, NV, RAM). free_volatile 0/None skips the RAM term (tag absent != no RAM), so cards lacking the tag keep prior NV+slot behaviour. - Install failure with a memory-class SW (0x6A84 or, in the install context, 0x6F00) sets Controller.keycard_install_full -> estimate clamps to 0 for the session (ground truth beats any estimate). New helper is_install_memory_failure; classify_card_error maps 0x6A84 -> "Card full". Reset on card swap. - Self-calibration: after a successful INSTALL, re-read free NV+RAM on a fresh clear channel and store sanity-bounded per-instance deltas (keycard_measured_instance_nv / _volatile). - UX: Storage shows free RAM; create-confirm/warning say "Card appears full" instead of "≈0 more fit"; "Create anyway" override kept. - New KEYCARD_INSTANCE_VOLATILE_ESTIMATE_BYTES=512 (> measured 303 B floor). - l10n: 5 new strings added + compiled across all 9 catalogs. Tests: RAM-gate + 0/None fallback + real-card(46596/303)->0, install clamp on 0x6A84/0x6F00, is_install_memory_failure, wipe resets calibration. Full suite: 1227 passed, 3 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…EY error A transient card error (secure-channel break, PC/SC hiccup, VERIFY stumble) at the final LOAD_KEY push used to discard the validated seed and bounce the user back to the menu — forcing a full re-type of the NGRAVE hex / mnemonic. A second attempt usually succeeds because the card link is fresh. Both push paths now loop: a transient error shows a Retry/Cancel screen and, on Retry, re-opens a fresh session and re-sends LOAD_KEY reusing the already derived seed64 — never re-typing. The seed is wiped only on success, an explicit Cancel, or a genuine card swap. - ToolsKeycardImportSeedView (Scan / Type words / Import hex): retry loop. - ToolsKeycardGenerateSeedLoadView (on-card Generate): same loop; the pending mnemonic/passphrase survive retries (an on-card-generated seed with no paper backup would otherwise be unrecoverable). Cover: tests/test_keycard_import_retry.py — both flows, error->retry->success keeps the words/pending and error->cancel discards and exits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the three screens that instruct the user to pull the card mid-flow (Seedkeeper backup Both/Another-card, and the import-from-Seedkeeper insert/reinsert), the card-removed redirect-to-Home and its 'Card removed' toast would dead-end the 2nd backup/import. Add a declarative classname allowlist (Controller._CARD_VIEW_REDIRECT_EXEMPT_CLASSNAMES) checked via _top_is_card_swap_view() in _on_card_removed_redirect and dispatch_card_removed_event; the secret wipe still fires (never touches the pending seed). Cover: tests/test_cards_menu_view.py::TestCardRemovedRedirect.
The Clone Card Secrets flow was a pure union (add new by fingerprint, never delete the destination). Add a scope chooser (All Secrets / Single Secret) and, for All, a mode chooser: - Add New (keep existing): the prior append behaviour (default, safe). - Replace destination: erase every secret on the destination (seedkeeper_reset_secret per header), then copy -> destination becomes an exact copy of the source. Guarded by a DireWarning that counts what will be deleted and tells the user to use a different card; a v1 destination is refused (can't delete); cancel deletes nothing; source is fully read before the destination is touched. - Single Secret: read source, pick one (reusing the View/Delete-Secrets label scheme), copy it (append, skip-if-exists). Security: new _wipe_collected_secrets() scrubs the plaintext payloads held in RAM across the card swap on every exit (success/cancel/error). i18n: 17 new strings added to messages.pot and all 9 catalogs, .mo recompiled. Tests: tests/test_seedkeeper_clone.py (routing, replace-wipe, v1 guard, cancel-no-delete, single-copy, RAM scrub). Two-card hardware run still pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…skip
Single-secret copy used the same fingerprint dedup as bulk clone and would
report 'Imported: 0, Skipped Existing: 1' when the destination already held a
secret with the SAME CONTENT (Seedkeeper fingerprints are sha256(content)[:8],
not name-based) -- confusing when the match is under a different label.
- Single copy now names the existing entry and asks 'Already on the
destination as {label}. Copy anyway?' (Copy Anyway / Skip) instead of
silently skipping. The named label is also a diagnostic: the source secret's
own label would reveal a source/destination mix-up.
- Defensive zero-fingerprint guard: a placeholder '00000000' is never treated
as a duplicate (excluded from the dest map and the dedup test).
- logger.info() prints the copy counts + destination fingerprints to aid
on-device debugging.
- Bulk All-Secrets copy keeps the silent append dedup (correct for bulk).
Tests: tests/test_seedkeeper_clone.py (copy-anyway imports, skip does not,
zero-fp not deduped); routing assertions updated for the confirm_duplicates arg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pysatochip's card_disconnect() closes the card connection but leaves the connector's RemovalObserver on pyscard's process-wide CardMonitor. That stale observer auto-reconnects to the next inserted card and starves a freshly-built connector across a physical swap — the "Unable to find seedkeeper" failure (no PIN prompt) on the 2nd card of a "Both" backup. Add seedkeeper_utils.teardown_satochip_connector() (card_disconnect + deleteObserver, each guarded) and route disconnect_smartcard_connections / init_satochip / card_probe / reader.release_other_smartcard_holders through it. The Seedkeeper "Both" save tears down save 1's connector while card 1 is still inserted, before prompting the swap.
… first try On the first session-op of a boot no pairing is cached yet, so the final LOAD_KEY push was itself that first op: it raised KeycardCardChangedError, the bare Destination(ToolsKeycardPairView) silently paired (v3.2 ephemeral PSK), then returned BackStackView — re-running the view from the top and discarding the just-typed seed while the card stayed empty. That's the recurring "import doesn't save on the first try" bug. ToolsKeycardImportSeedView / ToolsKeycardGenerateKeyView now call _ensure_card_paired() before any seed is collected (SELECT-only probe → _pair_then_resume pairs and re-enters the same view). The ImportSeedkeeperPush / GenerateSeedLoad push steps handle a mid-flow KeycardCardChangedError via _pair_then_resume without wiping pending_keycard_* (the resumed view re-reads the mnemonic). Cover: test_keycard_import_pairing.py + happy-path in the retry suite.
Two fixes under Tools > Keycard > Settings > Manage Instances > Create: 1. Chain into Init. After a successful create the flow returned to the deep Instances submenu instead of running Init, stranding the user (Init only happened after manual back-nav). ToolsKeycardInstancesCreateView now returns Destination(ToolsKeycardInitView, skip_current_view=True); the pre-existing active_keycard_aid switch makes Init target the new (uninitialised) instance. 2. Monotonic "≈N more fit". The estimate could jump *upward* (3→6) as instances were added: the post-install self-calibration measured a per-instance RAM cost smaller than the constant (RAM is reclaimed once the INSTALL buffers free), inflating the RAM headroom. Remove the self-calibration (keycard_measured_instance_* + _record_instance_nv_ measurement); the per-instance helpers return fixed conservative constants. Add view.apply_capacity_cap + Controller.keycard_capacity_ estimate: the predicted total capacity only ratchets down, so a max-4 card now reads 3 → 2 → 1 → "Card appears full". Delete resets the cap and the install-full clamp (a freed slot makes room again). Free EEPROM/RAM + instance count are still read live from the card; only the per-instance cost is a constant (the card never reports per-app usage). Cover: test_keycard_storage.py::TestApplyCapacityCap, test_keycard_views.py ::TestCreateInstanceMemoryGate (cap anchors/clamps, Delete reset) + test_create_auto_switches_active_instance (Init destination), test_controller.py (wipe resets cap+clamp). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…setting, context-aware spinner - Remove the Donate screen/button and 11 unused/legacy settings left over from the on-device-seed era (Xpub export/details, BIP32 account prompt, Seed word lengths, Plaintext/Encrypted QR, privacy/dire warnings, BitBox/Passport/ TAPSIGNER backups) plus the now-empty Load Backup Files submenu and the vestigial Smartcard support toggle (smartcard tools are always available now). - Remove the Bitcoin-network setting (this fork is mainnet-only): hardwire mainnet in the BTC amount component and drop the orphaned NetworkMismatchErrorView. - Context-aware loading spinner: parametrize LoadingScreenThread (neutral KeyCard default + BTC/ETH presets), generate keycard_60x60.png, and thread the chain context through ScanView -> ScanScreen so Bitcoin/Ethereum scans and address derivation show their own logo instead of always the Ethereum one. - Settings back-compat preserved: removed keys are silently ignored on load (settings.json and SettingsQR). Tests updated for the removed settings; add tests/test_loading_spinner.py and a settings back-compat regression test. - Bump VERSION to Keycard-Edition-0.1.4 and add the 0.1.4 CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
|
Hey thanks for the PR. There are many, many conflicts with this, probably due to some work I did on it over the last week or two, but can you rebase against the latest dev? (The functionality is there in B11 if you want to check it out) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updates the Keycard / SeedKeeper-only firmware branch from ethermachine/dev.