fix: handle Element Call same-index key rotation on screen share

Element Call rotates E2EE keys by re-sending index 0 with a new value
when screen share starts. The LiveKit frame cryptor caches derived AES
keys per index, so overwriting index 0 does not force re-derivation.

Fix: detect when index 0 value changes and map to incrementing internal
index so the frame cryptor gets a fresh key slot. Sets all accumulated
keys on late arrival so cryptor can try both during transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-10 13:43:56 +02:00
parent 3706f568b6
commit 488e50e73c

View File

@@ -495,15 +495,32 @@ class VoiceSession:
Store-only: keys are applied in on_track_subscribed() / on_e2ee_state_changed()
where the frame cryptor is guaranteed to exist. If the track is already
subscribed (late key arrival / rotation), set the key immediately.
Element Call may rotate keys by re-sending index 0 with a new value
(e.g. when screen share starts). The frame cryptor caches derived AES
keys per index, so we use an internal incrementing index to force
re-derivation.
"""
if not key:
return
if not self._caller_key:
self._caller_key = key
self._caller_identity = f"{sender}:{device_id}"
# Detect same-index key rotation: if index 0 changes value, use next internal index
old_key = self._caller_all_keys.get(index)
if old_key and old_key != key:
# Key rotated at same Element Call index — use next available internal index
internal_idx = max(self._caller_all_keys.keys()) + 1
logger.info("Key rotation detected: index %d changed value, mapping to internal index %d",
index, internal_idx)
self._caller_all_keys[internal_idx] = key
self._caller_key = key
effective_index = internal_idx
else:
self._caller_all_keys[index] = key
logger.info("E2EE key received from %s:%s (index=%d, %d bytes, raw=%s)",
sender, device_id, index, len(key), key.hex())
effective_index = index
logger.info("E2EE key received from %s:%s (index=%d, effective=%d, %d bytes, raw=%s)",
sender, device_id, index, effective_index, len(key), key.hex())
# Late key arrival: if track already subscribed, frame cryptor exists — set key now.
if self.lk_room and self._caller_identity:
caller_lk_id = self._caller_identity
@@ -515,8 +532,11 @@ class VoiceSession:
if has_subscribed:
try:
kp = self.lk_room.e2ee_manager.key_provider
_derive_and_set_key(kp, p.identity, key, index)
logger.info("Late key[%d] set for %s", index, p.identity)
# Set ALL keys (old + new) so cryptor can try both
for idx, k in sorted(self._caller_all_keys.items()):
_derive_and_set_key(kp, p.identity, k, idx)
logger.info("Late key rotation: set %d keys for %s",
len(self._caller_all_keys), p.identity)
except Exception as e:
logger.warning("Late key set_key failed: %s", e)
break