Skip to content

feat: pair trezor passphrase hidden wallets - #662

Open
jvsena42 wants to merge 20 commits into
masterfrom
feat/hw-passphrase-wallets
Open

feat: pair trezor passphrase hidden wallets#662
jvsena42 wants to merge 20 commits into
masterfrom
feat/hw-passphrase-wallets

Conversation

@jvsena42

@jvsena42 jvsena42 commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #615

This PR ports bitkit-android #1142 to iOS:

  1. Adds an optional Passphrase path to the Connect Hardware flow, so the hidden wallets of a paired Trezor can be watched alongside its standard wallet
  2. Tracks each passphrase-derived account set as its own hardware wallet identity, with its own home tile, funds label, activity scope and removal
  3. Asks for the passphrase again before signing a transfer when the Trezor session that held it is gone, and refuses to sign if it opens a different wallet
  4. Lets the connect flow offer an already paired device, so passphrase wallets can be added after the initial pairing
  5. Fixes a Trezor Bridge session bug that blocked every passphrase pairing after the first on the emulator

Description

Trezor keeps no record of its hidden wallets and forgets a passphrase with the session, so Bitkit watches each one by its extended public keys and asks for the passphrase again whenever a session has to be rebuilt. The passphrase is never persisted or logged, and both entry screens block screenshots the same way the wallet's own mnemonic screens do.

A device previously mapped to a single wallet. It now maps to one identity per passphrase, keyed by the wallet id derived from its accounts rather than by the transport id, which two identities on the same device share. Watchers, balances, activity, labels, routes and removal all follow that identity, and removing one hidden wallet leaves the others paired. This is the bulk of the diff and lands ahead of the feature itself, because leaving the old keying in place would blend two seeds' extended public keys into one stored record, change the standard wallet's derived id and orphan its activity.

Pairing a hidden wallet adds an entry next to the standard one instead of replacing it. The device is matched by shared key material, so re-reading accounts updates the right entry while a different passphrase creates a new one. Only the identity that currently holds the device session shows as connected, since only that one can sign. Session credentials are shared by every wallet on a device, so they are only cleared once no identity of it remains.

Before signing a transfer, Bitkit reopens the hidden wallet and accepts the session only if its accounts resolve back to the wallet being spent from. A wrong passphrase is not rejected by the device — it silently derives another wallet — so the mismatch is caught, the stray wallet that reading its accounts created is dropped, and the transfer fails instead of signing from the wrong wallet. A session that is still open signs with no prompt.

Discovery skips devices that are already paired, which left the Add Hardware Wallet button searching forever and made the passphrase step unreachable after the first pairing. It now falls back to offering a known reachable device once no new one is found.

Separately, the Bridge transport used for the emulator kept a released session id cached and offered it as the previous one when re-acquiring, and the bridge answered wrong previous session. Since switching to a passphrase wallet closes and reopens the session, this blocked every passphrase pairing after the first. Bridge is dev/E2E only, so this does not affect shipping builds, which are Bluetooth-only.

The six commits are meant to be read in order: storage identity, then the device id → wallet id rekeying, then the session layer, then the identity operations, then the two pieces of UI.

Linked Issues/Tasks

QA Notes

Needs a Trezor with passphrase protection enabled. On the emulator start it with TREZOR_PASSPHRASE_PROTECTION=true ../bitkit-docker/scripts/trezor-emulator start and confirm on the device for every account a passphrase session reads, otherwise the call blocks until it times out.

Manual Tests

  • 1a. Settings → Hardware Wallets → Add → Found → Connect: Device Connected shows Passphrase and Finish.
    • 1b. Passphrase → enter a passphrase → Continue: Passphrase Funds Found shows the balance and the label field prefilled with the device name.
    • 1c. Rename the label → Finish: home shows a second hardware tile under that name.
  • 2. Home: headline total includes both hardware balances, and only the identity holding the device session shows a green connection indicator.
  • 3. Device Connected or Passphrase Funds Found → Passphrase → re-enter a passphrase already watched: the step reports it is already added, no tile is added.
  • 4. Device with passphrase protection turned off in Trezor Suite (the factory default) → Device Connected → Passphrase → enter any passphrase: error says to enable passphrase protection in Trezor Suite, no wallet is added, and the standard wallet is not left marked as passphrase-protected (Transfer To Spending on it still signs without asking for a passphrase).
  • 5a. Settings → Hardware Wallets: one row per identity, each with its own balance.
    • 5b. Rename one row: only that identity is renamed.
    • 5c. Delete the hidden wallet row: the standard wallet keeps working and the device stays paired, no re-pairing needed.
  • 6. Settings → Hardware Wallets → Add on an already paired device: reaches Found and then Device Connected, instead of searching forever.
  • 7a. Hidden wallet tile → Transfer To Spending → Amount → Sign → Open Trezor Connect while its session is still open: signs with no passphrase prompt.
    • 7b. Force-quit and relaunch the app, then repeat: the passphrase sheet opens before signing.
    • 7c. Enter a wrong passphrase: error says it opens a different wallet, nothing is signed or broadcast, and no extra tile appears.
    • 7d. Swipe the sheet away mid-verify: nothing is signed.
    • 7e. Enter the correct passphrase: signing continues normally.
  • 8. regression: Pair a device without using the Passphrase path: home tile, detail screen, rename, remove and Transfer To Spending behave as before.
  • 9. Release build, both passphrase screens: screenshots and screen recording are blocked, and the passphrase is absent from the app logs.

Automated Checks

  • Unit tests added: cover the entry a connect refreshes, label inheritance across transport paths, superseding a wiped seed and decoding entries stored before hidden wallets existed, in BitkitTests/TrezorKnownDeviceMatchingTests.swift.
  • Unit tests added: cover identity-scoped save and removal against the real store, in BitkitTests/TrezorKnownDeviceStorageTests.swift.
  • Unit tests added: cover opening a hidden wallet, a device that cannot open one, an already-watched passphrase, the four ensureConnected outcomes, refusing to sign for another identity, and the stray wallet a wrong passphrase leaves behind, in BitkitTests/HwWalletManagerPassphraseTests.swift.
  • Unit tests added: cover two identities on one device with separate balances, only the session-holding identity reading as connected, and scoped removal, in BitkitTests/HwWalletManagerTests.swift.
  • Unit tests added: cover the passphrase step, the duplicate and protection-disabled messages, the paired step following the identity being paired, and the label surviving a late wallet emission, in BitkitTests/HwConnectViewModelTests.swift.
  • Unit tests added: cover the prompt gate, signing after a correct passphrase, the mismatch refusal and dismissing mid-verify, in BitkitTests/TransferViewModelHwTests.swift.
  • Unit tests modified: existing hardware tests moved to wallet-id keying across BitkitTests/HwWalletManagerTests.swift, HwWalletManagerFundingTests.swift, HwFundingSignerTests.swift, HwTransferMocks.swift and TransferViewModelHwTests.swift.
  • Test coverage removed: dropped the walletId(forDevice:) failure test from BitkitTests/TransferViewModelHwTests.swift, because the route now carries the wallet id and the lookup it guarded no longer exists.
  • Run locally against a concrete simulator, since the Rust xcframeworks are arm64-only: xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -destination 'platform=iOS Simulator,id=<UDID>' ONLY_ACTIVE_ARCH=YES test -only-testing:BitkitTests/<hardware suites>. The full suite cannot complete locally — AddressTypeIntegrationTests needs the regtest/Electrum backend and crashes the runner before reaching the hardware suites, which is pre-existing and unrelated to this branch.
  • node scripts/validate-translations.js: 0 errors.
  • CI: standard build, test and format checks run by the PR bot.

Known gaps

  • Two hidden wallets on the same device default to the same name until renamed, since the default label is the device name.
  • On-device passphrase entry is not offered by this flow. TrezorWalletMode.passphraseDevice is wired end to end and stays available on the Trezor dev dashboard, but the production flow is host entry only, matching Android.
  • A hidden wallet can persist with only part of its accounts when a read fails with a device error that is not classified as transient, and its wallet id would change once the missing accounts are read. This is a pre-existing gap in the partial-save guard that passphrase sessions make easier to hit.
  • TrezorManager still has no unit tests: it reaches TrezorService.shared, TrezorUiHandler.shared and the static known-device store directly, so the session work in it is covered indirectly through the pure matching helpers and the storage tests. Adding an injection seam is worth a follow-up.

Screenshot / Video

add-multiple-passphrase-wallets.mov
no-wallet-add-with-passphrase.mov
add-repeated-passphrase.mov
rename-and-delete-wallet.mov
passphrase-disabled.mov
transfer-current-session-wallet.mov
transfer-different-session.mov
transfer-base-wallet.mov

@jvsena42 jvsena42 self-assigned this Aug 11, 2026
@jvsena42

Copy link
Copy Markdown
Member Author
  1. Release build, both passphrase screens: screenshots and screen recording are blocked, and the passphrase is absent from the app logs.

This test requires a physical device

@jvsena42
jvsena42 marked this pull request as ready for review August 12, 2026 12:35
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds independently tracked Trezor hidden-wallet identities and passphrase-aware pairing and signing while rekeying hardware-wallet state from transport IDs to wallet IDs.

  • Adds passphrase-wallet pairing, duplicate detection, identity-scoped labels, balances, activity, and removal.
  • Revalidates the active wallet identity before funding and signing, prompting again when a hidden-wallet session has expired.
  • Updates watcher, navigation, storage, discovery, and Bridge session handling for multiple identities on one device.
  • Adds focused coverage for identity matching, storage, pairing, reconnection, and transfer signing.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current identity checks reject unresolved or mismatched sessions before signing, resolving both previously reported issues.

Important Files Changed

Filename Overview
Bitkit/Managers/HwWalletManager.swift Rekeys watcher and funding behavior by wallet identity and enforces a resolved identity before signing.
Bitkit/Managers/TrezorManager.swift Tracks the wallet opened by the live session and supports explicit standard or passphrase session reconstruction.
Bitkit/Services/Trezor/TrezorKnownDeviceStorage.swift Persists and removes known-device entries using wallet-scoped identity semantics.
Bitkit/ViewModels/TransferViewModel.swift Integrates passphrase prompting and identity-aware reconnection into hardware-funded transfers.
Bitkit/ViewModels/Trezor/HwConnectViewModel.swift Adds the passphrase pairing step and tracks the wallet identity produced by connection.
BitkitTests/HwWalletManagerPassphraseTests.swift Covers unresolved, mismatched, duplicate, and successfully reopened passphrase-wallet sessions.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as Transfer UI
    participant H as HwWalletManager
    participant T as TrezorManager
    participant D as Trezor
    U->>UI: Sign from walletId
    UI->>H: ensureConnected(walletId)
    H->>T: Ensure transport connected
    alt matching live identity
        T-->>H: connectedWalletId matches
    else hidden identity unavailable
        H-->>UI: Passphrase required
        U->>UI: Enter passphrase
        UI->>H: reconnectWithPassphrase(walletId)
        H->>T: Open passphrase session
        T->>D: Read accounts
        D-->>T: Derived wallet accounts
        T-->>H: connectedWalletId
        H->>H: Verify walletId matches
    end
    UI->>H: signFunding(walletId, transaction)
    H->>H: Recheck connected identity
    H->>D: Sign PSBT
    D-->>UI: Signed transaction
Loading

Reviews (3): Last reviewed commit: "Merge branch 'master' into feat/hw-passp..." | Re-trigger Greptile

Comment thread Bitkit/Managers/HwWalletManager.swift Outdated
@jvsena42
jvsena42 marked this pull request as draft August 12, 2026 12:49
@jvsena42
jvsena42 marked this pull request as ready for review August 12, 2026 17:30
Comment thread Bitkit/Managers/HwWalletManager.swift Outdated
@jvsena42
jvsena42 marked this pull request as draft August 13, 2026 17:00
@jvsena42

jvsena42 commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Draft for investigating the CI failure

Edit: it is just a flaky test

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.

Hardware wallet: passphrase (hidden) wallet pairing

1 participant