Skip to content

Password plus Apps - #11

Open
dkackman wants to merge 39 commits into
passwordfrom
password-gate
Open

Password plus Apps#11
dkackman wants to merge 39 commits into
passwordfrom
password-gate

Conversation

@dkackman

Copy link
Copy Markdown
Owner

No description provided.

Hadamcik and others added 30 commits August 20, 2026 18:57
Adds the drift-proof set of 32 password-gated endpoint names, a
maybe_unlock token in impl_endpoints_tauri! that expands to the
sage_password_gate::resolve() call for exactly those endpoints, and a
test that scans the sage-api request sources for password: Option<String>
fields and asserts the JSON matches exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to 341c782 which added serde_json as a dev-dependency of
sage-api for the password-gate drift test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drift test's include_str! list only named 6 of 8 files under
requests/, silently skipping data.rs and settings.rs (and any future
new module). A struct gaining a password field there would go
undetected. Resolve the directory at test time via CARGO_MANIFEST_DIR
and read_dir every *.rs file instead, with a loud failure if the
directory is unreadable or yields fewer than 6 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only GetSecretKey/SendXch/SignCoinSpends/SignMessage approvals reach a wallet
secret; capability and network-whitelist grants now resume without a prompt.
Also overrides any app-supplied password on wallet.getSecretKey, and clears two
clippy denials this plan introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…act BridgeTools

Recompute bridge-approval runtime visibility on every exit path from the
password phase so a still-queued approval is not stranded off-screen, and
replace BridgeTools' derived Debug with a hand-written impl that redacts the
password.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WalletSendXch::approval_request returned Ok(None) whenever the app held
WalletSendXchAutoSubmit, even on a password-protected wallet -- silent
auto-submit is incompatible with password protection since there is no
UI moment to collect the password. Extracts a pure requires_approval
predicate and threads a password_protected flag through BridgeContext,
resolved from sage.wallet_config.wallets (not the expensive Argon2
Keychain::is_password_protected probe) at both BridgeContext
construction sites in bridge_request.rs.
PasswordContext now listens for events.passwordRequest from Rust and
replies via commands.submitPasswordResponse, instead of callers invoking
requestPassword(hasPassword). Exposes only requireLocalAuth() for the two
UI-only Settings.tsx gates (RPC server start, run-on-startup toggle) that
have no Rust unlock operation behind them.

Queues concurrent requiresPassword requests (keyed by requestId) rather
than clobbering one dialog with another, so a second gated Rust operation
in flight doesn't silently hang for the 5-minute timeout. PasswordDialog
gains an optional attemptsRemaining prop to surface retry state.

This intentionally breaks the remaining requestPassword call sites
(WalletCard, ConfirmationDialog, useOfferProcessor, Offer, WalletConnect*)
for follow-up tasks to fix.
Dismissing the password dialog is a deliberate choice, not a failure.
addError now short-circuits on ErrorKind::Unauthorized with the exact
CANCELLED_REASON string from sage-password-gate::resolve, before any
other branch or state update, so no toast/dialog appears. Other
unauthorized errors (wrong password lockout, prompt timeout, etc.)
are unaffected.
The generic unauthorized-toast filter (kind === 'unauthorized' with reason
containing 'not found'/'No secret') predates the password gate and was
silently swallowing its two failure reasons: "Too many incorrect password
attempts" (resolve.rs:64) and "Password prompt timed out" (lib.rs:92).
Users saw no feedback at all on a real auth failure. Add both reasons as
named, Rust-file-referencing constants and extend the toast condition to
match them, leaving the "not found"/"No secret" behaviour and the silent
NotLoggedIn/NoSigningKey fallthrough untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`maybe_unlock` expanded identically for all 32 gated endpoints, so
`delete_key` and `get_secret_key` -- the only gated request types that
carry a `fingerprint` -- called `resolve`, which starts with
`sage.wallet()?`. Both are driven exclusively from the logged-out wallet
list, so they failed with `NotLoggedIn` before doing anything, and
`ErrorContext` silently swallows that reason: the dialog closed and
nothing happened. While logged in they verified the *active* wallet's
password but handed it to the keychain for `req.fingerprint`.

Adds `resolve_for_fingerprint`, which never touches `Sage::wallet()` --
the `password_protected` lookup already searches `wallet_config.wallets`
by fingerprint and needs no active wallet. The macro picks the form from
`password-gated-fingerprint.json`, a subset of `password-gated.json`
kept honest by a new drift test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cker

`GetSecretKey` approvals prompted for the active wallet while acting on
`params.fingerprint`, the same root cause as the endpoint fix. The
approval path now resolves against the fingerprint in the approval body.

`approval_requires_password` also keyed only off the approval body, so
every SendXch/SignMessage/SignCoinSpends approval on an unprotected
wallet hid the `bridge-approval` runtime, ran a no-op gate round-trip and
re-synced visibility -- a visible flicker whenever approvals were queued.
The hide/restore pair is now conditional on the target wallet actually
being password-protected. The gate call itself still runs unconditionally,
because the frontend may put a biometric gate in front of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`emit_to(app_handle, "main")` is not an isolation boundary. Delivery runs
through `Listeners::emit_js_filter` -> `match_any_or_filter`, which
short-circuits to true for any listener registered with `EventTarget::Any`
and never consults the target, and `capabilities/apps.json` grants app
webviews `core:event:allow-listen`. An app webview that calls
`listen('password-request', ...)` sees every prompt.

No password leaks -- the secret only ever rides `submit_password_response`,
which apps are not granted -- but the active wallet fingerprint, and
wallet switches over time, did. Removes the field (the frontend never
read it) and corrects the comments and the design spec, which both
asserted a guarantee the runtime does not provide.

`attemptsRemaining` stays: the dialog needs it, and a bare retry counter
identifies no wallet and reveals nothing an observer could not already
infer from the re-prompt timing it can see anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`resolve_with` minted a fresh `Uuid::new_v4()` per attempt, so
`PasswordContext`'s replace-in-place branch -- which looks a queued
request up by `requestId` -- could never match. One id for the whole
resolve makes that branch live, and a retry now resumes at the front of
the queue instead of appending behind a concurrent request.

Each attempt registers its own oneshot under the id; the previous entry
is always gone by then, consumed by `deliver` or removed on timeout.
Covered by a resolve-level test that all attempts share an id, and a
gate-state test that re-registering a consumed id delivers to the new
receiver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two gate failures a user actually sees -- too many attempts, prompt
timed out -- were echoed straight from the Rust reason string, so they
appeared untranslated unlike every other user-facing string. The reason
strings stay as the discriminator; the toast now renders through `t`,
matching the `incorrect_password` branch above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"1 attempts remaining" on the last try. Uses lingui's `<Plural>`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dkackman and others added 9 commits August 22, 2026 07:51
Endpoints routed through Sage::transact/transact_with only use the password
when req.auto_submit is set. The Send page builds the transaction with
auto_submit unset, so the gate collected a password that was then discarded,
and prompted again after the confirmation dialog.

Replace password-gated.json and password-gated-fingerprint.json with a single
password-gating.json mapping each endpoint to always | auto_submit |
fingerprint. Carrying an auto_submit field is not the criterion --
sign_coin_spends and take_offer have one but reach the keychain on every call.

A new drift test enforces the criterion that matters by scanning the sage
crate's endpoint implementations: a body calling extract_secrets or self.sign
must not be auto_submit, and one that only forwards to transact must be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-03-15 design and the 2026-03-16 matrix both described password
prompting as a frontend responsibility -- requestPassword at ~16 call sites,
enforcement split between ConfirmationDialog and per-call-site prompts. None
of that survives the password gate.

Corrected against the code rather than against the earlier drafts:

- has_password reads Wallet::password_protected from the wallet config, not a
  field on KeyData::Secret. keys.bin was never migrated, so the migration
  section describing a versioned deserialization fallback was fiction.
- delete_key verifies in Rust; the frontend no longer probes get_secret_key
  first.
- ImportKey has no password field. Keys import under the b"" sentinel and are
  protected afterwards through Settings.
- Error feedback is a table of the six ErrorContext branches, including the
  incorrect_password kind and the reconcileKeyProtection self-heal.

The matrix is rebuilt around gate modes, with a WalletConnect table and a new
app-bridge table, and records one over-prompt the gate introduced:
increase_derivation_index is `always`, but its extract_secrets call sits
inside `if hardened`, so an unhardened-only call now prompts for a password it
will not use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Added `requires_password` and `password_attempts` fields to `PendingBridgeApprovalView` to track password requirements and attempts.
- Updated `write_pending_approval` to handle password-related logic, including timeout adjustments for approvals requiring a password.
- Introduced `peek_pending_approval` and `record_password_attempt` functions to manage password attempts without consuming approvals.
- Modified `ResolveBridgeApprovalArgs` to include a password field, ensuring sensitive data is handled securely.
- Implemented logic in `process_after_approval` to manage password verification inline with approval processing.
- Updated the reconciliation process for wallet password protection to ensure consistency between the keychain and configuration.
- Enhanced tests to validate the new password protection features and ensure correct behavior under various scenarios.
…rotection for clarity and update logic to handle all wallets
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