Add password-encrypted vault

- Encrypt stored secret keys with AES-256-GCM under an Argon2id-derived key;
  the vault stays plaintext until a password is set (Settings -> Storage or
  the CLI set-password command)
- Only secret keys are encrypted; labels and npubs stay readable so profiles
  can be browsed while the vault is locked
- Backend: crypto module, Vault.crypto metadata, unlock/lock/set/remove
  password on App, VaultLocked/WrongPassword errors, secret resolution on the
  publish path
- IPC: set_vault_password, unlock_vault, lock_vault, remove_vault_password
- CLI: set-password, remove-password, unlock; create/publish prompt when the
  vault is locked (NFM_PASSWORD env or hidden prompt, never argv)
- GUI: unlock banner + modal on locked vaults, protect/change/remove password
  in Settings, password field styling
- Tests: Rust (argon2/AES round-trips, vault lifecycle) and Vitest (unlock
  flow, set/change/remove password), all green
This commit is contained in:
Avi 2026-08-03 19:00:41 -05:00
commit 7ca1d14dcb
23 changed files with 1540 additions and 43 deletions

View file

@ -14,8 +14,9 @@ runs in the Rust backend, which the GUI talks to over a JSON-lines IPC channel.
- Add, remove, enable/disable, and test relays
- Light / dark / system theme, configurable publish confirmation and key shortening
- Back up your vault from the UI
- Honest about security: the vault is stored in plaintext (as in the original CLI), readable
only by your user account; this is clearly disclosed in the app
- Password-protected vault: secret keys are encrypted at rest with AES-256-GCM under an
Argon2id-derived key. Without a password set, the vault is stored in plaintext (readable only by
your user account) and this is disclosed in the app
## Architecture
@ -88,10 +89,17 @@ cargo run --release -- switch <npub> # select the active profile
cargo run --release -- publish <npub> "Hello" # publish a text note
cargo run --release -- relays list|add|remove|enable|disable|test
cargo run --release -- settings get|set theme|confirm|shorten
cargo run --release -- info # show storage locations
cargo run --release -- set-password # encrypt the vault (or change its password)
cargo run --release -- remove-password # remove vault encryption
cargo run --release -- unlock # verify the vault password for this process
cargo run --release -- info # show storage locations and version
cargo run --release -- serve # JSON-lines IPC server (used by the GUI)
```
Passwords are read from the `NFM_PASSWORD` environment variable when set, otherwise you are
prompted interactively. They are never accepted as command-line arguments. `create` and `publish`
prompt for the vault password automatically when the vault is encrypted.
## Storage and migration
- The vault (`profiles_vault.json`) and settings live in
@ -100,8 +108,13 @@ cargo run --release -- serve # JSON-lines IPC server (used b
- The original CLI saved `profiles_vault.json` in its working directory. On first launch this
app finds that file, copies it to a timestamped `*.backup-<ts>` next to it, and imports your
profiles into the new location. The original file is left untouched.
- Private keys are stored in the vault in plaintext. Anyone with access to your user account
can read them; a password-encrypted vault is planned for a future version.
- The vault is stored in plaintext until you set a password (Settings → Storage, or
`set-password` in the CLI). Once protected, every secret key is encrypted at rest with
AES-256-GCM under a key derived from your password with Argon2id. Labels and public keys stay
readable so profiles can be browsed while the vault is locked. You unlock once per session;
the derived key lives only in memory and is never written to disk. Anyone with access to your
user account can still read the vault file, so the password is a defence-in-depth layer, not a
replacement for keeping your account secure.
## Development
@ -122,7 +135,8 @@ cargo clippy --all-targets
```
src/ Rust library + CLI + IPC server
app.rs application state loading/persistence
app.rs application state, vault password/unlock lifecycle
crypto.rs Argon2id key derivation + AES-256-GCM encryption
errors.rs structured AppError
ipc.rs JSON-lines serve() loop and request/reply envelope
main.rs CLI entry point
@ -130,7 +144,7 @@ src/ Rust library + CLI + IPC server
publish.rs note publishing with per-relay reports
relays.rs default relays, validation, connection tests
settings.rs theme and user preferences
vault.rs encrypted-vault-ready storage (currently plaintext)
vault.rs vault storage (plaintext or password-encrypted) and migration
frontend/
electron/ Electron main + preload (backend spawn, IPC, clipboard)
src/ React app (components, screens, state, styles)