fix(rtc): accept encryption_keys to-device without member.id; warn on unparseable keys (#135) #137

Merged
robocub merged 1 commit from fix/commet-encryption-keys-member-id into master 2026-09-28 00:03:16 +00:00
Member

Fixes #135. Upstream note for Commet: #136.

What: Commet's encrypted calls send io.element.call.encryption_keys with member: {claimed_device_id} and no id. Our EncKeyMember required id, so serde rejected the content and matrix-sdk skipped the typed handler silently — the Commet caller's media key was never installed, their frames stayed MissingKey, Discord heard nothing (while our ghosts' keys reached them, so they heard us).

Changes (crates/matrix-rtc):

  • member.id → Option<String> (serde(default)), sent_ts serde(default). Neither feeds identity (sender:claimed_device_id). Our outgoing content is unchanged (id: Some("{device_id}_m.call"), covered by the existing wire-shape tests).
  • Interop tripwire: catch-all AnyToDeviceEvent + RawEvent handler re-parses every encryption_keys event and WARNs with the content's field names (never values) if it doesn't fit our type — so the next client-shape gap is loud instead of silent.
  • 3 unit tests (Commet shape parses, EC shape still parses, shape describer leaks no key bytes) + CHANGELOG [Unreleased].

Verified on forgejo-runner: cargo clippy -p nvb-matrix-rtc --all-targets -D warnings clean; cargo test -p nvb-matrix-rtc 140 passed.

Not yet verified live: a Commet join against this build. Expected after deploy: received io.element.call.encryption_keys to-device … member_id=- → installed peer media key → the #73 heal line, and Discord hears the Commet caller. If the new WARN fires instead, Commet's shape diverges further; if nothing fires, Commet didn't target our device (see #135 residual).

🤖 Generated with Claude Code

Fixes #135. Upstream note for Commet: #136. **What:** Commet's encrypted calls send `io.element.call.encryption_keys` with `member: {claimed_device_id}` and no `id`. Our `EncKeyMember` required `id`, so serde rejected the content and matrix-sdk skipped the typed handler silently — the Commet caller's media key was never installed, their frames stayed `MissingKey`, Discord heard nothing (while our ghosts' keys reached them, so they heard us). **Changes** (`crates/matrix-rtc`): - `member.id` → `Option<String>` (`serde(default)`), `sent_ts` `serde(default)`. Neither feeds identity (`sender:claimed_device_id`). Our outgoing content is unchanged (`id: Some("{device_id}_m.call")`, covered by the existing wire-shape tests). - Interop tripwire: catch-all `AnyToDeviceEvent + RawEvent` handler re-parses every `encryption_keys` event and WARNs with the content's field names (never values) if it doesn't fit our type — so the next client-shape gap is loud instead of silent. - 3 unit tests (Commet shape parses, EC shape still parses, shape describer leaks no key bytes) + CHANGELOG `[Unreleased]`. **Verified on forgejo-runner:** `cargo clippy -p nvb-matrix-rtc --all-targets -D warnings` clean; `cargo test -p nvb-matrix-rtc` 140 passed. **Not yet verified live:** a Commet join against this build. Expected after deploy: `received io.element.call.encryption_keys to-device … member_id=-` → `installed peer media key` → the #73 heal line, and Discord hears the Commet caller. If the new WARN fires instead, Commet's shape diverges further; if nothing fires, Commet didn't target our device (see #135 residual). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(rtc): accept encryption_keys to-device without member.id, warn on unparseable keys (#135)
All checks were successful
CI / test (push) Successful in 5m2s
E2E (live) / e2e (push) Successful in 8m34s
CI / test (pull_request) Successful in 4m57s
4d05ff4e4f
Commet's experimental encrypted calls send io.element.call.encryption_keys
with member: {claimed_device_id} only. EncKeyMember required `id`, so serde
rejected the content and matrix-sdk skipped the typed handler with no log
at prod's RUST_LOG — the caller's media key was never installed, their
frames stayed MissingKey, and Discord heard silence while the ghosts' keys
reached them fine (one-way audio, Dark Voice 2026-09-27).

- member.id -> Option<String> (serde default); sent_ts serde default too
  (js-sdk marks it optional). Neither feeds identity, which is
  sender:claimed_device_id. What we send is unchanged.
- Interop tripwire: catch-all AnyToDeviceEvent + RawEvent handler re-parses
  every encryption_keys event and WARNs with the content's field names
  (never values) when it doesn't fit our type.
- Tests: Commet shape parses, EC shape still parses, shape describer leaks
  no key bytes.

Upstream note for Commet tracked in #136.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
robocub merged commit 15d8d5b15d into master 2026-09-28 00:03:16 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
nether/nether-voicebridge!137
No description provided.