fix: generate and publish E2EE key, always connect with encryption
Element Call encrypts media by default. Bot must: 1. Generate its own 32-byte E2EE key 2. Publish it to room state (io.element.call.encryption_keys) 3. Connect to LiveKit with HKDF E2EE enabled 4. Use caller's key when received, own key as fallback This fixes: Nicht verschlüsselt warning, silent audio (encrypted frames couldn't be decoded by VAD/STT) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
46
voice.py
46
voice.py
@@ -102,6 +102,26 @@ class VoiceSession:
|
||||
logger.info("E2EE key received from %s:%s (index=%d, %d bytes)",
|
||||
sender, device_id, index, len(key))
|
||||
|
||||
async def _publish_e2ee_key(self, key: bytes):
|
||||
"""Publish our E2EE key to room state so Element Call shares its key with us."""
|
||||
import base64 as b64
|
||||
key_b64 = b64.urlsafe_b64encode(key).decode().rstrip("=")
|
||||
content = {
|
||||
"call_id": "",
|
||||
"device_id": self.device_id,
|
||||
"keys": [{"index": 0, "key": key_b64}],
|
||||
}
|
||||
user_id = self.nio_client.user_id
|
||||
state_key = f"{user_id}:{self.device_id}"
|
||||
try:
|
||||
ENCRYPTION_KEYS_TYPE = "io.element.call.encryption_keys"
|
||||
await self.nio_client.room_put_state(
|
||||
self.room_id, ENCRYPTION_KEYS_TYPE, content, state_key=state_key,
|
||||
)
|
||||
logger.info("Published E2EE key (state_key=%s)", state_key)
|
||||
except Exception:
|
||||
logger.exception("Failed to publish E2EE key")
|
||||
|
||||
async def start(self):
|
||||
self._task = asyncio.create_task(self._run())
|
||||
|
||||
@@ -129,20 +149,29 @@ class VoiceSession:
|
||||
user_id = self.nio_client.user_id
|
||||
jwt = _generate_lk_jwt(self.room_id, user_id, self.device_id)
|
||||
|
||||
# Wait up to 10s for E2EE encryption key from Element Call
|
||||
# Generate our own E2EE key and publish it to the room
|
||||
# Element Call requires ALL participants to publish keys
|
||||
import secrets
|
||||
our_key = secrets.token_bytes(32)
|
||||
await self._publish_e2ee_key(our_key)
|
||||
logger.info("Published our E2EE key (%d bytes)", len(our_key))
|
||||
|
||||
# Wait up to 10s for caller's E2EE encryption key
|
||||
for _ in range(100):
|
||||
if self._e2ee_key:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
if not self._e2ee_key:
|
||||
logger.warning("No E2EE key received after 10s, connecting without encryption")
|
||||
|
||||
# Connect with E2EE if key available
|
||||
e2ee_opts = None
|
||||
# Use caller's key if available, otherwise use our own
|
||||
shared_key = self._e2ee_key or our_key
|
||||
if self._e2ee_key:
|
||||
e2ee_opts = _build_e2ee_options(self._e2ee_key)
|
||||
logger.info("E2EE enabled with HKDF (%d byte key)", len(self._e2ee_key))
|
||||
logger.info("Using caller's E2EE key (%d bytes)", len(self._e2ee_key))
|
||||
else:
|
||||
logger.warning("No caller key received after 10s, using our own key")
|
||||
|
||||
e2ee_opts = _build_e2ee_options(shared_key)
|
||||
|
||||
logger.info("E2EE enabled with HKDF")
|
||||
room_opts = rtc.RoomOptions(e2ee=e2ee_opts)
|
||||
self.lk_room = rtc.Room()
|
||||
|
||||
@@ -159,8 +188,7 @@ 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=%s), remote=%d",
|
||||
"HKDF" if self._e2ee_key else "off",
|
||||
logger.info("Connected (E2EE=HKDF), remote=%d",
|
||||
len(self.lk_room.remote_participants))
|
||||
|
||||
# Find the remote participant to link to
|
||||
|
||||
Reference in New Issue
Block a user