feat(e2ee): Add HKDF E2EE support for Element Call compatibility
Element Call uses HKDF-SHA256 + AES-128-GCM for frame encryption, while the LiveKit Rust SDK defaults to PBKDF2 + AES-256-GCM. - Multi-stage Dockerfile builds patched Rust FFI from EC-compat fork - Generates Python protobuf bindings with new fields - patch_sdk.py modifies installed livekit-rtc for new proto fields - agent.py passes E2EE options with HKDF to ctx.connect() - bot.py exchanges encryption keys via Matrix state events - Separate Dockerfile.bot for bot service (no Rust build needed) Ref: livekit/rust-sdks#904, livekit/python-sdks#570 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
53
bot.py
53
bot.py
@@ -38,6 +38,7 @@ from livekit import api
|
||||
|
||||
BOT_DEVICE_ID = "AIBOT"
|
||||
CALL_MEMBER_TYPE = "org.matrix.msc3401.call.member"
|
||||
ENCRYPTION_KEYS_TYPE = "io.element.call.encryption_keys"
|
||||
MODEL_STATE_TYPE = "ai.agiliton.model"
|
||||
RENAME_STATE_TYPE = "ai.agiliton.auto_rename"
|
||||
|
||||
@@ -398,14 +399,27 @@ class Bot:
|
||||
|
||||
if room_id not in self.dispatched_rooms:
|
||||
try:
|
||||
# Collect E2EE encryption keys from room state
|
||||
e2ee_key = await self._get_call_encryption_key(room_id, event.sender)
|
||||
dispatch_metadata = ""
|
||||
if e2ee_key:
|
||||
# Generate agent's own key and publish it
|
||||
agent_key = os.urandom(32)
|
||||
await self._publish_encryption_key(room_id, agent_key)
|
||||
dispatch_metadata = json.dumps({
|
||||
"e2ee_key": base64.b64encode(agent_key).decode(),
|
||||
})
|
||||
logger.info("E2EE key prepared for agent dispatch")
|
||||
|
||||
await self.lkapi.agent_dispatch.create_dispatch(
|
||||
api.CreateAgentDispatchRequest(
|
||||
agent_name=AGENT_NAME,
|
||||
room=lk_room_name,
|
||||
metadata=dispatch_metadata,
|
||||
)
|
||||
)
|
||||
self.dispatched_rooms.add(room_id)
|
||||
logger.info("Agent dispatched to LiveKit room %s", lk_room_name)
|
||||
logger.info("Agent dispatched to LiveKit room %s (e2ee=%s)", lk_room_name, bool(e2ee_key))
|
||||
except Exception:
|
||||
logger.exception("Dispatch failed for %s", lk_room_name)
|
||||
|
||||
@@ -1395,6 +1409,43 @@ class Bot:
|
||||
},
|
||||
)
|
||||
|
||||
async def _get_call_encryption_key(self, room_id: str, sender: str) -> bytes | None:
|
||||
"""Read E2EE encryption key from io.element.call.encryption_keys state events."""
|
||||
try:
|
||||
resp = await self.client.room_get_state_event(
|
||||
room_id, ENCRYPTION_KEYS_TYPE, sender,
|
||||
)
|
||||
if hasattr(resp, "content") and resp.content:
|
||||
keys = resp.content.get("keys", [])
|
||||
if keys:
|
||||
key_b64 = keys[0].get("key", "")
|
||||
if key_b64:
|
||||
# Element Call uses base64url encoding
|
||||
key_b64 += "=" * (-len(key_b64) % 4) # pad
|
||||
key = base64.urlsafe_b64decode(key_b64)
|
||||
logger.info("Got E2EE key from %s (%d bytes)", sender, len(key))
|
||||
return key
|
||||
except Exception as e:
|
||||
logger.debug("No encryption key from %s in %s: %s", sender, room_id, e)
|
||||
return None
|
||||
|
||||
async def _publish_encryption_key(self, room_id: str, key: bytes):
|
||||
"""Publish bot's E2EE encryption key as io.element.call.encryption_keys state event."""
|
||||
key_b64 = base64.urlsafe_b64encode(key).decode().rstrip("=")
|
||||
content = {
|
||||
"call_id": "",
|
||||
"device_id": BOT_DEVICE_ID,
|
||||
"keys": [{"index": 0, "key": key_b64}],
|
||||
}
|
||||
state_key = f"{BOT_USER}:{BOT_DEVICE_ID}"
|
||||
try:
|
||||
await self.client.room_put_state(
|
||||
room_id, ENCRYPTION_KEYS_TYPE, content, state_key=state_key,
|
||||
)
|
||||
logger.info("Published E2EE key in %s", room_id)
|
||||
except Exception:
|
||||
logger.exception("Failed to publish E2EE key in %s", room_id)
|
||||
|
||||
async def _route_verification(self, room, event: UnknownEvent):
|
||||
"""Route in-room verification events from UnknownEvent."""
|
||||
source = event.source or {}
|
||||
|
||||
Reference in New Issue
Block a user