fix: Use same shared key for both directions (caller key reuse)

Both bot and caller must use the same key in shared key mode.
Bot now reuses caller's key and publishes it back, instead of
generating a separate bot key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-21 20:41:23 +02:00
parent 2fa13c4958
commit 2d8a7b4420

View File

@@ -107,11 +107,7 @@ class VoiceSession:
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 the room is already connected, immediately set the key on the
key provider so we can decrypt the caller's audio.
"""
"""Receive E2EE key from Element Call participant."""
if not key:
return
identity = _make_lk_identity(sender, device_id)
@@ -120,15 +116,6 @@ class VoiceSession:
logger.info("E2EE key received from %s:%s (identity=%s, index=%d, %d bytes)",
sender, device_id, identity, index, len(key))
# If already connected, set key on the key provider immediately
if self.lk_room:
try:
kp = self.lk_room.e2ee_manager.key_provider
kp.set_key(identity, key, key_index=index)
logger.info("Live-updated caller E2EE key for %s", identity)
except Exception:
logger.warning("Could not live-update caller E2EE key", exc_info=True)
async def _fetch_encryption_key_http(self) -> bytes | None:
"""Fetch encryption key from room timeline (NOT state) via Matrix HTTP API.
@@ -214,16 +201,16 @@ class VoiceSession:
break
await asyncio.sleep(0.1)
# Publish bot's own key so caller can decrypt our audio
# Use caller's key for both directions (shared key mode).
# Bot publishes the SAME key so caller can decrypt our audio too.
if self._caller_key:
self._bot_key = self._caller_key # reuse caller's key
logger.info("Using caller's key as shared key for both directions (%d bytes)",
len(self._caller_key))
if self._publish_key_cb:
self._publish_key_cb(self._bot_key)
# Connect with caller's key as shared_key for immediate decryption,
# then set per-participant keys after connect for proper separation
connect_key = self._caller_key or self._bot_key
e2ee_opts = _build_e2ee_options(connect_key)
logger.info("E2EE connect key: %d bytes (from %s)",
len(connect_key), "caller" if self._caller_key else "bot")
e2ee_opts = _build_e2ee_options(self._bot_key)
room_opts = rtc.RoomOptions(e2ee=e2ee_opts)
self.lk_room = rtc.Room()
@@ -241,34 +228,9 @@ class VoiceSession:
logger.info("Track sub: %s %s kind=%s", p.identity, pub.sid, t.kind)
await self.lk_room.connect(self.lk_url, jwt, options=room_opts)
logger.info("Connected (E2EE=shared+per-participant), remote=%d",
logger.info("Connected (E2EE=shared key), remote=%d",
len(self.lk_room.remote_participants))
# Set per-participant E2EE keys after connect
bot_identity = _make_lk_identity(user_id, self.device_id)
try:
kp = self.lk_room.e2ee_manager.key_provider
# Set bot's own key (encrypts outgoing audio)
kp.set_key(bot_identity, self._bot_key, key_index=0)
logger.info("Set bot E2EE key for identity=%s (%d bytes)",
bot_identity, len(self._bot_key))
# Set caller's key (decrypts incoming audio)
if self._caller_key and self._caller_identity:
kp.set_key(self._caller_identity, self._caller_key, key_index=0)
logger.info("Set caller E2EE key for identity=%s (%d bytes)",
self._caller_identity, len(self._caller_key))
elif self._caller_key:
for p in self.lk_room.remote_participants.values():
kp.set_key(p.identity, self._caller_key, key_index=0)
logger.info("Set caller E2EE key for identity=%s (%d bytes)",
p.identity, len(self._caller_key))
break
except Exception:
logger.warning("Per-participant key setup failed, shared key used as fallback",
exc_info=True)
# Find the remote participant, wait up to 10s if not yet connected
remote_identity = None
for p in self.lk_room.remote_participants.values():