fix(voice): add MSC4143 call.member encryption key support

Element Call v0.17+ embeds encryption_keys in call.member state events
instead of separate timeline events. In E2EE rooms, timeline events are
encrypted and the bot HTTP fetch cannot decrypt them, causing DEC_FAILED.

- Extract caller keys from call.member state event on join
- Embed bot key in call.member state event
- Check call.member state in key fetch (before timeline fallback)
- Handle key updates in call.member during active calls
- Update voice.py key poller to check call.member state first
- Add debug logging for UnknownEvent types in call rooms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-23 08:12:44 +02:00
parent dcee2d30d5
commit bfc717372c
2 changed files with 154 additions and 24 deletions

View File

@@ -543,13 +543,50 @@ class VoiceSession:
break
async def _fetch_encryption_key_http(self) -> bytes | None:
"""Fetch encryption key from room timeline (NOT state) via Matrix HTTP API.
"""Fetch encryption key from call.member state (MSC4143) or timeline (legacy).
Element Call distributes encryption keys as timeline events, not state.
MSC4143: encryption_keys in org.matrix.msc3401.call.member state
Legacy: io.element.call.encryption_keys timeline events
"""
import httpx
homeserver = str(self.nio_client.homeserver)
token = self.nio_client.access_token
user_id = self.nio_client.user_id
# MSC4143: check call.member state events first
try:
state_url = f"{homeserver}/_matrix/client/v3/rooms/{self.room_id}/state"
async with httpx.AsyncClient(timeout=10.0) as http:
resp = await http.get(state_url, headers={"Authorization": f"Bearer {token}"})
if resp.status_code == 200:
for evt in resp.json():
if evt.get("type") != "org.matrix.msc3401.call.member":
continue
sender = evt.get("sender", "")
if sender == user_id:
continue
content = evt.get("content", {})
enc_keys = content.get("encryption_keys", [])
if enc_keys:
device = content.get("device_id", "")
import base64 as b64
for k in enc_keys:
key_b64 = k.get("key", "")
key_index = k.get("index", 0)
if key_b64:
key_b64 += "=" * (-len(key_b64) % 4)
key_bytes = b64.urlsafe_b64decode(key_b64)
if device:
self._caller_identity = f"{sender}:{device}"
self.on_encryption_key(sender, device, key_bytes, key_index)
max_idx = max(self._caller_all_keys.keys()) if self._caller_all_keys else key_index
latest = self._caller_all_keys.get(max_idx, key_bytes)
logger.info("Got key from call.member state (sender=%s, index=%d)", sender, key_index)
return latest
except Exception as e:
logger.debug("call.member state key fetch failed: %s", e)
# Legacy: timeline scan
url = f"{homeserver}/_matrix/client/v3/rooms/{self.room_id}/messages"
try:
async with httpx.AsyncClient(timeout=10.0) as http: