From 263507da3db072ff0e5cae2a4b1f3ad57ceb285d Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 15 Apr 2026 11:32:34 -0400 Subject: [PATCH] fix(matrix): defer secret storage during forced repair --- .../src/matrix/sdk/crypto-bootstrap.test.ts | 43 +++++++++++++++++++ .../matrix/src/matrix/sdk/crypto-bootstrap.ts | 17 +++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts index 07ee889f6fd..a30a1405e0f 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts @@ -277,6 +277,49 @@ describe("MatrixCryptoBootstrapper", () => { expectSecretStorageRepairRetry(deps, crypto, bootstrapCrossSigning); }); + it("does not mutate secret storage before forced repair fails on password UIA without a password", async () => { + const deps = createBootstrapperDeps(); + deps.getPassword = vi.fn(() => undefined); + const bootstrapCrossSigning = vi.fn< + ({ + authUploadDeviceSigningKeys, + }: { + authUploadDeviceSigningKeys?: ( + makeRequest: (authData: Record | null) => Promise, + ) => Promise; + }) => Promise + >(async ({ authUploadDeviceSigningKeys }) => { + await authUploadDeviceSigningKeys?.(async (authData) => { + if (authData === null) { + throw new Error("need auth"); + } + if (authData.type === "m.login.dummy") { + throw new Error("dummy rejected"); + } + return undefined; + }); + }); + const crypto = createCryptoApi({ + bootstrapCrossSigning, + getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()), + }); + const bootstrapper = new MatrixCryptoBootstrapper( + deps as unknown as MatrixCryptoBootstrapperDeps, + ); + + await expect( + bootstrapper.bootstrap(crypto, { + strict: true, + forceResetCrossSigning: true, + allowSecretStorageRecreateWithoutRecoveryKey: true, + }), + ).rejects.toThrow( + "Matrix cross-signing key upload requires UIA; provide matrix.password for m.login.password fallback", + ); + + expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled(); + }); + 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 4a1a03fa83b..d337b64dfe2 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts @@ -47,14 +47,18 @@ export class MatrixCryptoBootstrapper { options: MatrixCryptoBootstrapOptions = {}, ): Promise { const strict = options.strict === true; + const deferSecretStorageBootstrapUntilAfterCrossSigning = + options.forceResetCrossSigning === true; // Register verification listeners before expensive bootstrap work so incoming requests // are not missed during startup. this.registerVerificationRequestHandler(crypto); - await this.bootstrapSecretStorage(crypto, { - strict, - allowSecretStorageRecreateWithoutRecoveryKey: - options.allowSecretStorageRecreateWithoutRecoveryKey === true, - }); + if (!deferSecretStorageBootstrapUntilAfterCrossSigning) { + await this.bootstrapSecretStorage(crypto, { + strict, + allowSecretStorageRecreateWithoutRecoveryKey: + options.allowSecretStorageRecreateWithoutRecoveryKey === true, + }); + } const crossSigning = await this.bootstrapCrossSigning(crypto, { forceResetCrossSigning: options.forceResetCrossSigning === true, allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false, @@ -62,6 +66,9 @@ export class MatrixCryptoBootstrapper { options.allowSecretStorageRecreateWithoutRecoveryKey === true, strict, }); + // Forced repair may need password UIA to upload new cross-signing keys. Delay any + // secret-storage repair/recreation until after that step succeeds so passwordless bots do + // not partially mutate SSSS on homeservers that require password-based UIA. await this.bootstrapSecretStorage(crypto, { strict, allowSecretStorageRecreateWithoutRecoveryKey: