Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Olm crypto: no self-device key verification / scoped reset, silent identity-key clobber on store+device_id reuse, and permanent OTK-upload wedge ("already exists")

未关闭
#187 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
python

调研方向

Start with _share_keys in machine.py and key generation in account.py, then inspect the client API methods and crypto/store/asyncpg/store.py:88-92. Reproduce the self-device mismatch and already-exists upload behavior using the described Synapse setup before choosing the scope. Done means the selected recovery and upload paths are covered by tests and no longer silently clobber keys or remain permanently wedged.

由索引模型根据 Issue 内容生成。

描述

Description

OlmMachine has no startup verification that the server's stored device keys match the local account, no scoped recovery for a single-device identity-key desync, and its OTK upload path can wedge permanently when the server already holds one-time keys under the same key IDs. We hit all three at once during an incident on 2026-09-23 (Synapse homeserver, mautrix 0.21.1), and each one turned a recoverable state into a full re-registration.

Defect 1 — share_keys() silently clobbers server-side device keys when the local store is fresh but the device_id exists

Reproduction chain (verified from gateway logs + Synapse responses):

  1. Device X exists on the server with uploaded device keys (normal operation).
  2. A second client instance starts with the same device_id but a fresh/empty crypto store. OlmMachine.load() creates a new OlmAccount (new identity keys), account.shared = False.
  3. First share_keys() call (via handle_otk_count or explicit startup call) builds device_keys because not self.account.shared (machine.py:313-317) and uploads them. Synapse's POST /keys/upload replaces the stored device keys for device X with the new ones.
  4. The original client (which still holds the real Olm account for X) now sees server has different identity keys for device X and permanently refuses E2EE.

There is no server-side guard against this: Synapse only checks the signature, and the new keys are correctly self-signed. From the server's perspective the impostor's upload is indistinguishable from a legitimate re-upload. But OlmMachine could detect it: fetch the stored device keys for our own device_id first (query_keys with {mxid: []}), and if the server already has an ed25519 key that differs from account.identity_keys["ed25519"], refuse the silent clobber and surface a distinct error instead of uploading.

How the desync arises (our case): a credential leak started a duplicate client under the same user/device_id with an empty store. The same state also occurs when a crypto store is deleted/corrupted and the client restarts while the old device record survives server-side, or when a profile is copied to a second machine with the same device_id (store regenerated, device reused). None of these are exotic: "delete crypto.db and restart" is standard advice.

Defect 2 — one M_UNKNOWN: One time key … already exists in a batch wedges key sharing permanently

After the clobber, our fresh store's OTK ID counter started at AAAAAQ while the server still held the impostor's 55 one-time keys whose IDs overlapped that counter range. Every share_keys() then failed atomically:

mautrix.errors.request.MUnknown: One time key signed_curve25519:AAAAAQ already exists.
Old key: {"key":"wltvheOsyhxg…","signatures":{"@user:hs":{"ed25519:DEVICE_X":"…"}}};
new key: {'key': '45QmiSXhAQTs…', 'signatures': {'@user:hs': {'ed25519:DEVICE_X': '…'}}}

Code-level root cause in _share_keys (machine.py:302-332):

  • get_one_time_keys() regenerates the same key IDs from the account counter (account.py:96-98), so every retry re-emits the same colliding IDs.
  • resp = await self.client.upload_keys(...) raises → the function exits before self.account.shared = True, mark_keys_as_published() and put_account() (machine.py:327-331). Because mark_keys_as_published() never runs, libolm keeps returning the same unpublished keys forever.
  • There is no handling for the already exists case: no skip of colliding key IDs, no counter advance, no fallback to uploading keys one by one. The counter never advances past the server's existing IDs, so the batch can never succeed. In our case the loop ran 86 failures over 48 minutes until we deleted the device server-side and re-registered.

This is the Python-side twin of matrix-org/matrix-rust-sdk#6520 and the Synapse-side atomicity of matrix-org/synapse#7365. A transport-layer workaround is being shipped elsewhere (openclaw/openclaw#74529 rewrites the 400 to a synthetic 200 with empty counts, which makes the SDK mint fresh IDs on the next tick).

Suggested fixes (any one unwedges):

  • Catch M_UNKNOWN containing already exists in _share_keys, mark_keys_as_published() to burn the colliding batch, put_account(), and retry once with freshly generated IDs.
  • Or upload keys individually (or in chunks) so one collision fails the whole batch; Synapse rejects duplicates per-key.
Defect 3 — no client API to delete a device / purge its server-side keys, and no recovery API for a single-device desync

The client API surface (mautrix/client/api/) has create_device_msc4190, logout, logout_all — but no delete_devices. We had to drop to raw DELETE /_matrix/client/v3/devices/{id} with ad-hoc UIA (m.login.password auth_data), which the client API does not support either. matrix-nio carries delete_devices with UIA support as precedent (AsyncClient.delete_devices). When a device's server-side record is poisoned (stale OTKs under wrong keys, wedged key counts), "delete the device record and start clean" is the recovery path, and the SDK should own it.

Additionally, the crypto store already has a per-account delete() (crypto/store/asyncpg/store.py:88-92) that clears crypto_account + Olm sessions + outbound Megolm sessions for one account_id — but nothing exposes a guided "reset this device's E2EE state" flow (verify server keys → if mismatch, either restore the local account if we hold the original, or delete server device + local account + re-upload) with the UIA dance handled. Today every embedder hand-rolls this, and the failure modes are non-obvious (deleting the server device revokes all its access tokens; claiming a stale OTK whose private half is gone makes inbound sessions undecryptable).

Environment

  • mautrix 0.21.1 (gaps confirmed present on current master by source inspection of machine.py / account.py / client API, 2026-09-26)
  • Synapse homeserver, Matrix spec v1.12
  • python-olm 3.x bindings

Suggestion

A share_keys() pre-flight check against query_keys for our own device + a delete_devices client API with UIA would have turned a multi-hour outage with full re-registration into a clean, automatic recovery. Happy to work the PR(s) if the approach sounds reasonable — the pre-flight check and the already-exists unwedge are both small, and delete_devices can follow matrix-nio's implementation closely.

主要语言
Python
星标
251
派生
85
平均合并
1 小时 53 分钟
30 天内合并 PR
1

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

mautrix/python 的其他 Issue

查看 mautrix/python 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。