fix(e2ee): pass caller_key as shared_key at connect time
Per-participant set_key() for remote identities doesn't work for incoming decryption in this Rust FFI build (set_shared_key() after connect is also ignored in per-participant mode). Solution: initialize with caller_key as shared_key (true shared-key mode) so the Rust FFI uses it for incoming decryption. Then override outgoing encryption via set_key(bot_identity, bot_key) after connect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
36
voice.py
36
voice.py
@@ -69,14 +69,16 @@ def _generate_lk_jwt(room_id, user_id, device_id):
|
|||||||
KDF_HKDF = 1
|
KDF_HKDF = 1
|
||||||
|
|
||||||
|
|
||||||
def _build_e2ee_options() -> rtc.E2EEOptions:
|
def _build_e2ee_options(caller_key: bytes = b"") -> rtc.E2EEOptions:
|
||||||
"""Build HKDF E2EE options matching Element Call's key derivation.
|
"""Build HKDF E2EE options matching Element Call's key derivation.
|
||||||
|
|
||||||
No shared_key — initializes in per-participant mode so set_key() works.
|
Pass caller_key as shared_key to initialize in true shared-key mode.
|
||||||
|
This ensures the Rust FFI decrypts incoming frames using caller's key.
|
||||||
|
Outgoing encryption is overridden via set_key(bot_identity, bot_key) after connect.
|
||||||
Element Call uses: ratchetWindowSize=16, keyringSize=256, salt="LKFrameEncryptionKey"
|
Element Call uses: ratchetWindowSize=16, keyringSize=256, salt="LKFrameEncryptionKey"
|
||||||
"""
|
"""
|
||||||
key_opts = rtc.KeyProviderOptions(
|
key_opts = rtc.KeyProviderOptions(
|
||||||
shared_key=b"",
|
shared_key=caller_key,
|
||||||
ratchet_window_size=16,
|
ratchet_window_size=16,
|
||||||
ratchet_salt=b"LKFrameEncryptionKey",
|
ratchet_salt=b"LKFrameEncryptionKey",
|
||||||
failure_tolerance=-1,
|
failure_tolerance=-1,
|
||||||
@@ -223,9 +225,10 @@ class VoiceSession:
|
|||||||
break
|
break
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
# Connect with E2EE in per-participant mode (no shared_key)
|
# Connect with caller_key as shared_key so Rust FFI decrypts
|
||||||
# so set_key() calls work correctly for both directions
|
# incoming audio in true shared-key mode. Outgoing encryption
|
||||||
e2ee_opts = _build_e2ee_options()
|
# is overridden to bot_key via set_key(bot_identity) after connect.
|
||||||
|
e2ee_opts = _build_e2ee_options(self._caller_key or b"")
|
||||||
room_opts = rtc.RoomOptions(e2ee=e2ee_opts)
|
room_opts = rtc.RoomOptions(e2ee=e2ee_opts)
|
||||||
self.lk_room = rtc.Room()
|
self.lk_room = rtc.Room()
|
||||||
|
|
||||||
@@ -267,25 +270,12 @@ class VoiceSession:
|
|||||||
if remote_identity:
|
if remote_identity:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Set caller's key(s) — decrypts incoming audio
|
# Caller key was passed as shared_key at connect time — no further
|
||||||
# Use all collected keys with their correct indices (Element Call may rotate)
|
# per-participant set_key needed for decryption.
|
||||||
# Also set as shared key fallback: Rust FFI may not use per-participant
|
|
||||||
# key for remote participants in all code paths.
|
|
||||||
if self._caller_key:
|
if self._caller_key:
|
||||||
caller_id = remote_identity or self._caller_identity
|
logger.info("Caller key active as shared_key (%d bytes, index 0)", len(self._caller_key))
|
||||||
if caller_id:
|
|
||||||
keys_to_set = self._caller_all_keys or {0: self._caller_key}
|
|
||||||
for idx, key in keys_to_set.items():
|
|
||||||
kp.set_key(caller_id, key, idx)
|
|
||||||
logger.info("Set caller key[%d] for %s (%d bytes)", idx, caller_id, len(key))
|
|
||||||
# Shared key fallback: use highest-index caller key
|
|
||||||
max_idx = max(keys_to_set.keys())
|
|
||||||
kp.set_shared_key(keys_to_set[max_idx], max_idx)
|
|
||||||
logger.info("Set shared key fallback[%d] (%d bytes)", max_idx, len(keys_to_set[max_idx]))
|
|
||||||
else:
|
|
||||||
logger.warning("Have caller key but no caller identity")
|
|
||||||
else:
|
else:
|
||||||
logger.warning("No caller E2EE key available")
|
logger.warning("No caller E2EE key — incoming audio will be silence")
|
||||||
|
|
||||||
if remote_identity:
|
if remote_identity:
|
||||||
logger.info("Linking to remote participant: %s", remote_identity)
|
logger.info("Linking to remote participant: %s", remote_identity)
|
||||||
|
|||||||
Reference in New Issue
Block a user