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")
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- python
- Área
- api, cryptography, security
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- Python
- Estrellas
- 251
- Forks
- 85
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de mautrix/python
-
SQLite transaction stays open when its task is cancelledPosiblemente ocupada @jrderuiter la tomó hace 1 día. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 30/100
-
using inspect.unwrap on `maurix.types.Obj` places the object into a state where serializing raises a RecursionErrorPosiblemente ocupada @Deln0r la tomó hace 153 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 45/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 30/100
Todos los issues de mautrix/python
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
aws-samples/sample-per-model-bedrock#191 ·
Los mantenedores suelen responder en 1 día
-
Docs viewer: inverted grey colormap, Phantom CT window -20 to 35 HU, smoothing up to 40 mmPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertoarea: docs enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
PyTomography/PyTomography#331 ·
Los mantenedores suelen responder en 1 día
-
effort: low prio: low ready
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
digitalfabrik/integreat-cms#4616 ·
Los mantenedores suelen responder en 5 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
FuRongJun-1999/dsh-memory#226 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug difficulty:2 good first issue help wanted
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
MakazhanAlpamys/Soup#1865 ·
Los mantenedores suelen responder en 1 día