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")
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
- Ambito
- api, cryptography, security
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):
- Device
Xexists on the server with uploaded device keys (normal operation). - A second client instance starts with the same device_id but a fresh/empty crypto store.
OlmMachine.load()creates a newOlmAccount(new identity keys),account.shared = False. - First
share_keys()call (viahandle_otk_countor explicit startup call) buildsdevice_keysbecausenot self.account.shared(machine.py:313-317) and uploads them. Synapse'sPOST /keys/uploadreplaces the stored device keys for device X with the new ones. - The original client (which still holds the real Olm account for X) now sees
server has different identity keys for device Xand 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 beforeself.account.shared = True,mark_keys_as_published()andput_account()(machine.py:327-331). Becausemark_keys_as_published()never runs, libolm keeps returning the same unpublished keys forever.- There is no handling for the
already existscase: 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_UNKNOWNcontainingalready existsin_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
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di mautrix/python
-
SQLite transaction stays open when its task is cancelledForse già presa @jrderuiter l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
-
using inspect.unwrap on `maurix.types.Obj` places the object into a state where serializing raises a RecursionErrorForse già presa @Deln0r l’ha presa 152 giorni fa. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 45/100
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 30/100
Tutte le issue di mautrix/python
Issue simili
-
SSM GetParameter throttling during concurrent workflow launchesForse già presa @alexiswl l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100
-
[Bug] Dev image build fails: `npm install` in the root Dockerfile runs out of memory (exit 134)Forse già presa @andest01 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
enhancement good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 79/100
pesu-dev/discord_bot#97 ·
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 88/100
gradio-app/gradio#13964 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 93/100
BSData/horus-heresy-3rd-edition#3232 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno