openclaw - 💡(How to fix) Fix BUG: --force-reset-cross-signing fires bootstrapCrossSigning TWICE, destroys E2EE state, leaves recovery-key.json pointing at empty secret storage [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78396Fetched 2026-05-07 03:37:25
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
3
Author
Timeline (top)
commented ×1

Error Message

Inside bootstrapCrossSigning, the first resetCrossSigning() hits the stale/broken secret storage and throws an error that satisfies isRepairableSecretStorageAccessError. Because allowSecretStorageRecreateWithoutRecoveryKey is true, the repair block fires: The second resetCrossSigning() either fails outright (UIA rate-limit, state race on the newly created secret storage) or uploads keys the SDK then can't persist locally. With strict: true the error propagates up to bootstrapOwnDeviceVerification, which discards the staged recovery key. The net state after this whole circus: Any user who runs verify bootstrap --force-reset-cross-signing against an account with stale/mismatched secret storage (the exact situation you're supposed to use --force-reset-cross-signing to fix) gets their E2EE state destroyed silently. The command exits without a clear fatal error, recovery-key.json on disk looks plausible, and the user won't discover the damage until they try to verify and find everything is broken.

Root Cause

Running openclaw matrix verify bootstrap --force-reset-cross-signing triggers two full cross-signing resets in one invocation. The second one routinely fails. End result: private_identity in the crypto store has all-null keys, the recovery key written to disk no longer matches anything on the server, and the room-key backup is gone. The user is left with a completely bricked Matrix E2EE state that verify device --recovery-key-stdin cannot recover because there is nothing coherent left to recover.

Fix Action

Fix

Two-line change in MatrixCryptoBootstrapper.bootstrap:

1. Always run bootstrapSecretStorage upfront — when force-resetting, run it non-strict with allowSecretStorageRecreateWithoutRecoveryKey: true so a fresh, working secret storage exists before the cross-signing reset touches it.

2. Pass allowSecretStorageRecreateWithoutRecoveryKey: false into the first bootstrapCrossSigning call when deferSecretStorageBootstrapUntilAfterCrossSigning is true, so the repair block's second resetCrossSigning() can never fire.

-		if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(crypto, {
-			strict,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true
-		});
-		let crossSigning = await this.bootstrapCrossSigning(crypto, {
-			forceResetCrossSigning: options.forceResetCrossSigning === true,
-			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true,
-			strict
-		});
+		await this.bootstrapSecretStorage(crypto, {
+			strict: deferSecretStorageBootstrapUntilAfterCrossSigning ? false : strict,
+			allowSecretStorageRecreateWithoutRecoveryKey: deferSecretStorageBootstrapUntilAfterCrossSigning ? true : options.allowSecretStorageRecreateWithoutRecoveryKey === true
+		});
+		let crossSigning = await this.bootstrapCrossSigning(crypto, {
+			forceResetCrossSigning: options.forceResetCrossSigning === true,
+			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
+			allowSecretStorageRecreateWithoutRecoveryKey: !deferSecretStorageBootstrapUntilAfterCrossSigning && options.allowSecretStorageRecreateWithoutRecoveryKey === true,
+			strict
+		});

Patch file: fix-double-reset-cross-signing.patch

Code Example

if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(...)

---

// line 2613-2630
if (options.allowSecretStorageRecreateWithoutRecoveryKey && isRepairableSecretStorageAccessError(err)) {
    await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
        allowSecretStorageRecreateWithoutRecoveryKey: true,
        forceNewSecretStorage: true          // writes recovery-key.json with R1
    });
    await resetCrossSigning();              // SECOND RESET — this is the bug
    await this.trustFreshOwnIdentity(crypto);
}

---

# Rotate recovery keys (simulate the failure condition):
openclaw matrix verify bootstrap --force-reset-cross-signing

# Now watch it explode:
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin
# Recovery key accepted: no
# Backup usable: no
# Device verified by owner: no
# Backup: missing on server

---

-		if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(crypto, {
-			strict,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true
-		});
-		let crossSigning = await this.bootstrapCrossSigning(crypto, {
-			forceResetCrossSigning: options.forceResetCrossSigning === true,
-			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true,
-			strict
-		});
+		await this.bootstrapSecretStorage(crypto, {
+			strict: deferSecretStorageBootstrapUntilAfterCrossSigning ? false : strict,
+			allowSecretStorageRecreateWithoutRecoveryKey: deferSecretStorageBootstrapUntilAfterCrossSigning ? true : options.allowSecretStorageRecreateWithoutRecoveryKey === true
+		});
+		let crossSigning = await this.bootstrapCrossSigning(crypto, {
+			forceResetCrossSigning: options.forceResetCrossSigning === true,
+			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
+			allowSecretStorageRecreateWithoutRecoveryKey: !deferSecretStorageBootstrapUntilAfterCrossSigning && options.allowSecretStorageRecreateWithoutRecoveryKey === true,
+			strict
+		});
RAW_BUFFERClick to expand / collapse

BUG: --force-reset-cross-signing fires bootstrapCrossSigning TWICE, destroys state, leaves user fucked

Version: 2026.5.4 (325df3e)
Component: MatrixCryptoBootstrapper.bootstrap / bootstrapCrossSigning
File: dist/crypto-runtime-Gs7hsrok.js

What happens

Running openclaw matrix verify bootstrap --force-reset-cross-signing triggers two full cross-signing resets in one invocation. The second one routinely fails. End result: private_identity in the crypto store has all-null keys, the recovery key written to disk no longer matches anything on the server, and the room-key backup is gone. The user is left with a completely bricked Matrix E2EE state that verify device --recovery-key-stdin cannot recover because there is nothing coherent left to recover.

Root cause — read this before you close it as "works as designed"

MatrixCryptoBootstrapper.bootstrap (crypto-runtime-Gs7hsrok.js:2496) sets deferSecretStorageBootstrapUntilAfterCrossSigning = true when forceResetCrossSigning is set, which skips the initial bootstrapSecretStorage call (line 2500):

if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(...)

Then it calls bootstrapCrossSigning with allowSecretStorageRecreateWithoutRecoveryKey: true (line 2507, inherited from createMatrixExplicitBootstrapOptions which hardcodes this to true).

Inside bootstrapCrossSigning, the first resetCrossSigning() hits the stale/broken secret storage and throws an error that satisfies isRepairableSecretStorageAccessError. Because allowSecretStorageRecreateWithoutRecoveryKey is true, the repair block fires:

// line 2613-2630
if (options.allowSecretStorageRecreateWithoutRecoveryKey && isRepairableSecretStorageAccessError(err)) {
    await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
        allowSecretStorageRecreateWithoutRecoveryKey: true,
        forceNewSecretStorage: true          // writes recovery-key.json with R1
    });
    await resetCrossSigning();              // SECOND RESET — this is the bug
    await this.trustFreshOwnIdentity(crypto);
}

The second resetCrossSigning() either fails outright (UIA rate-limit, state race on the newly created secret storage) or uploads keys the SDK then can't persist locally. With strict: true the error propagates up to bootstrapOwnDeviceVerification, which discards the staged recovery key. The net state after this whole circus:

  • private_identity in crypto IDB store: all keys null
  • recovery-key.json on disk: has R1 (written before the second reset failed)
  • Server secret storage: has key ID for R1 but contains no cross-signing private keys (second reset failed before storing them)
  • Room-key backup: missing from server

Running verify device --recovery-key-stdin with either the old key OR R1 will fail. The old key doesn't match the server. R1 matches the server key ID but there's nothing useful in that secret storage. Backup usable: no, Backup: missing on server. Device is permanently unverified until a full wipe and re-bootstrap.

Reproduction

# Rotate recovery keys (simulate the failure condition):
openclaw matrix verify bootstrap --force-reset-cross-signing

# Now watch it explode:
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin
# Recovery key accepted: no
# Backup usable: no
# Device verified by owner: no
# Backup: missing on server

Fix

Two-line change in MatrixCryptoBootstrapper.bootstrap:

1. Always run bootstrapSecretStorage upfront — when force-resetting, run it non-strict with allowSecretStorageRecreateWithoutRecoveryKey: true so a fresh, working secret storage exists before the cross-signing reset touches it.

2. Pass allowSecretStorageRecreateWithoutRecoveryKey: false into the first bootstrapCrossSigning call when deferSecretStorageBootstrapUntilAfterCrossSigning is true, so the repair block's second resetCrossSigning() can never fire.

-		if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(crypto, {
-			strict,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true
-		});
-		let crossSigning = await this.bootstrapCrossSigning(crypto, {
-			forceResetCrossSigning: options.forceResetCrossSigning === true,
-			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
-			allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true,
-			strict
-		});
+		await this.bootstrapSecretStorage(crypto, {
+			strict: deferSecretStorageBootstrapUntilAfterCrossSigning ? false : strict,
+			allowSecretStorageRecreateWithoutRecoveryKey: deferSecretStorageBootstrapUntilAfterCrossSigning ? true : options.allowSecretStorageRecreateWithoutRecoveryKey === true
+		});
+		let crossSigning = await this.bootstrapCrossSigning(crypto, {
+			forceResetCrossSigning: options.forceResetCrossSigning === true,
+			allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
+			allowSecretStorageRecreateWithoutRecoveryKey: !deferSecretStorageBootstrapUntilAfterCrossSigning && options.allowSecretStorageRecreateWithoutRecoveryKey === true,
+			strict
+		});

Patch file: fix-double-reset-cross-signing.patch

Impact

Any user who runs verify bootstrap --force-reset-cross-signing against an account with stale/mismatched secret storage (the exact situation you're supposed to use --force-reset-cross-signing to fix) gets their E2EE state destroyed silently. The command exits without a clear fatal error, recovery-key.json on disk looks plausible, and the user won't discover the damage until they try to verify and find everything is broken.

This is a data-destroying bug in the one command that is supposed to rescue broken state.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix BUG: --force-reset-cross-signing fires bootstrapCrossSigning TWICE, destroys E2EE state, leaves recovery-key.json pointing at empty secret storage [1 comments, 2 participants]