Skip to content

Make call encryption keys work: array shape + room-event transport - #32

Merged
TroyHernandez merged 7 commits into
mainfrom
fix-call-key-array
Oct 8, 2026
Merged

TroyHernandez merged 7 commits into
mainfrom
fix-call-key-array

Conversation

@TroyHernandez

@TroyHernandez TroyHernandez commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Make MatrixRTC call encryption keys actually work, found during the first live FluffyChat↔bot call: the bot connected, subscribed to the caller's audio, received frames, but every frame was undecryptable (e2ee_state_changed MISSING_KEY) because no peer key was ever applied.

Two independent bugs, both in the call-key path:

1. keys is an array, not an object. The io.element.call.encryption_keys content carries keys as an array of {index, key}. mx_call_key_content() emitted it as a single object and mx_call_key_parse() read it as one; off the wire the Olm/megolm plaintext is parsed with simplifyVector = FALSE, so keys arrives as a list of lists, $index is NULL, and every real key event was dropped. The tests encoded the same wrong shape, so they passed against it.

2. FluffyChat sends the key as a room event, not to-device. matrix-dart-sdk distributes the call key as an encrypted room event with a top-level device_id. mx_crypto_process_sync() flattened every decrypted room event to a text-message record, dropping type and content, so the key was unreachable even once decrypted; and mx_call_handle() only read to_device.

Changes:

  • mx_call_key_content(): keys serializes as an array; superset content carrying both top-level device_id (FluffyChat) and the member block (Element Call), so one object serves either transport.
  • mx_call_key_parse(): reads the array (tolerating simplifyVector = FALSE list-of-lists, a data frame, and a lone object), accepts both the to-device shape (member.claimed_device_id, content room_id) and the room-event shape (top-level device_id, room from the event); returns one entry per key.
  • mx_crypto_process_sync(): decrypted room-event records keep their inner type and content (additive; text consumers still read body).
  • mx_call_handle(): applies call keys from processed$events (room events in the call's room) as well as to_device.
  • mx_call_send_key(): also posts the key as an encrypted room event (guarded) so FluffyChat can decrypt the bot; mx_send_encrypted() gained an event_type argument.

Tests: spec-shaped events parsed with simplifyVector = FALSE (the exact regressed shape), to-device and room-event shapes, multi-key events, object tolerance, handle reading room-event keys, and a serialization check that keys is a JSON array. 935 pass. Version 0.2.1.2.

The io.element.call.encryption_keys wire format (MSC3401 / Element Call
/ FluffyChat) carries keys as an array of {index, key}, but
mx_call_key_content() emitted and mx_call_key_parse() read it as a
single object. Off the wire the Olm plaintext is parsed with
simplifyVector = FALSE, so keys arrived as a list of lists, $index was
NULL, and the parser dropped every real key event: a peer's audio
decrypted to silence (e2ee_state_changed MISSING_KEY) and our own key
reached no one. The tests encoded the same wrong object shape, so they
passed against it.

mx_call_key_content() now wraps the entry so it serializes as an array;
mx_call_key_parse() reads the array (tolerating a data frame or a lone
object) and returns one entry per key, so a single event can carry
several indices; mx_call_handle() applies each. Tests use a spec-shaped
event parsed with simplifyVector = FALSE, the exact shape that
regressed.
FluffyChat / matrix-dart-sdk distributes the call encryption key as an
encrypted ROOM event with a top-level device_id, not a to-device event.
mx_crypto_process_sync() dropped type and content from decrypted room
events, so the key was unreachable even after it was decrypted, and
mx_call_handle() only looked at to-device: the bot received the caller's
audio but had no key and decoded silence.

- mx_crypto_process_sync(): decrypted room-event records keep their
  inner type and content (additive; text consumers still read body).
- mx_call_handle(): reads call keys from processed$events (room events
  in the call's room) as well as to_device.
- mx_call_key_parse(): accepts the room-event shape (top-level
  device_id, room taken from the event) alongside the to-device shape.
- mx_call_key_content(): superset carrying both device_id and member, so
  one object serves either transport.
- mx_call_send_key(): also posts the key as an encrypted room event
  (guarded) so FluffyChat can decrypt the bot; mx_send_encrypted() gained
  an event_type argument.
A leftover title/description block from the previous edit attached to
the internal helper, generating a stray man page and a roxygen
undocumented-parameter warning.
@TroyHernandez TroyHernandez changed the title Fix call encryption keys: keys is an array of {index, key} Make call encryption keys work: array shape + room-event transport Oct 7, 2026
mx_call_send_key posts our key as a room event; the homeserver echoes
it back in our own sync, where mx_call_handle's processed$events scan
picked it up and applied our own key to our own participant slot.
Skip parsed entries whose identity is the call's own.
A peer can open several Olm sessions to us and reply on any of them. The
single inbound slot per peer was overwritten by each new session, so a
message on a replaced session failed to decrypt and was dropped silently
(a FluffyChat call key arrived on a session the bot had replaced, leaving
the caller's audio MISSING_KEY). olm_in now holds a list of inbound
sessions per peer curve25519; mx_crypto_process_sync appends new inbound
sessions rather than overwriting, and olm_receive tries them all plus the
outbound session. Legacy single-session stores load unchanged.

The 'cannot decrypt Olm to-device message' warning now reports the
message type, how many sessions were tried, and the create_inbound error
for a failed prekey, so a dropped message is diagnosable.
@TroyHernandez
TroyHernandez merged commit fddd668 into main Oct 8, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the fix-call-key-array branch October 8, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant