* 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>
There was no test script, so the suite was run by hand with a glob and Node's
default parallelism. Under that, the bot audio integration test intermittently
took down its spawned server with an access violation on Windows, which reads
as a product failure and is not one: it passes alone, paired, and serially.
Several test files each start a real server, and enough of them at once is what
does it.
Serial is slower and correct, which is the right way round for a suite this
size. 91 of 91 pass.
Bumps the version, adds the CHANGELOG entry, and updates the download links
and version history on both site files.
The major number reflects the size of the release rather than a break in
compatibility. There are no migration steps: the schema changes only add, and
Ferry ships switched off. The notes say plainly that it touches shared code and
that Ferry has not been through a real deployment, and recommend a backup
first, because promising that nothing breaks is not something this release has
earned yet.
Each Haven server brings its own Discord bot, since Discord caps unverified
applications at 100 guilds. Reading uses a gateway WebSocket, because Discord
has no outgoing webhook for new messages. Writing uses a per-pairing Discord
channel webhook rather than the bot user, which is the only way a relayed
message can carry the Haven author's own name and avatar.
Pairings live in ferry_links and act as the allowlist, not just a routing
table: a member can only reach Discord channels an admin paired with the Haven
channel they are standing in. Each pairing picks its own direction (two way, or
either one way) and its own outgoing mode (mirror everything, or only messages
addressed with the "=>" prefix). Sending needs the new use_ferry permission.
The trigger is "=>" and not ">>" because a leading ">" is Haven's blockquote
marker, so an unresolved target would render as a quote instead of visibly
failing. Resolved targets are stripped from the stored message and recorded in
messages.ferry_target so history shows a destination badge instead of raw
routing syntax.
Loop prevention drops the gateway echo of our own webhook ids and, by default,
every other bot. Mentions are blocked by default so one member cannot ping a
whole Discord server through the bridge. DMs are off by default, outbound only,
and stamped with the author's name, since a bot cannot impersonate in a DM.
ws becomes a direct dependency; it was only reaching us through socket.io.
Covers everything since v3.47.0, including the eight commits that were
pushed after that tag and never released: the invite buttons, the upload
cancel button, unban for moderators who can ban, the sub-channel nesting
permission fix, the Braid Layout fixes, and the group DM encryption design.