E2EE Key Management#
Overview#
E2EE key management in matrix-js-sdk and Element covers three interconnected concerns:
- Key backup — uploading Megolm session keys to the server so they can be restored if local storage is lost, and keeping backup state coherent when keys are created, cached, or reset.
- Encrypted history sharing (MSC4268) — distributing room key bundles to newly invited users via
m.room_key_bundleto-device messages, including handling late arrivals and enforcing download ordering. - Recovery flows — detecting and repairing "key storage out of sync" states where the backup decryption key is absent from local cache and/or Secret Storage (4S).
The primary implementation lives in src/rust-crypto/backup.ts (RustBackupManager) and src/rust-crypto/rust-crypto.ts in matrix-js-sdk, with UI-side recovery orchestration in DeviceListener.ts, KeyStoragePanelViewModel.ts, and resetKeyBackup.ts in element-web.
Key Backup: Consistency and Cache Coherency#
The RustBackupManager in src/rust-crypto/backup.ts owns the backup lifecycle:
setupKeyBackup()creates a new backup: generates a random private key, deletes any existing backup, uploads the new one to the server.saveBackupDecryptionKey()persists the decryption key locally and emitsCryptoEvent.KeyBackupDecryptionKeyCached.resetKeyBackup()andcheckKeyBackupAndEnable()are wired throughsrc/rust-crypto/rust-crypto.ts.
Event Ordering Bug (PR #5281)#
CryptoEvent.KeyBackupDecryptionKeyCached was previously emitted before local backup state (activeVersion, eventVersion, serverVersion) was updated. Listeners querying backup info immediately after the event saw "Unsupported algorithm undefined." The fix moves backup activation entirely into setupKeyBackup() so the cached-key event only fires after state is fully coherent.
Enabling Backup Over an Untrusted Existing Backup (PR #30707)#
When a user enables key backup, KeyStoragePanelViewModel.setEnabled checks the existing server-side backup's trust state:
- If
trustInfo.trustedortrustInfo.matchesDecryptionKey→ reuse the existing backup. - If neither → call
resetKeyBackupAndWait()to replace it with a new one.
Without this check, the UI silently failed when the existing backup was untrusted.
Key Storage Out-of-Sync Recovery#
"Key storage out of sync" arises when the backup decryption key is missing from the local cache, Secret Storage (4S), or both — often after a device migration or partial backup setup.
Detection (element-web DeviceListener)#
Two methods in DeviceListener.ts classify what needs resetting:
keyStorageOutOfSyncNeedsBackupReset(forgotRecovery)— returnstruewhen the key backup should exist but its decryption key is inaccessible. TheforgotRecoveryflag controls whether to check Secret Storage (if the user remembers their recovery key) or only the local cache (if they've forgotten it).keyStorageOutOfSyncNeedsCrossSigningReset(forgotRecovery)— returnstruewhen cross-signing keys are missing from both local cache and Secret Storage.
Suppressing Intermediate Toasts#
whilePaused(fn) temporarily pauses the DeviceListener while a multi-step fix runs, preventing spurious warning toasts from firing between intermediate states (e.g., while a backup is being torn down before a new one is created). KeyStoragePanelViewModel uses this wrapper around enable/disable sequences.
Reset Execution#
resetKeyBackupAndWait(crypto) in src/utils/crypto/resetKeyBackup.ts calls crypto.resetKeyBackup() then waits for checkKeyBackupAndEnable() to confirm the new backup is active before returning — ensuring callers see a consistent state.
Room Key Bundles and Encrypted History Sharing (MSC4268)#
MSC4268 allows clients to share encrypted room history with newly invited users via m.room_key_bundle to-device messages. Three PRs harden this flow:
Late Bundle Arrival (PR #5080)#
When a user accepts an encrypted room invite, the m.room_key_bundle message may arrive after the join is processed. The fix :
maybeAcceptKeyBundle()returns a boolean indicating whether the bundle was immediately imported.- If not,
markRoomAsPendingKeyBundle()records the room as waiting. - When the bundle to-device message subsequently arrives for a pending room, it is automatically imported.
Download Keys Before Building Bundles (PR #5171)#
When constructing outbound key bundles for history sharing, keys held only in the server-side backup are not yet available locally. The fix adds a backup download step before bundle construction, so the bundle includes all known keys rather than only those already in local storage.
History Visibility Guard (PR #5216)#
History should not be shared when the room has restricted visibility. In MatrixClient.invite(), history sharing now only proceeds when the room's historyVisibility is shared or world_readable — it is skipped for invited or joined settings, with a debug log emitted when skipping.
Key Source Files#
| File | Repo | Purpose |
|---|---|---|
src/rust-crypto/backup.ts | matrix-js-sdk | RustBackupManager: backup creation, key caching, KeyBackupDecryptionKeyCached event |
src/rust-crypto/rust-crypto.ts | matrix-js-sdk | resetKeyBackup, checkKeyBackupAndEnable, getKeyBackupInfo |
src/client.ts | matrix-js-sdk | joinRoom, MatrixClient.invite() with history visibility guard |
apps/web/src/device-listener/DeviceListener.ts | element-web | Out-of-sync detection, whilePaused, backup/cross-signing reset triggers |
apps/web/src/utils/crypto/resetKeyBackup.ts | element-web | resetKeyBackupAndWait — atomic reset + wait for activation |
apps/web/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts | element-web | UI enable flow: trust checks, resetKeyBackupAndWait call |