diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index b779952a86..5445082e25 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -48,6 +48,11 @@ import { asyncSomeParallel } from "./utils/arrays.ts"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; +// Unfortunately named account data key used by Element X to indicate that the user +// has chosen to disable server side key backups. We need to set and honour this +// to prevent Element X from automatically turning key backup back on. +const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled"; + const logger = baseLogger.getChild("DeviceListener:"); export default class DeviceListener { @@ -323,9 +328,11 @@ export default class DeviceListener { logger.info("Some secrets not cached: showing KEY_STORAGE_OUT_OF_SYNC toast"); showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC); } else if (defaultKeyId === null) { - // the user just hasn't set up 4S yet: prompt them to do so - logger.info("No default 4S key: showing SET_UP_RECOVERY toast"); - showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); + // the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage) + const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY); + if (!disabledEvent?.getContent().disabled) { + showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); + } } else { // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did // in 'other' situations. Possibly we should consider prompting for a full reset in this case? diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 0bb6fc00c0..01baeff323 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -921,5 +921,62 @@ describe("DeviceListener", () => { }); }); }); + + describe("set up recovery", () => { + const rooms = [{ roomId: "!room1" }] as unknown as Room[]; + + beforeEach(() => { + mockCrypto!.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ + trustCrossSignedDevices: true, + crossSigningVerified: true, + }), + ); + mockCrypto!.isCrossSigningReady.mockResolvedValue(true); + mockCrypto!.isSecretStorageReady.mockResolvedValue(false); + mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null); + mockClient!.getRooms.mockReturnValue(rooms); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + }); + + it("shows the 'set up recovery' toast if user has not set up 4S", async () => { + await createAndStart(); + + expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(SetupEncryptionToast.Kind.SET_UP_RECOVERY); + }); + + it("does not show the 'set up recovery' toast if secret storage is set up", async () => { + mockCrypto!.isSecretStorageReady.mockResolvedValue(true); + mockClient.secretStorage.getDefaultKeyId.mockResolvedValue("thiskey"); + await createAndStart(); + + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.SET_UP_RECOVERY, + ); + }); + + it("does not show the 'set up recovery' toast if user has no encrypted rooms", async () => { + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); + await createAndStart(); + + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.SET_UP_RECOVERY, + ); + }); + + it("does not show the 'set up recovery' toast if the user has chosen to disable key storage", async () => { + mockClient!.getAccountData.mockImplementation((k: string) => { + if (k === "m.org.matrix.custom.backup_disabled") { + return new MatrixEvent({ content: { disabled: true } }); + } + return undefined; + }); + await createAndStart(); + + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.SET_UP_RECOVERY, + ); + }); + }); }); });