From 893e07a54337a1562f9391f8a751698fd2f4d5d9 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Sun, 22 Feb 2026 07:19:28 +0200 Subject: [PATCH] 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 --- voice.py | 52 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/voice.py b/voice.py index 3f5dbd9..a3de111 100644 --- a/voice.py +++ b/voice.py @@ -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: