Matrix: a new device starts deaf to everything sent while it did not exist #193

Closed
opened 2026-09-11 22:26:02 -04:00 by Salastil · 1 comment
Owner

No device dehydration - nothing implements MSC3814 or its predecessor.

Dehydration is how a client picks up the encrypted messages that arrived before it
existed: the account leaves a dehydrated device on the server holding the keys, and a
new login rehydrates it. Without it, a freshly verified device can read history from key
backup but has no claim on to-device messages sent in the gap, so some messages stay
undecryptable on that device and nowhere else.

No device dehydration - nothing implements MSC3814 or its predecessor. Dehydration is how a client picks up the encrypted messages that arrived before it existed: the account leaves a dehydrated device on the server holding the keys, and a new login rehydrates it. Without it, a freshly verified device can read history from key backup but has no claim on to-device messages sent in the gap, so some messages stay undecryptable on that device and nowhere else.
Salastil added the featurelow priority labels 2026-09-11 22:26:02 -04:00
Author
Owner

Finished — https://git.salastil.com/Salastil/nobilis/commit/412c232, https://git.salastil.com/Salastil/moho/commit/0ea4167

This needed a prerequisite that was not in the issue: moho had no secret storage
at all
. Dehydration only helps a brand new login, and that login has no local
crypto store, so the pickle key had to come from somewhere on the server. Nothing
wrote m.secret_storage.* — setup_recovery_key keeps the backup key locally and
hands over base58 to write down, and cross-signing uploads public keys while the
private ones stay local.

So it is two pieces: ssss.rs (the account's safe — key description, default-key
pointer, one event per secret, ciphertext keyed by which key encrypted it) and
dehydration.rs (the device itself). The pickle key lives under
org.matrix.msc3814, where Element puts it, so a device set up in either is usable
by the other.

Two decisions worth recording:

  • The recovery code is never written to disk. A draft had it on the account
    config so reconnects could rehydrate unattended — that would have put the key to
    the entire safe in a config file to save typing it once. The pickle key is cached
    by the crypto store after the first unlock instead, and the code is asked for only
    on a login that has never run.
  • An existing safe is opened, never replaced. Making a second one would orphan
    every secret in the first, including a backup key another client depends on — so
    the setup field asks for the existing code or passphrase.

A test caught a real bug in the live path: the pickle key goes into account data as
base64 text, and from_slice wants raw bytes. The first fix was also wrong —
vodozemac encodes with STANDARD_NO_PAD, so decoding as padded base64 fails on every
key this daemon writes. Both spellings are accepted now.

550 tests green. Not exercised against a real homeserver with a real second
login
— the crypto is the crate's and the account-data plumbing is unit-tested,
but the end-to-end flow wants a live account.

Finished — https://git.salastil.com/Salastil/nobilis/commit/412c232, https://git.salastil.com/Salastil/moho/commit/0ea4167 This needed a prerequisite that was not in the issue: moho had **no secret storage at all**. Dehydration only helps a brand new login, and that login has no local crypto store, so the pickle key had to come from somewhere on the server. Nothing wrote `m.secret_storage.*` — `setup_recovery_key` keeps the backup key locally and hands over base58 to write down, and cross-signing uploads public keys while the private ones stay local. So it is two pieces: `ssss.rs` (the account's safe — key description, default-key pointer, one event per secret, ciphertext keyed by which key encrypted it) and `dehydration.rs` (the device itself). The pickle key lives under `org.matrix.msc3814`, where Element puts it, so a device set up in either is usable by the other. Two decisions worth recording: - **The recovery code is never written to disk.** A draft had it on the account config so reconnects could rehydrate unattended — that would have put the key to the entire safe in a config file to save typing it once. The pickle key is cached by the crypto store after the first unlock instead, and the code is asked for only on a login that has never run. - **An existing safe is opened, never replaced.** Making a second one would orphan every secret in the first, including a backup key another client depends on — so the setup field asks for the existing code or passphrase. A test caught a real bug in the live path: the pickle key goes into account data as base64 text, and `from_slice` wants raw bytes. The first fix was also wrong — vodozemac encodes with STANDARD_NO_PAD, so decoding as padded base64 fails on every key this daemon writes. Both spellings are accepted now. 550 tests green. **Not exercised against a real homeserver with a real second login** — the crypto is the crate's and the account-data plumbing is unit-tested, but the end-to-end flow wants a live account.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#193