matrix: back up agents' room keys with a controller-minted recovery passphrase
An agent's matrix encryption keys lived only in its local crypto store, so `destroy --purge` or a primary-account token reset lost every encrypted room's history with no way back (#5022). swarm-controller now mints a random passphrase (32 bytes from the kernel's CSPRNG, base64url) for every account listed under each agent's `matrix/` directory, at `swarm/agents/<agent>/matrix-recovery/<account>`, in a five-minute pass. The write is a cas=0 create after a read; a path that holds anything is never replaced, and a create that loses a race counts as done. The controller's existing `data/swarm/agents/*` grant covers the write and the agent's own `agents/<agent>/*` read grant covers the read, so no policy changes. hive-matrix-daemon reads the passphrase when it brings an account up, after cross-signing: with no backup on the homeserver it enables recovery with it (backup + secret storage); with one there it recovers, which imports the backup key and the self-signing key, and room keys then download through `BackupDownloadStrategy::OneShot`. No passphrase yet logs at INFO and the account comes up without backup; any failure logs WARN and never stops the daemon. Recovery runs after `ensure_cross_signing`, not before: on a new account cross-signing must exist locally for `enable` to export it into secret storage, and on an existing identity `bootstrap_cross_signing_if_needed` is a no-op, leaving `recover` to import and sign. Part of #5022 (S1+S2, with the S4 docs).
This commit is contained in:
parent
bff605035c
commit
45568f6e60
11 changed files with 615 additions and 23 deletions
|
|
@ -408,6 +408,28 @@ async fn account_token(cfg: &AccountCfg) -> Result<Option<Token>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Read `account`'s recovery passphrase from the store and hand it to
|
||||
/// [`client::ensure_recovery`]. With none — no store, or `swarm-controller`
|
||||
/// has not minted it yet — key backup stays off until the next start.
|
||||
async fn ensure_recovery(client: &Client, account: &str) {
|
||||
let found = match credential::agent_name() {
|
||||
Some(agent) => credential::Passphrase::from_store(&agent, account).await,
|
||||
None => Ok(None),
|
||||
};
|
||||
match found {
|
||||
Ok(Some(passphrase)) => client::ensure_recovery(client, account, &passphrase).await,
|
||||
Ok(None) => tracing::info!(
|
||||
account,
|
||||
"no matrix recovery passphrase in the store; key backup stays off"
|
||||
),
|
||||
Err(e) => tracing::warn!(
|
||||
account,
|
||||
error = %format!("{e:#}"),
|
||||
"could not read the matrix recovery passphrase; key backup stays off"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore one account's client (when its token exists), install its
|
||||
/// message handler, and build its sync loop. Returns
|
||||
/// `Ok(Some((client, sync_loop)))` when the account came up, `Ok(None)`
|
||||
|
|
@ -450,6 +472,8 @@ async fn bring_up_account(
|
|||
// flagged "unverified" in other users' clients. Idempotent + swallows
|
||||
// failures (a UIAA-requiring homeserver) inside ensure_cross_signing.
|
||||
client::ensure_cross_signing(&client, &cfg.name).await;
|
||||
// Best-effort, and after cross-signing: see `client::ensure_recovery`.
|
||||
ensure_recovery(&client, &cfg.name).await;
|
||||
|
||||
let sync_client = client.clone();
|
||||
let cb_client = client.clone();
|
||||
|
|
|
|||
Loading…
Reference in a new issue