fix(e2ee): set caller keys at correct indices from timeline

Element Call may rotate encryption keys to index > 0. Previously we
always called set_key(identity, key, 0) regardless of the actual index,
causing decryption to fail when the active key was at a non-zero index.

- _fetch_encryption_key_http: collect all {index->key} pairs from event
- _run: set each caller key at its correct index
- on_encryption_key: handle multiple indices, remove first-key-only gate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-22 07:19:28 +02:00
parent 685218247a
commit 893e07a543

View File

@@ -100,24 +100,29 @@ class VoiceSession:
self._http_session = None
self._caller_key: bytes | None = None
self._caller_identity: str | None = None
self._caller_all_keys: dict = {} # {index: bytes} — all caller keys by index
self._bot_key: bytes = bot_key or os.urandom(16)
self._publish_key_cb = publish_key_cb
def on_encryption_key(self, sender, device_id, key, index):
"""Receive E2EE key from Element Call participant."""
if key and not self._caller_key:
if not key:
return
if not self._caller_key:
self._caller_key = key
self._caller_identity = f"{sender}:{device_id}"
logger.info("E2EE key received from %s:%s (index=%d, %d bytes)",
sender, device_id, index, len(key))
# Live-update key provider if already connected
if self.lk_room and hasattr(self.lk_room, 'e2ee_manager'):
try:
kp = self.lk_room.e2ee_manager.key_provider
kp.set_key(self._caller_identity, key, index)
logger.info("Live-set caller key for %s", self._caller_identity)
except Exception as e:
logger.warning("Failed to live-set caller key: %s", e)
self._caller_all_keys[index] = key
logger.info("E2EE key received from %s:%s (index=%d, %d bytes)",
sender, device_id, index, len(key))
# Live-update key provider if already connected
if self.lk_room and hasattr(self.lk_room, 'e2ee_manager'):
try:
kp = self.lk_room.e2ee_manager.key_provider
caller_id = self._caller_identity or f"{sender}:{device_id}"
kp.set_key(caller_id, key, index)
logger.info("Live-set caller key[%d] for %s", index, caller_id)
except Exception as e:
logger.warning("Failed to live-set caller key: %s", e)
async def _fetch_encryption_key_http(self) -> bytes | None:
"""Fetch encryption key from room timeline (NOT state) via Matrix HTTP API.
@@ -149,15 +154,23 @@ class VoiceSession:
device = content.get("device_id", "")
logger.info("Found encryption_keys timeline event: sender=%s device=%s",
sender, device)
all_keys = {}
import base64 as b64
for k in content.get("keys", []):
key_b64 = k.get("key", "")
key_index = k.get("index", 0)
if key_b64:
key_b64 += "=" * (-len(key_b64) % 4)
import base64 as b64
key_bytes = b64.urlsafe_b64decode(key_b64)
if device:
self._caller_identity = f"{sender}:{device}"
return key_bytes
all_keys[key_index] = key_bytes
if all_keys:
if device:
self._caller_identity = f"{sender}:{device}"
self._caller_all_keys.update(all_keys)
max_idx = max(all_keys.keys())
logger.info("Loaded caller keys at indices %s (using %d)",
sorted(all_keys.keys()), max_idx)
return all_keys[max_idx]
logger.info("No encryption_keys events in last %d timeline events", len(events))
except Exception as e:
logger.warning("HTTP encryption key fetch failed: %s", e)
@@ -254,12 +267,15 @@ class VoiceSession:
if remote_identity:
break
# Set caller's key — decrypts incoming audio
# Set caller's key(s) — decrypts incoming audio
# Use all collected keys with their correct indices (Element Call may rotate)
if self._caller_key:
caller_id = remote_identity or self._caller_identity
if caller_id:
kp.set_key(caller_id, self._caller_key, 0)
logger.info("Set caller key for %s (%d bytes)", caller_id, len(self._caller_key))
keys_to_set = self._caller_all_keys or {0: self._caller_key}
for idx, key in keys_to_set.items():
kp.set_key(caller_id, key, idx)
logger.info("Set caller key[%d] for %s (%d bytes)", idx, caller_id, len(key))
else:
logger.warning("Have caller key but no caller identity")
else: