diff --git a/voice.py b/voice.py index b5b9ad0..a0b1076 100644 --- a/voice.py +++ b/voice.py @@ -257,6 +257,24 @@ class VoiceSession: logger.info("Connected (E2EE=HKDF), remote=%d", len(self.lk_room.remote_participants)) + # Element Call rotates its encryption key when bot joins the LiveKit room. + # We fetched the pre-join key above; now wait for EC to generate and publish + # the post-join rotated key via Matrix timeline. + pre_max_idx = max(self._caller_all_keys.keys()) if self._caller_all_keys else -1 + logger.info("Polling for EC key rotation (pre-join max_idx=%d)...", pre_max_idx) + for _attempt in range(10): # poll up to 5s (10 × 0.5s) + await asyncio.sleep(0.5) + await self._fetch_encryption_key_http() + new_max = max(self._caller_all_keys.keys()) if self._caller_all_keys else -1 + if new_max > pre_max_idx: + self._caller_key = self._caller_all_keys[new_max] + logger.info("Key rotated: index %d→%d (%d bytes)", + pre_max_idx, new_max, len(self._caller_key)) + break + logger.debug("Key rotation poll %d: max_idx still %d", _attempt + 1, new_max) + else: + logger.warning("No key rotation after 5s — using pre-join key[%d]", pre_max_idx) + # Set per-participant keys via key provider kp = self.lk_room.e2ee_manager.key_provider @@ -279,21 +297,24 @@ class VoiceSession: if remote_identity: break - # Set caller's per-participant key (HKDF info=caller_identity = matching EC JS). - if self._caller_key and remote_identity: + # Set ALL known caller keys (per-participant, HKDF info=remote_identity). + # EC may have already rotated (index 0→1) by the time bot connects. + if self._caller_all_keys and remote_identity: try: - kp.set_key(remote_identity, self._caller_key, 0) - logger.info("Set caller key for %s (index=0, %d bytes)", remote_identity, len(self._caller_key)) - # Also set via caller_identity (belt+suspenders if identities differ) + for idx, k in sorted(self._caller_all_keys.items()): + kp.set_key(remote_identity, k, idx) + logger.info("Set caller key[%d] for %s (%d bytes)", idx, remote_identity, len(k)) + # Belt+suspenders: also set via matrix identity if different from LK identity if self._caller_identity and self._caller_identity != remote_identity: - kp.set_key(self._caller_identity, self._caller_key, 0) - logger.info("Also set caller key via identity %s", self._caller_identity) + for idx, k in sorted(self._caller_all_keys.items()): + kp.set_key(self._caller_identity, k, idx) + logger.info("Also set all caller keys via identity %s", self._caller_identity) except Exception as e: - logger.warning("Failed to set caller per-participant key: %s", e) - elif not self._caller_key: - logger.warning("No caller E2EE key — incoming audio will be silence") + logger.warning("Failed to set caller per-participant keys: %s", e) + elif not self._caller_all_keys: + logger.warning("No caller E2EE keys — incoming audio will be silence") elif not remote_identity: - logger.warning("No remote participant found — caller key not set") + logger.warning("No remote participant found — caller keys not set") if remote_identity: logger.info("Linking to remote participant: %s", remote_identity)