fix: send bot E2EE key via Olm-encrypted to-device message
Element X only reads encryption keys from encrypted to-device messages, not room events or call.member state. Bot now sends its key via Olm-encrypted to-device to all call participants, matching Element Call's encryptAndSendToDevice behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
99
bot.py
99
bot.py
@@ -3705,6 +3705,105 @@ class Bot:
|
||||
except Exception:
|
||||
logger.exception("Failed to update call.member state with E2EE key in %s", room_id)
|
||||
|
||||
# To-device: send Olm-encrypted key to all other call participants
|
||||
# Element X reads keys from encrypted to-device messages only
|
||||
await self._send_e2ee_key_to_device(room_id, key_b64)
|
||||
|
||||
async def _send_e2ee_key_to_device(self, room_id: str, key_b64: str):
|
||||
"""Send bot's E2EE key as Olm-encrypted to-device message to call participants."""
|
||||
try:
|
||||
import httpx
|
||||
import uuid as _uuid
|
||||
token = self.client.access_token
|
||||
|
||||
# Find other call participants from call.member state events
|
||||
state_url = f"{HOMESERVER}/_matrix/client/v3/rooms/{room_id}/state"
|
||||
async with httpx.AsyncClient(timeout=10.0) as http:
|
||||
resp = await http.get(state_url, headers={"Authorization": f"Bearer {token}"})
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
targets = []
|
||||
for evt in resp.json():
|
||||
if evt.get("type") != CALL_MEMBER_TYPE:
|
||||
continue
|
||||
sender = evt.get("sender", "")
|
||||
if sender == BOT_USER:
|
||||
continue
|
||||
content_evt = evt.get("content", {})
|
||||
if not content_evt:
|
||||
continue
|
||||
device = content_evt.get("device_id", "")
|
||||
if device:
|
||||
targets.append((sender, device))
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
# Payload matching Element Call's to-device format
|
||||
payload_content = {
|
||||
"keys": {"index": 0, "key": key_b64},
|
||||
"room_id": room_id,
|
||||
"member": {"claimed_device_id": BOT_DEVICE_ID},
|
||||
}
|
||||
|
||||
# Olm-encrypt for each target device
|
||||
messages = {}
|
||||
olm = self.client.olm
|
||||
if not olm:
|
||||
logger.warning("No Olm instance — cannot send encrypted to-device key")
|
||||
return
|
||||
|
||||
for user_id, device_id in targets:
|
||||
# Get the device from the device store
|
||||
device = olm.device_store.get(user_id, device_id) if hasattr(olm, 'device_store') else None
|
||||
if not device:
|
||||
# Need to fetch device keys first
|
||||
logger.info("Fetching device keys for %s/%s", user_id, device_id)
|
||||
await self.client.keys_query()
|
||||
device = olm.device_store.get(user_id, device_id) if hasattr(olm, 'device_store') else None
|
||||
|
||||
if not device:
|
||||
logger.warning("Device %s/%s not found in store, sending unencrypted", user_id, device_id)
|
||||
messages.setdefault(user_id, {})[device_id] = payload_content
|
||||
continue
|
||||
|
||||
# Find or create Olm session
|
||||
session = olm.session_store.get(device.curve25519)
|
||||
if not session:
|
||||
# Claim one-time key and create session
|
||||
logger.info("Claiming one-time key for %s/%s", user_id, device_id)
|
||||
claim_resp = await self.client.keys_claim(user_id, device_id)
|
||||
session = olm.session_store.get(device.curve25519)
|
||||
|
||||
if session:
|
||||
encrypted = olm._olm_encrypt(
|
||||
session, device, ENCRYPTION_KEYS_TYPE, payload_content
|
||||
)
|
||||
messages.setdefault(user_id, {})[device_id] = encrypted
|
||||
logger.info("Olm-encrypted E2EE key for %s/%s", user_id, device_id)
|
||||
else:
|
||||
logger.warning("No Olm session for %s/%s, sending unencrypted", user_id, device_id)
|
||||
messages.setdefault(user_id, {})[device_id] = payload_content
|
||||
|
||||
# Send via PUT /sendToDevice
|
||||
event_type = "m.room.encrypted" if any(
|
||||
"ciphertext" in v.get(list(v.keys())[0], {}) if v else False
|
||||
for v in messages.values()
|
||||
) else ENCRYPTION_KEYS_TYPE
|
||||
txn_id = str(_uuid.uuid4())
|
||||
send_url = f"{HOMESERVER}/_matrix/client/v3/sendToDevice/{event_type}/{txn_id}"
|
||||
async with httpx.AsyncClient(timeout=10.0) as http:
|
||||
resp = await http.put(
|
||||
send_url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"messages": messages},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
logger.info("Sent E2EE key via to-device (%s) to %d target(s): %s",
|
||||
event_type, len(targets), targets)
|
||||
except Exception:
|
||||
logger.exception("Failed to send E2EE key via to-device")
|
||||
|
||||
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