Only the last-member-leaves path cleaned up a group's key, epoch, invite
and rewrap rows. That cleanup now lives in the shared DM delete, so every
way a group is deleted removes them.
The server relays every group key, so it could forge a rewrap request and
collect the group key from everyone online, or plant an epoch key of its
own. Now a device wraps the group key only for the encryption key it has
pinned for that person (the same pins 1:1 DMs use), accepts a wrapped key
only from a sender whose pinned key opens it, and requires each epoch to
carry a statement signed by the member who published it, naming the key,
the group, the epoch and exactly who it was shared with. Signing keys are
pinned on first sight too. When someone's keys change, Haven asks before
trusting them, as 1:1 DMs do, and until then the group key is not shared
with the new keys and messages signed with them stay hidden. Groups whose
current key predates this are rotated to a signed one automatically.
Only the sender's current signing key was tried, so a key reset turned
everything they had sent before into "could not be verified". The server
now keeps every signing key a person has published, and a message shows
when it verifies under any of them. A message that verifies under none of
its author's recorded keys is still hidden.
Setup ran twice at once on every connect and each run accepted any
published reply, so a new account could save one key to its backup while
the server kept another, and everyone then saw that person's group
messages as unverifiable. Setup is now shared by every caller, replies
carry a request id, and the key and its backup are stored in one step.
Another device racing on a new account adopts the winner's key.
Once members left a group, starting a 1:1 DM could land in the group
instead, where 1:1 messages were then refused. Groups are now skipped
when looking for an existing DM.
* Group DM E2E: crypto core, key distribution, and tests
Implements the design in docs/group-dm-e2e-plan.md far enough to prove the
properties hold. No UI yet — this is the layer everything else sits on.
public/js/e2e-group.js holds the primitives, deliberately free of storage,
sockets and DOM so the same file runs in the browser and under node --test.
Confidentiality comes from an AES-256 epoch key wrapped per member over the
pairwise ECDH secret that already exists; authenticity from an ECDSA P-256
signature over a digest binding channel, epoch, sender and the previous
message. Verification happens before decryption and a rejected message
returns null plaintext, so a forgery cannot be rendered by mistake.
The signature is what makes one shared group key safe: every member can
decrypt, only a key holder can author.
src/socketHandlers/groupE2E.js distributes wrapped blobs the server cannot
open, while enforcing what no client can check for itself — only a member
may publish an epoch, epochs are strictly sequential, a member reads only
their own blob, and an epoch must cover exactly the current membership.
That last rule stops a member publishing an epoch that silently omits
someone, locking them out of a conversation the UI still shows them in.
Two suites, 28 tests, runnable with npm test. The crypto suite is written
as attacks rather than happy paths: forging as another member, re-attributing
a genuine message, tampering, stripping the signature, replaying into another
channel or epoch, reading an epoch you were not given, and unwrapping a key
not wrapped for you. It also asserts scrollback still decrypts across several
rotations, which is the regression a per-message ratchet would have caused,
and that concurrent sends sharing a prev do not false-alarm.
Confirmed the suite is load-bearing by disabling the signature check: exactly
the five attack tests fail and nothing else.
* Group DMs invite instead of auto-joining, and rewraps require a request.
Addresses review on #5499: drop leftover setup.js, consent plus roster
dedup on start-group-dm, and pin rewraps to a prior request plus the
recipient's TOFU public key.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>