Encryption Reset (resetEncryption())#
CryptoApi.resetEncryption() is the SDK-level operation for wiping and regenerating a user's cryptographic identity. It is exposed in Element Web via the "Reset your cryptographic identity" dialog and is triggered in several flows: explicit reset from Settings, sync failure recovery, forgotten recovery key, and unverified devices with no alternative verification path .
Operation Sequence#
RustCrypto.resetEncryption() runs these steps sequentially — with no transactional rollback:
- Fire-and-forget delete of the dehydrated device .
- Delete all key backup versions from the server via
backupManager.deleteAllKeyBackupVersions(). - Wipe secret storage via
deleteSecretStorage(). - Upload new cross-signing keys via
bootstrapCrossSigning({ setupNewCrossSigning: true, ... })— the step that requires user authentication . - Create a new key backup via
resetKeyBackup().
deleteSecretStorage() Detail#
deleteSecretStorage() calls secretStorage.store(name, null) for four keys (m.cross_signing.master, m.cross_signing.self_signing, m.cross_signing.user_signing, m.megolm_backup.v1) then calls secretStorage.setDefaultKeyId(null). Passing null to store() writes an empty object {} as account data for each key, rather than deleting the event.
Known Bugs and Failure Modes#
1. Infinite Hang When Secret Storage Is Already Wiped (Issue #5474)#
Status: Open as of 2026-08-09. Affects v42.1.0 and current develop.
ServerSideSecretStorageImpl.setDefaultKeyId() returns a Promise that resolves only when a ClientEvent.AccountData echo for m.secret_storage.default_key arrives over /sync. The inner setAccountData() call's fulfillment is ignored; only the .catch() path is wired .
Since PR #4695, MatrixClient.setAccountData() short-circuits with no HTTP request and no event emission when the incoming content deep-equals the locally cached content . When m.secret_storage.default_key already contains {} (the state a previous interrupted reset leaves behind), setDefaultKeyId(null) writes {} → content matches → no request → no echo → the Promise never settles → resetEncryption() hangs indefinitely.
A second variant: if the local store lacks the event but the server already has {}, the PUT fires but the server returns no /sync update for a no-op, so setAccountData() itself hangs at await updatedResolvers.promise .
The same dedup-skip affects all four store(name, null) calls in deleteSecretStorage() under the same conditions.
2. Non-Atomic Ordering: Destructive Writes Before Authentication#
Secret storage is wiped (step 3) before the cross-signing key upload (step 4), which requires user authentication. If the user cancels auth, if the auth service return flow fails, or if the app is closed during step 4, the account is left with:
- All secret storage account data set to
{} - No key backup
- Old (now orphaned) cross-signing keys still on the server
Every subsequent resetEncryption() call on this account then triggers the infinite hang described above, permanently bricking the reset flow until a manual workaround is applied.
3. No Error Propagation from UI#
ResetIdentityBody calls resetEncryption() inside a click handler without a try/catch. If the promise rejects (or hangs), the component stays in the inProgress spinner state indefinitely with no user-visible error. The dialog warns users not to close the window — which, in a deadlock, they must do to escape.
Workaround for Affected Users#
Write non-empty content into m.secret_storage.default_key from the browser console to break the dedup-skip, then retry the reset :
client.setAccountData("m.secret_storage.default_key", { key: "dummy" })
Suggested Fix (from Issue #5474)#
setDefaultKeyId() should resolve on fulfillment of the inner setAccountData() call rather than relying on the account-data listener — since setAccountData() already waits for the remote echo internally. Alternatively, setDefaultKeyId(null) should short-circuit when the current content already equals {}.
Key Source Locations#
| File | Purpose |
|---|---|
src/rust-crypto/rust-crypto.ts L1435–1475 | resetEncryption() and deleteSecretStorage() |
src/secret-storage.ts L373–402 | setDefaultKeyId() – the deadlock source |
src/secret-storage.ts L513–518 | store(name, null) → writes {} |
src/client.ts L2206–2250 | setAccountData() dedup short-circuit |
src/rust-crypto/CrossSigningIdentity.ts | Cross-signing key bootstrap and export to secret storage |
apps/web/…/ResetIdentityBody.tsx | Element Web UI; calls resetEncryption() with no error handling |