diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts index fcf79616ffb..c91915d0974 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts @@ -159,6 +159,55 @@ describe("MatrixCryptoBootstrapper", () => { ); }); + it("recreates secret storage and retries cross-signing when explicit bootstrap hits a stale server key", async () => { + const deps = createBootstrapperDeps(); + const bootstrapCrossSigning = vi + .fn<() => Promise>() + .mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey")) + .mockResolvedValueOnce(undefined); + const crypto = createCryptoApi({ + bootstrapCrossSigning, + isCrossSigningReady: vi.fn(async () => true), + userHasCrossSigningKeys: vi.fn(async () => true), + getDeviceVerificationStatus: vi.fn(async () => ({ + isVerified: () => true, + localVerified: true, + crossSigningVerified: true, + signedByOwner: true, + })), + }); + const bootstrapper = new MatrixCryptoBootstrapper( + deps as unknown as MatrixCryptoBootstrapperDeps, + ); + + await bootstrapper.bootstrap(crypto, { + strict: true, + allowSecretStorageRecreateWithoutRecoveryKey: true, + allowAutomaticCrossSigningReset: false, + }); + + expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith( + crypto, + { + allowSecretStorageRecreateWithoutRecoveryKey: true, + forceNewSecretStorage: true, + }, + ); + expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + }); + it("fails in strict mode when cross-signing keys are still unpublished", async () => { const deps = createBootstrapperDeps(); const crypto = createCryptoApi({ diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts index 9c3d2655616..b7d2f6a422e 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts @@ -58,6 +58,8 @@ export class MatrixCryptoBootstrapper { const crossSigning = await this.bootstrapCrossSigning(crypto, { forceResetCrossSigning: options.forceResetCrossSigning === true, allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false, + allowSecretStorageRecreateWithoutRecoveryKey: + options.allowSecretStorageRecreateWithoutRecoveryKey === true, strict, }); await this.bootstrapSecretStorage(crypto, { @@ -105,6 +107,7 @@ export class MatrixCryptoBootstrapper { options: { forceResetCrossSigning: boolean; allowAutomaticCrossSigningReset: boolean; + allowSecretStorageRecreateWithoutRecoveryKey: boolean; strict: boolean; }, ): Promise<{ ready: boolean; published: boolean }> { @@ -171,30 +174,47 @@ export class MatrixCryptoBootstrapper { authUploadDeviceSigningKeys, }); } catch (err) { - if (!options.allowAutomaticCrossSigningReset) { + const shouldRepairSecretStorage = + options.allowSecretStorageRecreateWithoutRecoveryKey && + err instanceof Error && + err.message.includes("getSecretStorageKey callback returned falsey"); + if (shouldRepairSecretStorage) { + LogService.warn( + "MatrixClientLite", + "Cross-signing bootstrap could not access secret storage; recreating secret storage during explicit bootstrap and retrying.", + ); + await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, { + allowSecretStorageRecreateWithoutRecoveryKey: true, + forceNewSecretStorage: true, + }); + await crypto.bootstrapCrossSigning({ + authUploadDeviceSigningKeys, + }); + } else if (!options.allowAutomaticCrossSigningReset) { LogService.warn( "MatrixClientLite", "Initial cross-signing bootstrap failed and automatic reset is disabled:", err, ); return { ready: false, published: false }; - } - LogService.warn( - "MatrixClientLite", - "Initial cross-signing bootstrap failed, trying reset:", - err, - ); - try { - await crypto.bootstrapCrossSigning({ - setupNewCrossSigning: true, - authUploadDeviceSigningKeys, - }); - } catch (resetErr) { - LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr); - if (options.strict) { - throw resetErr instanceof Error ? resetErr : new Error(String(resetErr)); + } else { + LogService.warn( + "MatrixClientLite", + "Initial cross-signing bootstrap failed, trying reset:", + err, + ); + try { + await crypto.bootstrapCrossSigning({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys, + }); + } catch (resetErr) { + LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr); + if (options.strict) { + throw resetErr instanceof Error ? resetErr : new Error(String(resetErr)); + } + return { ready: false, published: false }; } - return { ready: false, published: false }; } } diff --git a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts index 54dbdce4f55..580296ffab1 100644 --- a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts +++ b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts @@ -131,6 +131,7 @@ export class MatrixRecoveryKeyStore { options: { setupNewKeyBackup?: boolean; allowSecretStorageRecreateWithoutRecoveryKey?: boolean; + forceNewSecretStorage?: boolean; } = {}, ): Promise { let status: MatrixSecretStorageStatus | null = null; @@ -185,6 +186,7 @@ export class MatrixRecoveryKeyStore { }; const shouldRecreateSecretStorage = + options.forceNewSecretStorage === true || !hasDefaultSecretStorageKey || (!recoveryKey && status?.ready === false) || hasKnownInvalidSecrets;