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:
|
except Exception:
|
||||||
logger.exception("Failed to update call.member state with E2EE key in %s", room_id)
|
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):
|
async def _route_verification(self, room, event: UnknownEvent):
|
||||||
"""Route in-room verification events from UnknownEvent."""
|
"""Route in-room verification events from UnknownEvent."""
|
||||||
source = event.source or {}
|
source = event.source or {}
|
||||||
|
|||||||
Reference in New Issue
Block a user