feat(account): hold key passphrases as wiped secrets and report a locked account as KeysLocked

A wrong mailbox password surfaced as InvalidOperation("no active user key could
be unlocked"), which a caller could only recognise by its text. It is now
ProtonError::KeysLocked, and AccountClient::unlock() checks a secret with a
single core/v4/users request instead of loading every address.

Derived passphrases, the mailbox password and the cache encryption key were
plain Vec<u8>s, cloned freely and never wiped. They are Zeroizing now, and the
passphrase map is KeyPassphrases: wiped on drop, redacted in Debug, with
derive_secret() for a client that wants a key only an unlocked account can
reproduce. with_key_passphrases takes it.

Breaking: ProtonError gains a variant, key_passphrases() and
with_key_passphrases() change type, and derive_key_passphrase returns
Zeroizing<Vec<u8>>.
This commit is contained in:
nvrl committed 2026-10-07 02:24:51 +02:00
1 parent 909eefd06e
commit 325c326c49
11 files changed
+351 -28

No files matched your search

+6
View File
@@ -137,6 +137,12 @@ graph TD
3. **Share & Node Keys**: Nodes (files and folders) belong to shares. A share's passphrase is decrypted by the address key. The decrypted share passphrase is used to unlock the node's private key (usually X25519/Ed25519 or legacy RSA).
4. **File Content Keys**: Files contain metadata and data blocks. The file's name and passphrase are encrypted. The symmetric **Content Key** (AES-256) is encapsulated in a PGP Public-Key Encrypted Session-Key (PKESK) packet addressed to the node key. See [content.rs](file:///home/narl/dev/private/proton-sdk-rs/crates/proton-sdk/src/crypto/content.rs).
#### Resuming without the password
A client that persists a session does not have to keep the mailbox password (on a single-password account, the login password). Capture `AccountClient::key_passphrases()` right after login and resume with `ProtonDriveClient::with_key_passphrases()`. A `KeyPassphrases` is wiped when dropped and prints as `KeyPassphrases(n redacted)`, but each passphrase unlocks its key, so store it as carefully as the password. `KeyPassphrases::derive_secret(purpose)` gives a 32-byte secret only an unlocked account can reproduce, for encrypting local state.
`AccountClient::unlock()` checks a password or a set of passphrases with one request. A secret that does not fit fails with `ProtonError::KeysLocked`, which a login prompt should answer by asking again, never by retrying.
### 2. Upload Pipeline
When uploading a file: