Skip to content

Commit 858757a

Browse files
committed
Support MSC4287 m.key_backup stable prefix as well as the unstable prefix
1 parent c02b970 commit 858757a

6 files changed

Lines changed: 112 additions & 57 deletions

File tree

apps/web/playwright/e2e/crypto/toasts.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ test.describe("'Turn on key storage' toast", () => {
128128
test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => {
129129
// Given the backup is disabled but we didn't set account data saying that is expected
130130
await disableKeyBackup(app);
131-
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
131+
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", {} as any as { disabled: boolean });
132+
await botClient.setAccountData("m.key_backup", {} as any as { enabled: boolean });
132133

133134
// Wait for the account data setting to stick
134135
await new Promise((resolve) => setTimeout(resolve, 2000));

apps/web/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
1010
import { logger } from "matrix-js-sdk/src/logger";
1111

1212
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
13-
import { DeviceListener, BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../device-listener";
13+
import {
14+
DeviceListener,
15+
ACCOUNT_DATA_KEY_M_KEY_BACKUP,
16+
ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE,
17+
} from "../../../../device-listener";
1418
import { useEventEmitterAsyncState } from "../../../../hooks/useEventEmitter";
1519
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
1620

@@ -113,7 +117,10 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
113117
}
114118

115119
// Set the flag so that EX no longer thinks the user wants backup disabled
116-
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
120+
await matrixClient.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP, { enabled: true });
121+
await matrixClient.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE, {
122+
disabled: false,
123+
});
117124
} else {
118125
logger.info("User requested disabling key backup");
119126
// This method will delete the key backup as well as server side recovery keys and other
@@ -123,7 +130,10 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
123130
// Set a flag to say that the user doesn't want key backup.
124131
// Element X uses this to determine whether to set up automatically,
125132
// so this will stop EX turning it back on spontaneously.
126-
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
133+
await matrixClient.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP, { enabled: false });
134+
await matrixClient.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE, {
135+
disabled: true,
136+
});
127137
}
128138
});
129139
} finally {

apps/web/src/device-listener/DeviceListener.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export class DeviceListener {
149149
}
150150

151151
/**
152-
* Set the account data "m.org.matrix.custom.backup_disabled" to { "disabled": true }.
152+
* Set the account data "m.key_backup" to { "enabled": false }.
153153
*/
154154
public async recordKeyBackupDisabled(): Promise<void> {
155155
await this.currentDevice?.recordKeyBackupDisabled();

apps/web/src/device-listener/DeviceListenerCurrentDevice.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ import { asyncSomeParallel } from "../utils/arrays";
2929
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
3030

3131
/**
32-
* Unfortunately-named account data key used by Element X to indicate that the user
33-
* has chosen to disable server side key backups.
34-
*
35-
* We need to set and honour this to prevent Element X from automatically turning key backup back on.
32+
* Account data key used to indicate that the user has chosen to enable or
33+
* disable server side key backups.
3634
*/
37-
export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled";
35+
export const ACCOUNT_DATA_KEY_M_KEY_BACKUP = "m.key_backup";
36+
export const ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE = "m.org.matrix.custom.backup_disabled";
3837

3938
/**
4039
* Account data key to indicate whether the user has chosen to enable or disable recovery.
@@ -130,10 +129,11 @@ export class DeviceListenerCurrentDevice {
130129
}
131130

132131
/**
133-
* Set the account data "m.org.matrix.custom.backup_disabled" to `{ "disabled": true }`.
132+
* Set the account data "m.key_backup" to `{ "enabled": false }`.
134133
*/
135134
public async recordKeyBackupDisabled(): Promise<void> {
136-
await this.client.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
135+
await this.client.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP, { enabled: false });
136+
await this.client.setAccountData(ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE, { disabled: true });
137137
}
138138

139139
/**
@@ -284,8 +284,15 @@ export class DeviceListenerCurrentDevice {
284284
* Otherwise, fetch it from the store as normal.
285285
*/
286286
public async recheckBackupDisabled(): Promise<boolean> {
287-
const backupDisabled = await this.client.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
288-
return !!backupDisabled?.disabled;
287+
const keyBackup = await this.client.getAccountDataFromServer(ACCOUNT_DATA_KEY_M_KEY_BACKUP);
288+
if (keyBackup) {
289+
return !keyBackup.enabled;
290+
}
291+
292+
const keyBackupDisabledUnstable = await this.client.getAccountDataFromServer(
293+
ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE,
294+
);
295+
return !!keyBackupDisabledUnstable?.disabled;
289296
}
290297

291298
/**
@@ -334,7 +341,8 @@ export class DeviceListenerCurrentDevice {
334341
ev.getType().startsWith("m.secret_storage.") ||
335342
ev.getType().startsWith("m.cross_signing.") ||
336343
ev.getType() === "m.megolm_backup.v1" ||
337-
ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY ||
344+
ev.getType() === ACCOUNT_DATA_KEY_M_KEY_BACKUP ||
345+
ev.getType() === ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE ||
338346
ev.getType() === RECOVERY_ACCOUNT_DATA_KEY
339347
) {
340348
this.deviceListener.recheck();

apps/web/test/unit-tests/DeviceListener-test.ts

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import { type Mocked, mocked } from "jest-mock";
9+
import { type Mocked, mocked, type MockedObject } from "jest-mock";
1010
import {
1111
MatrixEvent,
1212
type Room,
@@ -25,7 +25,11 @@ import {
2525
} from "matrix-js-sdk/src/crypto-api";
2626
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
2727

28-
import { DeviceListener, BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/device-listener";
28+
import {
29+
DeviceListener,
30+
ACCOUNT_DATA_KEY_M_KEY_BACKUP,
31+
ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE,
32+
} from "../../src/device-listener";
2933
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
3034
import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast";
3135
import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast";
@@ -429,9 +433,7 @@ describe("DeviceListener", () => {
429433
it("does not show an out-of-sync toast when the backup key is missing locally but backup is purposely disabled", async () => {
430434
mockCrypto!.getSecretStorageStatus.mockResolvedValue(readySecretStorageStatus);
431435
mockCrypto!.getSessionBackupPrivateKey.mockResolvedValue(null);
432-
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
433-
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
434-
);
436+
mockKeyBackupFromServer(mockClient, false);
435437

436438
await createAndStart();
437439

@@ -537,6 +539,10 @@ describe("DeviceListener", () => {
537539
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
538540
disabled: true,
539541
});
542+
543+
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.key_backup", {
544+
enabled: false,
545+
});
540546
});
541547

542548
it("sets the recovery account data when we call recordRecoveryDisabled", async () => {
@@ -568,7 +574,7 @@ describe("DeviceListener", () => {
568574

569575
it("shows the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
570576
// Given key backup is off but the account data saying we turned it off is not set
571-
// (m.org.matrix.custom.backup_disabled)
577+
// (m.key_backup or m.org.matrix.custom.backup_disabled)
572578
mockClient.getAccountData.mockReturnValue(undefined);
573579

574580
// When we launch the DeviceListener
@@ -581,11 +587,16 @@ describe("DeviceListener", () => {
581587
it("shows the 'Turn on key storage' toast if we turned on key storage", async () => {
582588
// Given key backup is off but the account data says we turned it on (this should not happen - the
583589
// account data should only be updated if we turn on key storage)
584-
mockClient.getAccountData.mockImplementation((eventType) =>
585-
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
586-
? new MatrixEvent({ content: { disabled: false } })
587-
: undefined,
588-
);
590+
mockClient.getAccountData.mockImplementation((eventType) => {
591+
switch (eventType) {
592+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
593+
return new MatrixEvent({ content: { enabled: true } });
594+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
595+
return new MatrixEvent({ content: { disabled: false } });
596+
default:
597+
return undefined;
598+
}
599+
});
589600

590601
// When we launch the DeviceListener
591602
await createAndStart();
@@ -596,9 +607,7 @@ describe("DeviceListener", () => {
596607

597608
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
598609
// Given key backup is off but the account data saying we turned it off is set
599-
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
600-
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
601-
);
610+
mockKeyBackupFromServer(mockClient, false);
602611

603612
// When we launch the DeviceListener
604613
await createAndStart();
@@ -627,11 +636,16 @@ describe("DeviceListener", () => {
627636

628637
it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => {
629638
// Given key backup is on and the account data says we turned it on
630-
mockClient.getAccountData.mockImplementation((eventType) =>
631-
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
632-
? new MatrixEvent({ content: { disabled: false } })
633-
: undefined,
634-
);
639+
mockClient.getAccountData.mockImplementation((eventType) => {
640+
switch (eventType) {
641+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
642+
return new MatrixEvent({ content: { enabled: true } });
643+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
644+
return new MatrixEvent({ content: { disabled: false } });
645+
default:
646+
return undefined;
647+
}
648+
});
635649

636650
// When we launch the DeviceListener
637651
await createAndStart();
@@ -643,11 +657,16 @@ describe("DeviceListener", () => {
643657
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
644658
// Given key backup is on but the account data saying we turned it off is set (this should never
645659
// happen - it should only be set when we turn off key storage or dismiss the toast)
646-
mockClient.getAccountData.mockImplementation((eventType) =>
647-
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
648-
? new MatrixEvent({ content: { disabled: true } })
649-
: undefined,
650-
);
660+
mockClient.getAccountData.mockImplementation((eventType) => {
661+
switch (eventType) {
662+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
663+
return new MatrixEvent({ content: { enabled: false } });
664+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
665+
return new MatrixEvent({ content: { disabled: true } });
666+
default:
667+
return undefined;
668+
}
669+
});
651670

652671
// When we launch the DeviceListener
653672
await createAndStart();
@@ -1195,10 +1214,16 @@ describe("DeviceListener", () => {
11951214

11961215
it("does not show the 'set up recovery' toast if the user has chosen to disable key storage", async () => {
11971216
mockClient!.getAccountData.mockImplementation((k: string) => {
1198-
if (k === "m.org.matrix.custom.backup_disabled") {
1199-
return new MatrixEvent({ content: { disabled: true } });
1217+
switch (k) {
1218+
case "m.org.matrix.custom.backup_disabled":
1219+
return new MatrixEvent({ content: { disabled: true } });
1220+
1221+
case "m.key_backup":
1222+
return new MatrixEvent({ content: { enabled: false } });
1223+
1224+
default:
1225+
return undefined;
12001226
}
1201-
return undefined;
12021227
});
12031228
await createAndStart();
12041229

@@ -1211,18 +1236,15 @@ describe("DeviceListener", () => {
12111236
describe("needs backup reset", () => {
12121237
it("should not need resetting if backup disabled", async () => {
12131238
const deviceListener = await createAndStart();
1214-
mockClient.getAccountDataFromServer.mockResolvedValue({
1215-
disabled: true,
1216-
});
1239+
mockKeyBackupFromServer(mockClient, false);
1240+
12171241
expect(await deviceListener.keyStorageOutOfSyncNeedsBackupReset(false)).toBe(false);
12181242
expect(await deviceListener.keyStorageOutOfSyncNeedsBackupReset(true)).toBe(false);
12191243
});
12201244

12211245
it("should not need resetting if backup key is present locally or in 4S, and user has 4S key", async () => {
12221246
const deviceListener = await createAndStart();
1223-
mockClient.getAccountDataFromServer.mockResolvedValue({
1224-
disabled: false,
1225-
});
1247+
mockKeyBackupFromServer(mockClient, true);
12261248

12271249
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
12281250
mockClient.isKeyBackupKeyStored.mockResolvedValue({});
@@ -1235,9 +1257,7 @@ describe("DeviceListener", () => {
12351257

12361258
it("should not need resetting if backup key is present locally and user forgot 4S key", async () => {
12371259
const deviceListener = await createAndStart();
1238-
mockClient.getAccountDataFromServer.mockResolvedValue({
1239-
disabled: false,
1240-
});
1260+
mockKeyBackupFromServer(mockClient, true);
12411261

12421262
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(new Uint8Array());
12431263
mockClient.isKeyBackupKeyStored.mockResolvedValue(null);
@@ -1246,9 +1266,7 @@ describe("DeviceListener", () => {
12461266

12471267
it("should need resetting if backup key is missing locally and user forgot 4S key", async () => {
12481268
const deviceListener = await createAndStart();
1249-
mockClient.getAccountDataFromServer.mockResolvedValue({
1250-
disabled: false,
1251-
});
1269+
mockKeyBackupFromServer(mockClient, true);
12521270

12531271
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
12541272
mockClient.isKeyBackupKeyStored.mockResolvedValue({});
@@ -1257,9 +1275,7 @@ describe("DeviceListener", () => {
12571275

12581276
it("should need resetting if backup key is missing locally and in 4s", async () => {
12591277
const deviceListener = await createAndStart();
1260-
mockClient.getAccountDataFromServer.mockResolvedValue({
1261-
disabled: false,
1262-
});
1278+
mockKeyBackupFromServer(mockClient, true);
12631279

12641280
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
12651281
mockClient.isKeyBackupKeyStored.mockResolvedValue(null);
@@ -1337,3 +1353,16 @@ describe("DeviceListener", () => {
13371353
});
13381354
});
13391355
});
1356+
1357+
function mockKeyBackupFromServer(client: MockedObject<MatrixClient>, enabled: boolean) {
1358+
client.getAccountDataFromServer.mockImplementation(async (eventType: string) => {
1359+
switch (eventType) {
1360+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
1361+
return { enabled } as any;
1362+
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
1363+
return { disabled: !enabled } as any;
1364+
default:
1365+
return null;
1366+
}
1367+
});
1368+
}

apps/web/test/unit-tests/components/viewmodels/settings/encryption/KeyStoragePanelViewModel-test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ describe("KeyStoragePanelViewModel", () => {
128128
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
129129
disabled: false,
130130
});
131+
132+
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.key_backup", {
133+
enabled: true,
134+
});
131135
});
132136

133137
it("should delete key storage when disabling", async () => {
@@ -145,5 +149,8 @@ describe("KeyStoragePanelViewModel", () => {
145149
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
146150
disabled: true,
147151
});
152+
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.key_backup", {
153+
enabled: false,
154+
});
148155
});
149156
});

0 commit comments

Comments
 (0)