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:
65
voice.py
65
voice.py
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user