Skip to content

KeepKey hardware wallet integration — Phase 1 (account setup + Orchard PCZT signing)#1

Draft
pastaghost wants to merge 18 commits into
mainfrom
feature/keepkey-phase1-account-setup
Draft

KeepKey hardware wallet integration — Phase 1 (account setup + Orchard PCZT signing)#1
pastaghost wants to merge 18 commits into
mainfrom
feature/keepkey-phase1-account-setup

Conversation

@pastaghost
Copy link
Copy Markdown

Summary

  • Adds full KeepKey USB hardware wallet support to zodl-android, mirroring the existing Keystone integration pattern
  • Phase 1: account setup (connect → FVK export → address display → account import) and Orchard PCZT signing
  • USB HID transport with 64-byte packet framing, protobuf encoding via device-protocol protos

What's included

Transport & Protocol

  • KeepKeyTransportProvider — USB HID framing (buildKeepKeyPackets / parseKeepKeyPackets), 64-byte packets, Android UsbManager integration
  • KeepKeyEmulatorTransportProvider — HTTP bridge transport for the kktech/kkemu Docker emulator (integration testing without physical hardware)
  • KeepKeyEmulatorDebugLink — debug link for LoadDevice / button automation in emulator tests
  • KeepKeySigningProtocolZcashSignPCZT → ZcashPCZTAction × N → ZcashSignedPCZT state machine
  • OrchardUfvkEncoder — ZIP-316 Bech32m encoding for UFVK strings
  • Blake2b — pure-Kotlin BLAKE2b-256 for seed fingerprint derivation

Use Cases

  • GetKeepKeyOrchardFVKUseCase — USB permission + connect + ZcashGetOrchardFVK + UFVK encode + seed fingerprint + unified address derivation
  • ImportKeepKeyAccountUseCase — SDK account import + navigation
  • KeepKeyProposalRepository — proposal lifecycle, PCZT creation, signing orchestration (mirrors KeystoneProposalRepository)

Screens

  • connectkeepkey/ — connect, connected, date, estimation, height, neworactive sub-screens (mirrors connectkeystone/)
  • selectkeepkeyaccount/ — account confirmation screen showing derived unified address before import
  • signkeepkeytransaction/ — signing screen with progress indicator, error/cancel handling

Testing

  • 6 HID framing unit tests (KeepKeyFramingTest)
  • 11 signing protocol unit tests with fake transport (KeepKeySigningProtocolTest)
  • 17 BLAKE2b unit tests against RFC 7693 vectors (Blake2bTest)
  • 10 Compose UI tests for connect screens (KeepKeyConnectViewTest)
  • 12 Compose UI tests for signing screen (SignKeepKeyTransactionViewTest)
  • 13 emulator integration tests against kktech/kkemu Docker image — auto-skip when emulator is unreachable (KeepKeyEmulatorIntegrationTest)

Test plan

  • Run ./gradlew :ui-lib:compileZcashmainnetInternalDebugKotlin — must be clean
  • Run ./gradlew :ui-lib:testZcashmainnetInternalDebugUnitTest — all unit tests pass
  • Start Docker emulator (cd keepkey-firmware/scripts/emulator && docker-compose up) and run instrumented integration tests
  • Manual OTG test: connect physical KeepKey, verify USB permission dialog, FVK export, and account import flow
  • Manual signing test: send a shielded Orchard transaction with physical KeepKey

Derivation path

  • Orchard (ZIP-32): m/32'/133'/0'
  • Transparent (BIP44): m/44'/133'/0'/0/0

Not included in this PR (deferred)

  • Phase 3: transparent → Orchard hybrid shielding (ZcashTransparentInput / ZcashTransparentSig)
  • WalletConnect transport (exploratory — kept decoupled from core signing logic)
  • Multi-account support (KeepKey exports account 0 only for now)

🤖 Generated with Claude Code

pastaghost added 18 commits May 21, 2026 00:04
Adds the build infrastructure, USB HID transport implementation, and
KeepKeyAccount model needed for KeepKey hardware wallet integration.

- Add com.google.protobuf Gradle plugin (v0.9.4) and protobuf-kotlin-lite
  (v4.28.2) to settings.gradle.kts and ui-lib/build.gradle.kts
- Create ui-lib/src/main/proto/ with README listing required firmware
  proto files (messages-zcash.proto, messages.proto, types.proto)
- Implement KeepKeyTransportProviderImpl: 64-byte HID framing (0x3F
  header), writePackets/readPackets, USB endpoint discovery, GetFeatures
  call to read firmware version
- Add KeepKeyAccount to the WalletAccount sealed interface hierarchy;
  stores seedFingerprint for per-session device binding
- Add ic_item_keepkey.xml drawable (light + dark), keepkey_account_name
  and connect/error string resources, keepkey_device_filter.xml
- Wire USB_DEVICE_ATTACHED intent filter and meta-data into MainActivity
- Register KeepKeyTransportProviderImpl in Koin ProviderModule

Note: proto files must be copied from keepkey-firmware/deps/device-protocol/
at commit de297b7a before the build will generate message bindings.
Lockfiles will need updating after first Gradle sync.
Copies messages-zcash.proto, messages.proto, and types.proto from
keepkey/device-protocol release/7.14.1-device-protocol into the Gradle
protobuf source set. Enables generateProto to produce Kotlin lite
bindings for all ZCash KeepKey messages (IDs 1300–1309).
Implements the complete account-setup UI for KeepKey Phase 1:

- ConnectKeepKeyUseCase: sends ZcashGetOrchardFVK (msg 1304), decodes
  ZcashOrchardFVK (msg 1305), encodes the UFVK via ZIP-316 + Bech32m,
  derives a stable seed fingerprint, imports the account and navigates.
- Blake2b (pure Kotlin, API-27 safe) and OrchardUfvkEncoder (F4Jumble +
  Bech32m) in the new crypto package.
- connectkeepkey/connect screen: KeepKeyConnectState/VM/View/Screen
- connectkeepkey/connected screen: success confirmation after pairing
- GetKeepKeyStatusUseCase: ENABLED when no KeepKey account exists
- IntegrationsVM: adds KeepKey list item (UNAVAILABLE hides it)
- AccountListVM: "Connect Hardware Wallet" button now routes to
  IntegrationsArgs and hides when any HW account (Keystone or KeepKey)
  is present
- KeepOpenFlow.KEEPKEY + KeepOpenVM strings for the sync-wait screen
- DI: factoryOf(ConnectKeepKey/GetKeepKeyStatus), viewModelOf(KeepKeyConnectVM)
- Nav graph: ConnectKeepKeyArgs and KeepKeyConnectedArgs routes
- String resources (en + es) for connect, connected, keep-open, and
  integrations screens
- ic_integrations_keepkey drawable (light + dark)
…type addition

- Add exhaustive `is KeepKeyAccount` branches to all `when` expressions
  across use cases, VMs, and repositories (Phase 2 paths throw; display
  paths use KeepKey icon/string/enum; proposal flows return flowOf(null))
- Fix protobuf Kotlin DSL: id("java"/"kotlin") → create("java"/"kotlin")
- Fix BLAKE2b IV literals: hex constants that overflow signed Long now use
  java.lang.Long.parseUnsignedLong for correct two's-complement bit patterns
- Fix AccountDataSource: seedFingerprint read from sdkAccount.seedFingerprint
  directly (not via .purpose which does not exist on Account)
- Remove @throws annotation from ConnectKeepKeyUseCase class level (invalid target)
- Fix duplicate keepkey_account_name string resource
- Add importKeepKeyAccount stub to FakeAccountDataSource in unit tests
- Build verified: compileZcashmainnetInternalDebugKotlin succeeds
- Tests verified: testZcashmainnetInternalDebugUnitTest passes
Add requestPermission() to KeepKeyTransportProvider interface and impl.
Uses a one-shot BroadcastReceiver with suspendCancellableCoroutine to
suspend until the user responds to the system permission dialog, then
unregisters the receiver. ConnectKeepKeyUseCase now calls requestPermission()
before connect(), throwing on denial.
Implements the proposal lifecycle for KeepKey USB signing:
- createProposal / createExactInput/OutputSwapProposal / createZip321Proposal / createShieldProposal
- signAndSubmit(): createPczt → addProofs → redactForSigner → USB signing exchange → broadcast
- Full ZcashSignPCZT → ZcashPCZTAction × N → ZcashSignedPCZT message loop
- Registered as a Koin singleton in RepositoryModule

Two SDK gaps are documented with TODO(sdk) comments and stubs that throw
UnsupportedOperationException at runtime until resolved:
  1. Extracting per-action fields (n_actions, digests, alpha, cv_net, …) from a Pczt
  2. Inserting RedPallas signatures back into the Pczt (Synchronizer.addSpendAuthSigsToPczt)
State/VM/View/Screen for the USB signing confirmation flow:
- "Confirm on KeepKey" title + subtitle from string resources
- CircularProgressIndicator while signAndSubmit() is in-flight
- Success navigates to TransactionProgressArgs via replace()
- Failure surfaces error message; cancel/back calls CancelProposalFlowUseCase
- SignKeepKeyTransactionVM registered in ViewModelModule (Koin)
- SignKeepKeyTransactionArgs registered in WalletNavGraph
- Spanish string translations added alongside English
Replace error() stubs in CreateProposal, GetProposal, ObserveProposal,
SubmitProposal, and CancelProposalFlow with real KeepKeyProposalRepository
calls. SubmitProposal now navigates to SignKeepKeyTransactionArgs for
KeepKey accounts. CancelProposalFlow clears the KeepKey repo on cancel.
Add new-or-active, date, estimation, and height sub-screens under
connectkeepkey/ mirroring the Keystone birthday flow. Update
KeepKeyConnectVM to navigate to KeepKeyNewOrActiveScreen instead of
running the full use case; the actual USB connect + FVK fetch happens
when the user confirms their device type or birthday choice. Wire all
new routes in WalletNavGraph and register VMs in ViewModelModule.
Generalize DisconnectUseCase and DisconnectVM from KeystoneAccount to
WalletAccount so KeepKey accounts can also be disconnected via the same
UI flow. getKeystoneAccount() renamed to getHardwareWalletAccount().
Also update TODO.md to mark ZA-44–46, ZA-63–65 complete.
Pulled buildKeepKeyPackets / parseKeepKeyPackets out of
KeepKeyTransportProvider into KeepKeyFraming.kt (internal visibility)
so JVM unit tests can reach them without Android SDK dependencies.
KeepKeyTransportProvider now delegates to those functions.

14 tests cover boundary sizes (0, 57, 58, 120, 121, 200 bytes),
big-endian type-ID / length encoding, per-packet invariants,
round-trips at every boundary, and both bad-marker error paths.
All 137 tasks pass.
Pulled the ZcashSignPCZT → ZcashPCZTAction × N → ZcashSignedPCZT
exchange out of KeepKeyProposalRepository into KeepKeySigningProtocol
(internal class, pure Kotlin, no Android/ZCash SDK types). The
repository now delegates to it.

11 tests in KeepKeySigningProtocolTest cover zero/single/multi-action
happy paths, MSG_FAILURE on both init and action messages, wrong
response type, out-of-order action index, account index encoding, and
verbatim PCZT byte forwarding. All 137 unit test tasks pass.
… unit tests

combine(isLoading, errorMessage) replaces map so the UI re-emits when
errorMessage changes independently of isLoading; previously error was
swallowed because the map only fired on isLoading changes.

Blake2bTest: correct RFC 7693 vector for "a" (hash[1]=0x3f, hash[3]=0x4e),
add "abc" vector — implementation was already correct.
KeepKeyEmulatorTransportProvider implements KeepKeyTransportProvider using
the Flask HTTP bridge (bridge.py) in front of the Docker emulator's UDP
sockets, making the full signing protocol testable without physical hardware.

KeepKeyEmulatorDebugLink wraps the debug link interface to load a known
mnemonic and simulate button presses in test setup.

KeepKeyEmulatorIntegrationTest drives ZcashGetOrchardFVK against the
emulator and asserts golden FVK vectors from test_msg_zcash_orchard.py.
Tests self-skip via Assume when the bridge is unreachable so CI without
the Docker stack is unaffected.
…ning tests

OrchardActionData carries alpha + sighash (legacy mode) per Orchard action.
KeepKeySigningProtocol.sign() now forwards these into ZcashPCZTAction and
also sets address_n, n_actions, total_amount, and fee in ZcashSignPCZT.
Existing production call site (nActions=0, actions=emptyList) is unchanged.

KeepKeyEmulatorIntegrationTest gains 7 signing tests: single-action
signature size/non-zero/count, 3-action count, different-sighash produces
different sig, different-account produces different sig, zero-actions empty.
All skip automatically when the Docker emulator is unreachable.
KeepKeyConnectViewTest: 10 tests covering idle, loading, error, and
callback states for KeepKeyConnectView and KeepKeyConnectedView.

SignKeepKeyTransactionViewTest: 12 tests covering title/subtitle display,
button visibility in loading state, error message show/hide, positive and
negative callbacks, and disabled-button enforcement.

All tests build state directly — no ViewModel, transport, or emulator.
…Case

Replace the monolithic ConnectKeepKeyUseCase with two focused use cases:
- GetKeepKeyOrchardFVKUseCase: USB permission + connect + FVK export +
  UFVK encode + seed fingerprint derivation + unified address derivation
- ImportKeepKeyAccountUseCase: account import + navigation

Add selectkeepkeyaccount/ package (State, View, ViewModel, Screen,
Args) that mirrors the Keystone account selection screen. The three
birthday-aware VMs (KeepKeyNewOrActiveVM, KeepKeyEstimationVM,
KeepKeyHeightVM) now call GetKeepKeyOrchardFVKUseCase and navigate to
SelectKeepKeyAccountArgs instead of importing directly. The selection
screen shows the abbreviated unified address derived from the FVK and
lets the user confirm before ImportKeepKeyAccountUseCase writes the
account to the SDK and navigates to the connected/resync flow.
- detekt: add @Suppress("TooManyFunctions") to KeepKeyTransportProviderImpl and
  KeepKeyEmulatorTransportProvider (both exceed the 11-function threshold matching
  the same pattern used by KeystoneProposalRepository and AccountDataSource)
- detekt: extract ZIP32_PURPOSE, ZCASH_COIN_TYPE, ORCHARD_FIELD_BYTES constants in
  KeepKeySigningProtocol to eliminate MagicNumber violations on derivation path
- detekt: add @Suppress("MagicNumber") to byte-manipulation functions (readVarint,
  buildKeepKeyPackets, parseKeepKeyPackets, fromHex, httpPost) where raw bit masks
  are the clearest expression of the protocol spec
- detekt: replace bare TODO(sdk) comments in KeepKeyProposalRepository with
  TODO [#2]: format referencing the new GitHub issue tracking the ZCash SDK gap
  (addSpendAuthSigsToPczt not yet available); open issue #2 in keepkey/zodl-android
- detekt: fix MaxLineLength in KeepKeyTransportProvider (wrap throw into block) and
  KeepKeyProposalRepository (split long Suppress annotation and shorten comments)
- ktlint: run ktlintFormat to fix formatting across all KeepKey files and any
  existing files we modified as part of the integration (block wrapping, chain
  method continuation, no-multi-spaces, multiline-expression-wrapping)
@pastaghost pastaghost force-pushed the feature/keepkey-phase1-account-setup branch from 8a8cab6 to 12d712e Compare May 21, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant