fix: E2EE key lookup for Element Call voice sessions
- Fix state_key format: try @user:domain:DEVICE_ID (Element Call format), then @user:domain, then scan all room state as fallback - Publish bot E2EE key to room so Element shows encrypted status - Extract caller device_id from call member event content - Also fix pipecat-poc pipeline with context aggregators (CF-1579) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
81
bot.py
81
bot.py
@@ -424,14 +424,19 @@ class Bot:
|
||||
model=model,
|
||||
)
|
||||
# Read existing encryption keys from room state before starting
|
||||
caller_key = await self._get_call_encryption_key(room_id, event.sender)
|
||||
caller_device_id = content.get("device_id", "")
|
||||
caller_key = await self._get_call_encryption_key(room_id, event.sender, caller_device_id)
|
||||
if caller_key:
|
||||
vs.on_encryption_key(event.sender, "", caller_key, 0)
|
||||
vs.on_encryption_key(event.sender, caller_device_id, caller_key, 0)
|
||||
|
||||
await vs.start()
|
||||
self.voice_sessions[room_id] = vs
|
||||
logger.info("Voice session started for room %s (e2ee_key=%s)",
|
||||
room_id, "yes" if caller_key else "no")
|
||||
|
||||
# Publish our E2EE key so Element Call sees us as encrypted
|
||||
if caller_key:
|
||||
await self._publish_encryption_key(room_id, caller_key)
|
||||
except Exception:
|
||||
logger.exception("Voice session start failed for %s", room_id)
|
||||
|
||||
@@ -1430,24 +1435,64 @@ class Bot:
|
||||
},
|
||||
)
|
||||
|
||||
async def _get_call_encryption_key(self, room_id: str, sender: str) -> bytes | None:
|
||||
"""Read E2EE encryption key from io.element.call.encryption_keys state events."""
|
||||
async def _get_call_encryption_key(self, room_id: str, sender: str, caller_device_id: str = "") -> bytes | None:
|
||||
"""Read E2EE encryption key from io.element.call.encryption_keys state events.
|
||||
|
||||
Element Call uses state_key format: @user:domain:DEVICE_ID
|
||||
Falls back to trying just @user:domain and scanning all room state.
|
||||
"""
|
||||
# Try state_key formats: @user:domain:device_id, then @user:domain
|
||||
state_keys_to_try = []
|
||||
if caller_device_id:
|
||||
state_keys_to_try.append(f"{sender}:{caller_device_id}")
|
||||
state_keys_to_try.append(sender)
|
||||
|
||||
for state_key in state_keys_to_try:
|
||||
try:
|
||||
resp = await self.client.room_get_state_event(
|
||||
room_id, ENCRYPTION_KEYS_TYPE, state_key,
|
||||
)
|
||||
key = self._extract_e2ee_key(resp, sender, state_key)
|
||||
if key:
|
||||
return key
|
||||
except Exception as e:
|
||||
logger.debug("No encryption key at state_key=%s: %s", state_key, e)
|
||||
|
||||
# Fallback: scan all room state for any encryption_keys event
|
||||
try:
|
||||
resp = await self.client.room_get_state_event(
|
||||
room_id, ENCRYPTION_KEYS_TYPE, sender,
|
||||
)
|
||||
if hasattr(resp, "content") and resp.content:
|
||||
keys = resp.content.get("keys", [])
|
||||
if keys:
|
||||
key_b64 = keys[0].get("key", "")
|
||||
if key_b64:
|
||||
# Element Call uses base64url encoding
|
||||
key_b64 += "=" * (-len(key_b64) % 4) # pad
|
||||
key = base64.urlsafe_b64decode(key_b64)
|
||||
logger.info("Got E2EE key from %s (%d bytes)", sender, len(key))
|
||||
return key
|
||||
resp = await self.client.room_get_state(room_id)
|
||||
if hasattr(resp, "events"):
|
||||
for evt in resp.events:
|
||||
if evt.get("type") == ENCRYPTION_KEYS_TYPE and evt.get("sender") != BOT_USER:
|
||||
content = evt.get("content", {})
|
||||
keys = content.get("keys", [])
|
||||
for k in keys:
|
||||
key_b64 = k.get("key", "")
|
||||
if key_b64:
|
||||
key_b64 += "=" * (-len(key_b64) % 4)
|
||||
key = base64.urlsafe_b64decode(key_b64)
|
||||
logger.info("Got E2EE key from room state scan (%s, %d bytes)",
|
||||
evt.get("state_key", "?"), len(key))
|
||||
return key
|
||||
except Exception as e:
|
||||
logger.debug("No encryption key from %s in %s: %s", sender, room_id, e)
|
||||
logger.debug("Room state scan for encryption keys failed: %s", e)
|
||||
|
||||
logger.warning("No E2EE encryption key found for %s in %s", sender, room_id)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_e2ee_key(resp, sender: str, state_key: str) -> bytes | None:
|
||||
"""Extract E2EE key bytes from a state event response."""
|
||||
if not hasattr(resp, "content") or not resp.content:
|
||||
return None
|
||||
keys = resp.content.get("keys", [])
|
||||
for k in keys:
|
||||
key_b64 = k.get("key", "")
|
||||
if key_b64:
|
||||
key_b64 += "=" * (-len(key_b64) % 4)
|
||||
key = base64.urlsafe_b64decode(key_b64)
|
||||
logger.info("Got E2EE key from %s (state_key=%s, %d bytes)", sender, state_key, len(key))
|
||||
return key
|
||||
return None
|
||||
|
||||
async def _publish_encryption_key(self, room_id: str, key: bytes):
|
||||
|
||||
Reference in New Issue
Block a user