diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index ed3a58f9e55..2a0ec77a4ff 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -842,6 +842,53 @@ describe("MatrixClient crypto bootstrapping", () => { }); }); + it("does not force-reset bootstrap when the device is already signed by its owner", async () => { + matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() })); + const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, { + encryption: true, + password: "secret-password", + }); + const bootstrapSpy = vi.fn().mockResolvedValue({ + crossSigningReady: false, + crossSigningPublished: false, + ownDeviceVerified: true, + }); + ( + client as unknown as { + cryptoBootstrapper: { bootstrap: typeof bootstrapSpy }; + } + ).cryptoBootstrapper.bootstrap = bootstrapSpy; + vi.spyOn(client, "getOwnDeviceVerificationStatus").mockResolvedValue({ + encryptionEnabled: true, + userId: "@bot:example.org", + deviceId: "DEVICE123", + verified: true, + localVerified: true, + crossSigningVerified: false, + signedByOwner: true, + recoveryKeyStored: false, + recoveryKeyCreatedAt: null, + recoveryKeyId: null, + backupVersion: null, + backup: { + serverVersion: null, + activeVersion: null, + trusted: null, + matchesDecryptionKey: null, + decryptionKeyCached: false, + keyLoadAttempted: false, + keyLoadError: null, + }, + }); + + await client.start(); + + expect(bootstrapSpy).toHaveBeenCalledTimes(1); + expect(bootstrapSpy.mock.calls[0]?.[1]).toEqual({ + allowAutomaticCrossSigningReset: false, + }); + }); + it("does not force-reset bootstrap when password is unavailable", async () => { matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() })); const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, { diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index fa58392c686..afdefb57c7e 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -309,9 +309,17 @@ export class MatrixClient { if (!crypto) { return; } - const initial = await this.cryptoBootstrapper.bootstrap(crypto); + const initial = await this.cryptoBootstrapper.bootstrap(crypto, { + allowAutomaticCrossSigningReset: false, + }); if (!initial.crossSigningPublished || initial.ownDeviceVerified === false) { - if (this.password?.trim()) { + const status = await this.getOwnDeviceVerificationStatus(); + if (status.signedByOwner) { + LogService.warn( + "MatrixClientLite", + "Cross-signing/bootstrap is incomplete for an already owner-signed device; skipping automatic reset and preserving the current identity. Restore the recovery key or run an explicit verification bootstrap if repair is needed.", + ); + } else if (this.password?.trim()) { try { const repaired = await this.cryptoBootstrapper.bootstrap(crypto, { forceResetCrossSigning: true, @@ -757,7 +765,9 @@ export class MatrixClient { return await fail(err instanceof Error ? err.message : String(err)); } - await this.cryptoBootstrapper.bootstrap(crypto); + await this.cryptoBootstrapper.bootstrap(crypto, { + allowAutomaticCrossSigningReset: false, + }); const status = await this.getOwnDeviceVerificationStatus(); if (!status.verified) { return { diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts index 651e31ecf80..3299d52c856 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts @@ -99,6 +99,36 @@ describe("MatrixCryptoBootstrapper", () => { ); }); + it("does not auto-reset cross-signing when automatic reset is disabled", async () => { + const deps = createBootstrapperDeps(); + const bootstrapCrossSigning = vi.fn(async () => {}); + const crypto = createCryptoApi({ + bootstrapCrossSigning, + isCrossSigningReady: vi.fn(async () => false), + userHasCrossSigningKeys: vi.fn(async () => false), + getDeviceVerificationStatus: vi.fn(async () => ({ + isVerified: () => true, + localVerified: true, + crossSigningVerified: false, + signedByOwner: true, + })), + }); + const bootstrapper = new MatrixCryptoBootstrapper( + deps as unknown as MatrixCryptoBootstrapperDeps, + ); + + await bootstrapper.bootstrap(crypto, { + allowAutomaticCrossSigningReset: false, + }); + + expect(bootstrapCrossSigning).toHaveBeenCalledTimes(1); + expect(bootstrapCrossSigning).toHaveBeenCalledWith( + 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 a0321ca3de5..0829d1aa983 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts @@ -26,6 +26,7 @@ export type MatrixCryptoBootstrapperDeps = { export type MatrixCryptoBootstrapOptions = { forceResetCrossSigning?: boolean; + allowAutomaticCrossSigningReset?: boolean; strict?: boolean; }; @@ -51,6 +52,7 @@ export class MatrixCryptoBootstrapper { await this.bootstrapSecretStorage(crypto, strict); const crossSigning = await this.bootstrapCrossSigning(crypto, { forceResetCrossSigning: options.forceResetCrossSigning === true, + allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false, strict, }); await this.bootstrapSecretStorage(crypto, strict); @@ -91,7 +93,11 @@ export class MatrixCryptoBootstrapper { private async bootstrapCrossSigning( crypto: MatrixCryptoBootstrapApi, - options: { forceResetCrossSigning: boolean; strict: boolean }, + options: { + forceResetCrossSigning: boolean; + allowAutomaticCrossSigningReset: boolean; + strict: boolean; + }, ): Promise<{ ready: boolean; published: boolean }> { const userId = await this.deps.getUserId(); const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({ @@ -156,6 +162,14 @@ export class MatrixCryptoBootstrapper { authUploadDeviceSigningKeys, }); } catch (err) { + 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:", @@ -182,6 +196,10 @@ export class MatrixCryptoBootstrapper { return { ready: true, published: true }; } + if (!options.allowAutomaticCrossSigningReset) { + return { ready: firstPassReady, published: firstPassPublished }; + } + // Fallback: recover from broken local/server state by creating a fresh identity. try { await crypto.bootstrapCrossSigning({