fix: use caller's E2EE key (not own), fetch via HTTP API

All participants must use the SAME shared key. Bot was generating
its own key which couldn't decrypt user's audio. Now:
1. Fetch caller's key from room state via HTTP API
2. Fall back to waiting for key via sync handler
3. Publish the SAME key back (not a new one)
4. Only connect with E2EE if key available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-21 17:17:33 +02:00
parent 753d6543d4
commit 4e1e372ca2

View File

@@ -102,6 +102,41 @@ class VoiceSession:
logger.info("E2EE key received from %s:%s (index=%d, %d bytes)", logger.info("E2EE key received from %s:%s (index=%d, %d bytes)",
sender, device_id, index, len(key)) sender, device_id, index, len(key))
async def _fetch_encryption_key_http(self) -> bytes | None:
"""Fetch encryption key from room state via Matrix HTTP API."""
import httpx
homeserver = str(self.nio_client.homeserver)
token = self.nio_client.access_token
url = f"{homeserver}/_matrix/client/v3/rooms/{self.room_id}/state"
try:
async with httpx.AsyncClient(timeout=10.0) as http:
resp = await http.get(url, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
events = resp.json()
for evt in events:
evt_type = evt.get("type", "")
if evt_type == "io.element.call.encryption_keys":
sender = evt.get("sender", "")
user_id = self.nio_client.user_id
if sender == user_id:
continue # skip our own key
content = evt.get("content", {})
state_key = evt.get("state_key", "")
logger.info("Found encryption_keys event: sender=%s state_key=%s content=%s",
sender, state_key, content)
for k in content.get("keys", []):
key_b64 = k.get("key", "")
if key_b64:
key_b64 += "=" * (-len(key_b64) % 4)
import base64 as b64
return b64.urlsafe_b64decode(key_b64)
# Log all state event types for debugging
types = [e.get("type", "") for e in events]
logger.info("Room state event types: %s", types)
except Exception as e:
logger.warning("HTTP encryption key fetch failed: %s", e)
return None
async def _publish_e2ee_key(self, key: bytes): async def _publish_e2ee_key(self, key: bytes):
"""Publish our E2EE key to room state so Element Call shares its key with us.""" """Publish our E2EE key to room state so Element Call shares its key with us."""
import base64 as b64 import base64 as b64
@@ -149,29 +184,31 @@ class VoiceSession:
user_id = self.nio_client.user_id user_id = self.nio_client.user_id
jwt = _generate_lk_jwt(self.room_id, user_id, self.device_id) jwt = _generate_lk_jwt(self.room_id, user_id, self.device_id)
# Generate our own E2EE key and publish it to the room # Actively fetch encryption key from room state via HTTP API
# Element Call requires ALL participants to publish keys # Element Call publishes keys as state events during active calls
import secrets caller_key = await self._fetch_encryption_key_http()
our_key = secrets.token_bytes(32)
await self._publish_e2ee_key(our_key)
logger.info("Published our E2EE key (%d bytes)", len(our_key))
# Wait up to 10s for caller's E2EE encryption key if caller_key:
self._e2ee_key = caller_key
logger.info("Got caller E2EE key via HTTP (%d bytes)", len(caller_key))
else:
# Wait up to 10s for key via sync handler
logger.info("No key in room state yet, waiting for sync...")
for _ in range(100): for _ in range(100):
if self._e2ee_key: if self._e2ee_key:
break break
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# Use caller's key if available, otherwise use our own
shared_key = self._e2ee_key or our_key
if self._e2ee_key: if self._e2ee_key:
logger.info("Using caller's E2EE key (%d bytes)", len(self._e2ee_key)) shared_key = self._e2ee_key
else: logger.info("Using caller's E2EE key (%d bytes)", len(shared_key))
logger.warning("No caller key received after 10s, using our own key") # Publish the SAME key so Element Call sees us as encrypted
await self._publish_e2ee_key(shared_key)
e2ee_opts = _build_e2ee_options(shared_key) e2ee_opts = _build_e2ee_options(shared_key)
else:
logger.warning("No E2EE key available — connecting WITHOUT encryption")
e2ee_opts = None
logger.info("E2EE enabled with HKDF")
room_opts = rtc.RoomOptions(e2ee=e2ee_opts) room_opts = rtc.RoomOptions(e2ee=e2ee_opts)
self.lk_room = rtc.Room() self.lk_room = rtc.Room()