Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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")

Aperta
#187 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
python

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Lingua principale
Python
Stelle
251
Fork
85
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di mautrix/python

Tutte le issue di mautrix/python

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.