fix(e2ee): bind a key epoch into room-key offers and show a per-call session fingerprint (#1394)

* fix(e2ee): bind the key epoch into wrapped room-key offers

The holder's rotation counter now rides inside encrypted_key as a
versioned header and is bound as AES-GCM additional data, so a receiver
can tell a current room key from a superseded one. Receivers keep a
per-sender high-water mark and apply an offer only at or above it; the
mark resets when that sender announces a fresh ephemeral key. Blobs in
the pre-epoch layout are still accepted for holders on the older build
(compat path, scheduled for removal next release). No server or schema
change: the relay treats encrypted_key as opaque.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(e2ee): show a per-call session fingerprint for every voice peer

A peer with no published identity key has no safety number, so the TOFU
badge gave the user nothing to compare out of band. Every accepted
announce now also carries a fingerprint of the peer's ephemeral session
key, shown on the unverified badge and labelled as changing every call
and not an identity; the local user's own session fingerprint is shown
on their row so it can be read back. safetyNumber is unchanged and stays
null for unverified peers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ledger): mark OC-0001 and OC-0003 fixed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-19 19:04:51 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent c86d803a18
commit eacba10cff
12 changed files with 566 additions and 70 deletions
+16
View File
@@ -1175,6 +1175,22 @@ The key holder wraps the room key for a specific participant:
}
```
`encrypted_key` is opaque to the server (base64 and length checks only). Its
layout is:
```
0x01 ‖ epoch (u64 big-endian) ‖ AES-GCM ciphertext of the 32-byte room key
```
`epoch` is the holder's key-rotation counter, bound as GCM additional data so
the relay cannot change it without failing authentication. A receiver applies
an offer only if its epoch is at least the highest it has already applied from
that sender (equal is allowed: the holder re-sends the current key when a peer
re-announces); the mark resets when the sender announces a new ephemeral key.
A blob with no header (exactly 48 bytes: key + GCM tag) is the pre-epoch
format and is still accepted from holders on an older build; that
compatibility path is scheduled for removal in the next release.
### voice_e2ee_offer (Server -> Client, relay to target)
Delivered only to `target_user_id`, with the sender attached: