From 63545f032edb23dc29bfbe1468e2c3de051d2cda Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Sun, 22 Feb 2026 10:34:20 +0200 Subject: [PATCH] fix(voice): set E2EE keys immediately after connect, before rotation wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: caller track subscribed during 2s rotation wait creates a frame cryptor with no key → DEC_FAILED state → all incoming frames dropped. Setting the key after the wait doesn't recover the cryptor. Fix: set bot + caller keys immediately after lk_room.connect(), using the Matrix-provided caller identity. The post-rotation and post-find-remote key updates remain as belt+suspenders. Co-Authored-By: Claude Sonnet 4.6 --- voice.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/voice.py b/voice.py index 340494f..47e81ad 100644 --- a/voice.py +++ b/voice.py @@ -257,6 +257,23 @@ class VoiceSession: logger.info("Connected (E2EE=HKDF), remote=%d", len(self.lk_room.remote_participants)) + # *** FIX: Set keys immediately after connect — BEFORE the rotation wait. + # The caller's track is subscribed during the wait; if no key is set when + # the frame cryptor is first created it enters DEC_FAILED and drops all frames + # even after the key is set later. + kp = self.lk_room.e2ee_manager.key_provider + kp.set_key(bot_identity, self._bot_key, 0) + logger.info("Set bot raw key for %s (%d bytes)", bot_identity, len(self._bot_key)) + if self._caller_identity and self._caller_all_keys: + for idx, base_k in sorted(self._caller_all_keys.items()): + kp.set_key(self._caller_identity, base_k, idx) + logger.info("Early-set caller raw keys %s for %s", + list(self._caller_all_keys.keys()), self._caller_identity) + elif self._caller_key and self._caller_identity: + kp.set_key(self._caller_identity, self._caller_key, 0) + logger.info("Early-set caller raw key[0] for %s (%d bytes)", + self._caller_identity, len(self._caller_key)) + # Element Call rotates its encryption key when bot joins the LiveKit room. # EC sends the new key via Matrix (Megolm-encrypted); nio sync will decrypt it # and call on_encryption_key(), which updates self._caller_all_keys. @@ -276,14 +293,6 @@ class VoiceSession: else: logger.warning("No key rotation after 2s — using pre-join key[%d]", pre_max_idx) - # Set per-participant keys via key provider — pass raw base keys. - # KDF_HKDF=1: Rust FFI applies HKDF(base_key, ratchetSalt, ...) internally. - kp = self.lk_room.e2ee_manager.key_provider - - # Bot's own key — raw base key, Rust FFI derives AES frame key via HKDF - kp.set_key(bot_identity, self._bot_key, 0) - logger.info("Set bot raw key for %s (%d bytes)", bot_identity, len(self._bot_key)) - # Find the remote participant, wait up to 10s if not yet connected remote_identity = None for p in self.lk_room.remote_participants.values():