Files
Haven/docs
Amnibro 82d448e95b Design: end-to-end encrypted group DMs (with signed messages) (#5498)
* Design: end-to-end encrypted group DMs

Group DMs do not exist yet — start-dm takes a single targetUserId — so this
is a greenfield design rather than a retrofit.

Proposes an epoch group key: one random AES-256 key per group per epoch,
wrapped to each member over the pairwise ECDH channel that already exists,
rotated on every membership change. Messages and attachments are encrypted
once regardless of group size.

Records two properties of the current 1:1 scheme that the design turns on,
both of which are easy to miss: there is no forward secrecy, and there is no
cryptographic sender authentication — the pairwise key is symmetric, so
either party can already produce a ciphertext the other accepts, and sender
identity is a server assertion. The chosen scheme therefore widens the set
who could forge from one to N-1 rather than introducing a new weakness, and
the doc says so rather than glossing it.

Rejects pairwise fan-out (multiplies every message and attachment by member
count), sender keys (do not authenticate without a signing key Haven lacks),
and MLS (no vetted stack across JS, Kotlin and Swift; disproportionate for
3-10 person groups).

Notes the upgrade path in dependency order, starting with an ECDSA signing
key that would close the same authentication gap 1:1 DMs already have.

* Group DM E2E: sign messages rather than settling for a shared-key trade

Revises the design to do sender authentication properly instead of noting
the gap and moving on.

Adds an ECDSA P-256 signing identity key as the foundation: wrapped in the
same password-derived blob, published through the same TOFU-pinned flow.
Every message is signed over a digest binding channel, epoch, sender and
ciphertext, so a valid ciphertext cannot be replayed into another group or
epoch, or re-attributed. Confidentiality comes from the epoch key and
authenticity from the signature, which is what makes one shared group key
safe: everyone can decrypt, only a key holder can author.

Applies the same signature to 1:1 DMs, which have the identical forge gap
today.

P-256 over Ed25519 for reach only — WebCrypto, JCA and CryptoKit all cover
it with no polyfill, and this has to work identically across three
independent client implementations.

Documents why a per-message ratchet is the wrong answer here despite being
the textbook one. Haven decrypts history on demand from server-held
ciphertext on every scroll-back, so deleting old message keys would make
re-fetched history undecryptable. A ratchet buys forward secrecy only by
breaking scrollback, or by persisting every message key forever — more key
material at rest than the single epoch key it replaces. Forward secrecy is
therefore epoch-granular with rotation policy as the knob.

Also records that key presence is effectively universal (publish-public-key
fires on every init) while key availability is not, since a fresh device
without a password lands in the existing ghost state. Only the recovery
path needs to care.

* Group DM E2E: detect a tampered channel by verifying the conversation

Adds transcript chaining and roster gossip, and records why network
identifiers were rejected for the same job.

Each message carries prev, the hash of the previous envelope its sender had
seen, inside the signed digest. Signatures alone authenticate each message
but say nothing about the set of them, so a hostile server can still drop,
withhold or reorder and every remaining signature still verifies. Chaining
turns the history into an append-only structure the server cannot rewrite:
a withheld message leaves the next prev dangling, reordering fails to link,
and a split view makes two members' chains diverge. Concurrent sends share
a prev legitimately, so clients treat it as a DAG and alarm only on a prev
that never arrives.

Roster gossip has each member publish a signed digest over the sorted group
key list. Matching digests mean nobody has been fed a substituted key;
a lone mismatch names the disagreement. A 1:1 conversation cannot do this
because there is no third party to disagree with — the group is its own
redundancy, so substitution becomes detectable without reading safety
numbers aloud. The existing pairwise safety number stays as the manual
escalation.

MAC and IP were considered for this and do not work: a browser cannot read
its own MAC, MAC does not survive the first router hop so it could only be
self-reported, self-reported values are attacker-controlled, a
server-observed IP is circular when the server is the untrusted party, both
are deliberately unstable now so they would emit false alerts that train
people to dismiss warnings, storing them is the tracking surface this
project avoids, and neither says anything about whether a fetched public
key is genuine.

* Point the constraint note at the tamper-detection section too
2026-08-15 10:49:31 -04:00
..
2026-08-13 12:28:28 -04:00