fix(matrix): bind and bound SAS requests

This commit is contained in:
Xubin Ren
2026-09-03 23:49:20 +08:00
parent 04aeec6c0f
commit 236185d4f5
2 changed files with 83 additions and 1 deletions
+22 -1
View File
@@ -80,6 +80,7 @@ _MSGTYPE_MAP = {"m.image": "image", "m.audio": "audio", "m.video": "video", "m.f
_SAS_METHOD = "m.sas.v1"
_SAS_REQUEST_MAX_AGE_MS = 10 * 60 * 1000
_SAS_REQUEST_MAX_FUTURE_MS = 5 * 60 * 1000
_SAS_REQUEST_MAX_PENDING = 256
MATRIX_MEDIA_EVENT_FILTER = (RoomMessageMedia, RoomEncryptedMedia)
MatrixMediaEvent: TypeAlias = RoomMessageMedia | RoomEncryptedMedia
@@ -759,6 +760,16 @@ class MatrixChannel(BaseChannel):
if request.timestamp_ms >= oldest_allowed
}
def _remember_sas_verification_request(
self,
transaction_id: str,
request: _SasVerificationRequest,
) -> None:
self._sas_verification_requests[transaction_id] = request
while len(self._sas_verification_requests) > _SAS_REQUEST_MAX_PENDING:
oldest_transaction_id = next(iter(self._sas_verification_requests))
self._sas_verification_requests.pop(oldest_transaction_id)
async def _send_sas_control_message(
self,
*,
@@ -849,7 +860,7 @@ class MatrixChannel(BaseChannel):
},
)
if sent:
self._sas_verification_requests[transaction_id] = request
self._remember_sas_verification_request(transaction_id, request)
return
if event_type == "m.key.verification.done":
@@ -882,6 +893,16 @@ class MatrixChannel(BaseChannel):
return
if isinstance(event, KeyVerificationStart):
request = self._sas_verification_requests.get(transaction_id)
from_device = str(getattr(event, "from_device", "") or "")
if request is not None and (
request.sender != sender or request.device_id != from_device
):
self.logger.warning(
"Ignoring Matrix SAS start for transaction {} from unexpected device",
transaction_id,
)
return
if "emoji" not in (getattr(event, "short_authentication_string", None) or []):
self.logger.info(
"Ignoring Matrix SAS verification from {} without emoji support",
@@ -241,10 +241,12 @@ class _FakeKeyVerificationStart:
*,
sender: str = "@alice:matrix.org",
transaction_id: str = "tx1",
from_device: str = "ALICEDEVICE",
short_authentication_string: list[str] | None = None,
) -> None:
self.sender = sender
self.transaction_id = transaction_id
self.from_device = from_device
self.short_authentication_string = short_authentication_string or ["emoji"]
@@ -477,6 +479,65 @@ async def test_sas_verification_request_sends_ready_to_allowed_device(monkeypatc
assert channel._sas_verification_requests["tx1"].device_id == "ALICEDEVICE"
@pytest.mark.asyncio
async def test_sas_verification_requests_are_bounded(monkeypatch) -> None:
monkeypatch.setattr(matrix_module.time, "time", lambda: 1_000.0)
channel = MatrixChannel(
_make_config(
allow_from=["@alice:matrix.org"],
e2ee_enabled=True,
sas_verification=True,
),
MessageBus(),
)
client = _FakeAsyncClient("", "", "", None)
client.device_id = "BOTDEVICE"
channel.client = client
for index in range(matrix_module._SAS_REQUEST_MAX_PENDING + 1):
await channel._handle_key_verification_event(_unknown_verification_event(
"m.key.verification.request",
transaction_id=f"tx-{index}",
from_device="ALICEDEVICE",
methods=["m.sas.v1"],
timestamp=1_000_000,
))
assert len(channel._sas_verification_requests) == matrix_module._SAS_REQUEST_MAX_PENDING
assert "tx-0" not in channel._sas_verification_requests
assert f"tx-{matrix_module._SAS_REQUEST_MAX_PENDING}" in channel._sas_verification_requests
@pytest.mark.asyncio
async def test_sas_verification_start_must_match_requested_device(monkeypatch) -> None:
_patch_key_verification_events(monkeypatch)
monkeypatch.setattr(matrix_module.time, "time", lambda: 1_000.0)
channel = MatrixChannel(
_make_config(
allow_from=["@alice:matrix.org"],
e2ee_enabled=True,
sas_verification=True,
),
MessageBus(),
)
client = _FakeAsyncClient("", "", "", None)
client.device_id = "BOTDEVICE"
channel.client = client
await channel._handle_key_verification_event(_unknown_verification_event(
"m.key.verification.request",
from_device="ALICEDEVICE",
methods=["m.sas.v1"],
timestamp=1_000_000,
))
await channel._handle_key_verification_event(
_FakeKeyVerificationStart(from_device="OTHERDEVICE")
)
assert client.accept_key_verification_calls == []
assert channel._sas_verification_requests["tx1"].device_id == "ALICEDEVICE"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("sender", "methods", "timestamp"),