wallet: actually open the wallet file, and remember its passphrase #149
Loading…
Reference in a new issue
No description provided.
Delete branch "onionwire/t_67879b98-wallet-actually-open-the-wallet-file-and"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this fixes
monero-wallet-rpcstarts with no wallet open and holds one only for the lifeof its process. Every app start therefore produced a live child that answered
-13 No wallet filetoget_balance/get_address/create_address/transferwhile the wallet file sat on disk — a restart, a Lock/unlock, a walletre-point all ended the same way. Nothing opened it.
This PR makes the app open the configured catalogue file itself after the child
is up, and keeps the passphrase that does it.
Core (src/wallet.rs, src/wallet_child.rs)
Wallet::open_wallet(filename, password)— the RPC call that was missing.The passphrase travels in the JSON-RPC body, never on argv:
--wallet-file/--passwordare readable by any process of the same userthrough
ps.WalletConfig::wallet_passphrase(Zeroizing,with_wallet_passphrase,redacted in the hand-written
Debug,PartialEqextended, and deliberatelyNOT filled in by
Wallet::config()— that builds the endpoint config, so acaller persisting one does a read-modify-write).
Error::is_unreachable()— the one retryable class: a freshly spawnedmonero-wallet-rpcbinds its port a moment afterspawnreturns, so an openretries the connect/write failures with a backoff up to 10s. A refused
passphrase or a timeout is the RPC's own answer and is not repeated.
ChildWalletState(Unknown/Open/NeedsPassphrase) and two newWalletChildStatusfields:wallet_open,needs_passphrase. A live child isnot a usable wallet, and the status now says which.
Store (src/store.rs)
Additive
wallet_config.sealed_passphrasecolumn, sealed with the samebackup::aead_decryptdata key under its own AAD(
onionwire-store-v1:wallet-passphrase). Existing installs gain it on reopen —no migration step, no plaintext window. Nothing is dropped: the upgrade path
(
migrate_plaintext_to_encrypted) copies the whole db, and a test now covers itwith a passphrase set.
Node (src/node.rs)
start_wallet_childopens the configured file after the child comes up. Fouroutcomes: not in the catalogue -> nothing to open; no known passphrase ->
needs_passphraseand the start still succeeds (an install whose walletpredates the stored passphrase must not be turned into a start failure); the
RPC opens it ->
Openand the passphrase is sealed; the RPC refuses ->Errnaming the file, and the child is stopped rather than left inert.open_wallet_with_passphrase(filename, passphrase)— the one-time prompt path.Same refusals as
open_wallet: a filename outside the catalogue, an emptypassphrase, no configured wallet RPC. The passphrase is sealed only after a
live daemon opened the file with it; an unverified secret is never stored.
wallet_status()reports the above; a successful probe (get_address) is alsotreated as proof the file is open, whoever opened it.
wallet_create/wallet_restoreseal the passphrase they just used, and markthe file open while a child is running;
wallet_save_config(F7) keeps thestored passphrase instead of overwriting it with the screen's empty field.
TUI (src/tui.rs) and SDK
WalletSetup::UnlockPass— one entry, echo off,Entersubmits,Esccancels, buffer zeroized on every exit. It fires when the saved-wallet picker
selects a file this install has no passphrase for, and from
son F6.Wire::openWalletWithPassphrase(filename, passphrase)(async,spawn_blocking— the RPC can wait for a freshly spawned child;
Stoppedafter shutdown), pluswallet_open/needs_passphraseonWalletChildStatus. Kotlin README showsthe flow.
Docs
docs/WALLET.md,docs/THREAT_MODEL.mdandREADME.mdnow say the passphrase iskept and what that costs: the store passphrase is sufficient to spend — an
attacker with it can open the file and move the balance, with no second secret.
That is a deliberate trade (a passphrase re-typed after every restart is one
users defeat with a weak one), it does not weaken the spend-authority warning at
create/restore,
/tipstill refuses on a view-only wallet before any dial, and/wipekeeps the wallet file and its passphrase as it kept the file before.Tests
Acceptance:
cargo test --locked --target x86_64-unknown-linux-gnu(green, wholesuite) and
cargo clippy --locked --target x86_64-unknown-linux-gnu --all-targets -- -D warnings(clean). SDK workspace:
cargo test --locked(52 lib tests) andcargo clippy --locked --all-targets -- -D warnings, both green.New: four
src/node.rstests drive a real child lifecycle (fakemonero-wallet-rpcscript writing its argv, a loopback mock on the port the child was spawned with,
spawn_blockingso the mock can answer while the start blocks) — open issued onthe restart with the stored passphrase, no
open_walletat all without one,a refused passphrase distinct and unsealed, sealed-on-success — plus
tests/wallet.rsasserting the request bytes (open_wallet+ filename +password) and
tests/store.rscovering the sealed column, its wipe semanticsand the plaintext upgrade. Mutation check: stubbing
open_child_walletandopen_wallet_filetoOk(())fails all four node tests, so they measure theopen rather than the start.
No
Cargo.lockchange (no new dependency).