diff --git a/voice.py b/voice.py index 7083105..261c0e9 100644 --- a/voice.py +++ b/voice.py @@ -102,6 +102,26 @@ class VoiceSession: logger.info("E2EE key received from %s:%s (index=%d, %d bytes)", sender, device_id, index, len(key)) + 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 + key_b64 = b64.urlsafe_b64encode(key).decode().rstrip("=") + content = { + "call_id": "", + "device_id": self.device_id, + "keys": [{"index": 0, "key": key_b64}], + } + user_id = self.nio_client.user_id + state_key = f"{user_id}:{self.device_id}" + try: + ENCRYPTION_KEYS_TYPE = "io.element.call.encryption_keys" + await self.nio_client.room_put_state( + self.room_id, ENCRYPTION_KEYS_TYPE, content, state_key=state_key, + ) + logger.info("Published E2EE key (state_key=%s)", state_key) + except Exception: + logger.exception("Failed to publish E2EE key") + async def start(self): self._task = asyncio.create_task(self._run()) @@ -129,20 +149,29 @@ class VoiceSession: user_id = self.nio_client.user_id jwt = _generate_lk_jwt(self.room_id, user_id, self.device_id) - # Wait up to 10s for E2EE encryption key from Element Call + # 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)) + + # Wait up to 10s for caller's E2EE encryption key for _ in range(100): if self._e2ee_key: break await asyncio.sleep(0.1) - if not self._e2ee_key: - logger.warning("No E2EE key received after 10s, connecting without encryption") - # Connect with E2EE if key available - e2ee_opts = None + # Use caller's key if available, otherwise use our own + shared_key = self._e2ee_key or our_key if self._e2ee_key: - e2ee_opts = _build_e2ee_options(self._e2ee_key) - logger.info("E2EE enabled with HKDF (%d byte key)", len(self._e2ee_key)) + logger.info("Using caller's E2EE key (%d bytes)", len(self._e2ee_key)) + else: + logger.warning("No caller key received after 10s, using our own key") + e2ee_opts = _build_e2ee_options(shared_key) + + logger.info("E2EE enabled with HKDF") room_opts = rtc.RoomOptions(e2ee=e2ee_opts) self.lk_room = rtc.Room() @@ -159,8 +188,7 @@ class VoiceSession: logger.info("Track sub: %s %s kind=%s", p.identity, pub.sid, t.kind) await self.lk_room.connect(self.lk_url, jwt, options=room_opts) - logger.info("Connected (E2EE=%s), remote=%d", - "HKDF" if self._e2ee_key else "off", + logger.info("Connected (E2EE=HKDF), remote=%d", len(self.lk_room.remote_participants)) # Find the remote participant to link to