fix(voice): set E2EE keys immediately after connect, before rotation wait

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 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-22 10:34:20 +02:00
parent 4ab5486b5c
commit 63545f032e

View File

@@ -257,6 +257,23 @@ class VoiceSession:
logger.info("Connected (E2EE=HKDF), remote=%d", logger.info("Connected (E2EE=HKDF), remote=%d",
len(self.lk_room.remote_participants)) 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. # 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 # 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. # and call on_encryption_key(), which updates self._caller_all_keys.
@@ -276,14 +293,6 @@ class VoiceSession:
else: else:
logger.warning("No key rotation after 2s — using pre-join key[%d]", pre_max_idx) 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 # Find the remote participant, wait up to 10s if not yet connected
remote_identity = None remote_identity = None
for p in self.lk_room.remote_participants.values(): for p in self.lk_room.remote_participants.values():