Skip to content

hww: support host passphrase entry - #2115

Merged
benma merged 6 commits into
BitBoxSwiss:masterfrom
benma-agent:benma-agent/host-passphrase-polling
Sep 25, 2026
Merged

benma merged 6 commits into
BitBoxSwiss:masterfrom
benma-agent:benma-agent/host-passphrase-polling

Conversation

@benma-agent

@benma-agent benma-agent commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Allow host entry of the optional BIP39 passphrase while keeping device entry as the default. The user approves switching to host entry on the device, then confirms the submitted passphrase before unlocking. Rejection, cancellation or invalid input restarts device entry.

Run the new unlock flow inside the paired Noise channel, preserving legacy unlock compatibility. Keep the existing request/response transport with polling for device entry, and reuse session reset on reconnect to abandon interrupted workflows safely.

Restrict host input to passphrases that can also be entered on the device. Withdraw the host-entry option before consent or confirmation starts, and prevent repeated unlock calls from changing an unlocked wallet.

Extend the Python API with callbacks for host input and optional entry controls, with polling owned by the library. Demonstrate entry, cancellation and withdrawal in send_message.py.

Keep the Python 3.7 requirement and supporting UI, request-buffer cleanup, U2F and test-helper changes in separate prerequisite commits.

Related PRs:

Require Python 3.7 so the host-passphrase configuration can use the standard-library dataclass.
This keeps BitBoxConfig's optional callbacks and defaults declarative, without a custom
constructor or an additional dataclasses backport dependency for Python 3.6.

Update the package requirement, classifier and runtime version check together so installers and
direct imports agree on the minimum version.

Keep typing_extensions: Python 3.7 still needs its TypedDict and Protocol backports, which only
became part of typing in Python 3.8.
An HWW workflow can send an intermediate response and wait in next_request while keeping its
future and UI alive. The C transport releases its request lock after the response has been read,
so that lock does not protect the UI throughout a workflow spanning multiple requests.

The existing U2F guard prevents overlapping U2F workflows but does not check for an active HWW
task. A U2F unlock or confirmation could therefore start between HWW requests and compete with
the HWW workflow for the screen and unlock state.

Reject U2F workflow startup whenever async_usb is not idle, before acquiring the U2F workflow
guard. This preserves HWW ownership while processing a request, delivering an intermediate
response or waiting for the next request. U2F workflows can start again once the HWW task has
finished or been cancelled. The check runs in the existing single-threaded scheduling context
and requires no additional lock.

Host passphrase entry makes this gap particularly visible: the device can wait for user input on
the host indefinitely. Apply the guard to all active HWW workflows so the protection also covers
other uses of next_request and is independent of the new unlock messages.

Extend the workflow guard regression to cover an HWW task before and after its intermediate
response is consumed, and to allow U2F startup again after cancellation.
Allow TestingUi string-entry callbacks to return futures. Tests need to keep device input
pending while processing host requests, then complete or cancel it at a chosen point. This
enables coverage of host passphrase entry and races with device input completion, which an
immediately returning callback cannot represent.

Store only the async callback and always await it in enter_string. Preserve existing synchronous
callers by making set_enter_string wrap its callback's result in core::future::ready and
delegate to set_enter_string_async. Both setters replace the same callback, and
remove_enter_string clears it regardless of how it was installed. One callback slot avoids
precedence rules and prevents removal from leaving a separate async callback active.

Update the mnemonic input helper to wrap the same async callback. Mnemonic words return ready
futures, while other prompts forward the saved callback's future. Existing password and mnemonic
test callers keep their synchronous interface.
next_request decrypts each Noise continuation into a temporary Vec before decoding the protobuf
request. Dropping that Vec normally frees its allocation without erasing the plaintext.
Continuations can carry secrets such as a host-supplied BIP39 passphrase, so the temporary
message buffer must be wiped independently of the decoded value.

Wrap the decrypted buffer in Zeroizing so its contents are erased when decoding finishes,
including when decoding returns an error. The guard covers the raw plaintext buffer; decoded
protobuf fields own separate data and still require their own cleanup.
Display strings can contain BIP39 passphrases, mnemonic words or password presets. Freeing the
Rust-side C-string buffers without wiping them leaves that text in reusable heap memory. Appending
the terminating NUL to a buffer sized only for the text can also reallocate it, leaving a second
copy behind even if the final allocation is wiped.

Make display_str_to_cstr_vec return Zeroizing<Vec<c_char>> using the existing zeroizing C-string
conversion. It allocates the full buffer, including the NUL, before copying the text. Apply this
at the common UI boundary so confirm and the other display callers share the same cleanup,
including when an async screen is cancelled. Keep printable-ASCII validation, truncation and the
C character type unchanged, and retain the menu string owners for the full menu lifetime.

This change protects the Rust conversion buffers; copies owned by C components still require
their own cleanup.
@benma-agent
benma-agent force-pushed the benma-agent/host-passphrase-polling branch 2 times, most recently from 5905299 to 843a043 Compare September 20, 2026 09:28
@benma
benma marked this pull request as ready for review September 20, 2026 16:42
@benma
benma requested review from NickeZ and cedwies September 20, 2026 16:42

@cedwies cedwies left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK.

There is one issue I would like to discuss (also mentioned in #2000:

Capital i and lowercase l are pixel identical. This issue predates this PR but IMO becomes a larger problem for host passphrase entry, since a malicious host can swap out letters while the user has no chance to detect it. We should fix this issue before merging this PR.

@benma

benma commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Concept ACK.

There is one issue I would like to discuss (also mentioned in #2000:

Capital i and lowercase l are pixel identical. This issue predates this PR but IMO becomes a larger problem for host passphrase entry, since a malicious host can swap out letters while the user has no chance to detect it. We should fix this issue before merging this PR.

Tried here, but after discussing, we don't continue with this: #2117

The search space to recover the right passphrase is very small, which makes an effective ransom attack here very unlikely.

@cedwies cedwies left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

Allow host entry when the optional BIP39 passphrase is enabled, keeping device entry as the
default. Requesting host entry requires device approval, then the user confirms the submitted
value on the device before unlocking, including an empty passphrase. Rejection, cancellation or
invalid input restarts device entry.

Run the new unlock flow inside the paired Noise channel to protect host input, retaining legacy
unlock for older clients. Poll progress within the existing request/response model and use the
existing reconnect reset to discard abandoned workflows.

Limit host passphrases to the device keyboard's character set and length so users can still
access the same wallet through device entry. Withdraw the host-entry option before consent or
passphrase confirmation starts, and relock after interrupted unlocks. Repeated unlock calls
leave an already unlocked wallet unchanged.

Expose Python callbacks for host input and optional entry controls, with polling handled by the
library. Support both a host-entry button and requesting host entry automatically. Extend
send_message.py to demonstrate host entry, cancellation and withdrawal of the host-entry option.
@benma-agent
benma-agent force-pushed the benma-agent/host-passphrase-polling branch from 843a043 to 853a27c Compare September 25, 2026 15:19
@benma
benma merged commit 4474a3e into BitBoxSwiss:master Sep 25, 2026
17 of 37 checks passed
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.

3 participants