diff --git a/voice.py b/voice.py index 261c0e9..88f4021 100644 --- a/voice.py +++ b/voice.py @@ -102,6 +102,41 @@ class VoiceSession: logger.info("E2EE key received from %s:%s (index=%d, %d bytes)", 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): """Publish our E2EE key to room state so Element Call shares its key with us.""" import base64 as b64 @@ -149,29 +184,31 @@ class VoiceSession: user_id = self.nio_client.user_id jwt = _generate_lk_jwt(self.room_id, user_id, self.device_id) - # Generate our own E2EE key and publish it to the room - # Element Call requires ALL participants to publish keys - import secrets - our_key = secrets.token_bytes(32) - await self._publish_e2ee_key(our_key) - logger.info("Published our E2EE key (%d bytes)", len(our_key)) + # Actively fetch encryption key from room state via HTTP API + # Element Call publishes keys as state events during active calls + caller_key = await self._fetch_encryption_key_http() - # Wait up to 10s for caller's E2EE encryption key - for _ in range(100): - if self._e2ee_key: - break - 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: - logger.info("Using caller's E2EE key (%d bytes)", len(self._e2ee_key)) + if caller_key: + self._e2ee_key = caller_key + logger.info("Got caller E2EE key via HTTP (%d bytes)", len(caller_key)) else: - logger.warning("No caller key received after 10s, using our own key") + # Wait up to 10s for key via sync handler + logger.info("No key in room state yet, waiting for sync...") + for _ in range(100): + if self._e2ee_key: + break + await asyncio.sleep(0.1) - e2ee_opts = _build_e2ee_options(shared_key) + if self._e2ee_key: + shared_key = self._e2ee_key + logger.info("Using caller's E2EE key (%d bytes)", len(shared_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) + 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) self.lk_room = rtc.Room()