Password plus Apps - #11
Open
dkackman wants to merge 39 commits into
Open
Conversation
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>
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
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.
No description provided.