Support MSC4287 m.key_backup stable prefix as well as the unstable prefix (#33034)

* Support MSC4287 m.key_backup stable prefix as well as the unstable prefix

* Update comments to avoid talking about EX since this now applies more widely

* Make comments about m.key_backup more helpful

* Improve comment on recheckBackupDisabled

* Explicitly say backup is disabled only if enabled is actually set to false
This commit is contained in:
Andy Balaam
2026-06-02 11:25:03 +01:00
committed by GitHub
parent 1c04814c2b
commit 65b0ac8c28
6 changed files with 123 additions and 63 deletions
+75 -43
View File
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Mocked, mocked } from "jest-mock";
import { type Mocked, mocked, type MockedObject } from "jest-mock";
import {
MatrixEvent,
type Room,
@@ -25,7 +25,12 @@ import {
} from "matrix-js-sdk/src/crypto-api";
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
import { DeviceListener, BACKUP_DISABLED_ACCOUNT_DATA_KEY, RECOVERY_ACCOUNT_DATA_KEY } from "../../src/device-listener";
import {
DeviceListener,
ACCOUNT_DATA_KEY_M_KEY_BACKUP,
ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE,
RECOVERY_ACCOUNT_DATA_KEY,
} from "../../src/device-listener";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast";
import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast";
@@ -322,8 +327,10 @@ describe("DeviceListener", () => {
// @ts-ignore implementing a function with complex return type
mockClient!.getAccountDataFromServer.mockImplementation(async (key) => {
if (key === BACKUP_DISABLED_ACCOUNT_DATA_KEY) {
if (key === ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE) {
return { disabled: true };
} else if (key == ACCOUNT_DATA_KEY_M_KEY_BACKUP) {
return { enabled: false };
} else if (key === RECOVERY_ACCOUNT_DATA_KEY) {
return null;
} else {
@@ -513,9 +520,7 @@ describe("DeviceListener", () => {
it("does not show an out-of-sync toast when the backup key is missing locally but backup is purposely disabled", async () => {
mockCrypto!.getSecretStorageStatus.mockResolvedValue(readySecretStorageStatus);
mockCrypto!.getSessionBackupPrivateKey.mockResolvedValue(null);
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
);
mockKeyBackupFromServer(mockClient, false);
await createAndStart();
@@ -621,6 +626,10 @@ describe("DeviceListener", () => {
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
disabled: true,
});
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.key_backup", {
enabled: false,
});
});
it("sets the recovery account data when we call recordRecoveryDisabled", async () => {
@@ -652,7 +661,7 @@ describe("DeviceListener", () => {
it("shows the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
// Given key backup is off but the account data saying we turned it off is not set
// (m.org.matrix.custom.backup_disabled)
// (m.key_backup or m.org.matrix.custom.backup_disabled)
mockClient.getAccountData.mockReturnValue(undefined);
// When we launch the DeviceListener
@@ -665,11 +674,16 @@ describe("DeviceListener", () => {
it("shows the 'Turn on key storage' toast if we turned on key storage", async () => {
// Given key backup is off but the account data says we turned it on (this should not happen - the
// account data should only be updated if we turn on key storage)
mockClient.getAccountData.mockImplementation((eventType) =>
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
? new MatrixEvent({ content: { disabled: false } })
: undefined,
);
mockClient.getAccountData.mockImplementation((eventType) => {
switch (eventType) {
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
return new MatrixEvent({ content: { enabled: true } });
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
return new MatrixEvent({ content: { disabled: false } });
default:
return undefined;
}
});
// When we launch the DeviceListener
await createAndStart();
@@ -680,9 +694,7 @@ describe("DeviceListener", () => {
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
// Given key backup is off but the account data saying we turned it off is set
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
);
mockKeyBackupFromServer(mockClient, false);
// When we launch the DeviceListener
await createAndStart();
@@ -711,11 +723,16 @@ describe("DeviceListener", () => {
it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => {
// Given key backup is on and the account data says we turned it on
mockClient.getAccountData.mockImplementation((eventType) =>
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
? new MatrixEvent({ content: { disabled: false } })
: undefined,
);
mockClient.getAccountData.mockImplementation((eventType) => {
switch (eventType) {
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
return new MatrixEvent({ content: { enabled: true } });
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
return new MatrixEvent({ content: { disabled: false } });
default:
return undefined;
}
});
// When we launch the DeviceListener
await createAndStart();
@@ -727,11 +744,16 @@ describe("DeviceListener", () => {
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
// Given key backup is on but the account data saying we turned it off is set (this should never
// happen - it should only be set when we turn off key storage or dismiss the toast)
mockClient.getAccountData.mockImplementation((eventType) =>
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
? new MatrixEvent({ content: { disabled: true } })
: undefined,
);
mockClient.getAccountData.mockImplementation((eventType) => {
switch (eventType) {
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
return new MatrixEvent({ content: { enabled: false } });
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
return new MatrixEvent({ content: { disabled: true } });
default:
return undefined;
}
});
// When we launch the DeviceListener
await createAndStart();
@@ -1279,10 +1301,16 @@ describe("DeviceListener", () => {
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 } });
switch (k) {
case "m.org.matrix.custom.backup_disabled":
return new MatrixEvent({ content: { disabled: true } });
case "m.key_backup":
return new MatrixEvent({ content: { enabled: false } });
default:
return undefined;
}
return undefined;
});
await createAndStart();
@@ -1295,18 +1323,15 @@ describe("DeviceListener", () => {
describe("needs backup reset", () => {
it("should not need resetting if backup disabled", async () => {
const deviceListener = await createAndStart();
mockClient.getAccountDataFromServer.mockResolvedValue({
disabled: true,
});
mockKeyBackupFromServer(mockClient, false);
expect(await deviceListener.keyStorageOutOfSyncNeedsBackupReset(false)).toBe(false);
expect(await deviceListener.keyStorageOutOfSyncNeedsBackupReset(true)).toBe(false);
});
it("should not need resetting if backup key is present locally or in 4S, and user has 4S key", async () => {
const deviceListener = await createAndStart();
mockClient.getAccountDataFromServer.mockResolvedValue({
disabled: false,
});
mockKeyBackupFromServer(mockClient, true);
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
mockClient.isKeyBackupKeyStored.mockResolvedValue({});
@@ -1319,9 +1344,7 @@ describe("DeviceListener", () => {
it("should not need resetting if backup key is present locally and user forgot 4S key", async () => {
const deviceListener = await createAndStart();
mockClient.getAccountDataFromServer.mockResolvedValue({
disabled: false,
});
mockKeyBackupFromServer(mockClient, true);
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(new Uint8Array());
mockClient.isKeyBackupKeyStored.mockResolvedValue(null);
@@ -1330,9 +1353,7 @@ describe("DeviceListener", () => {
it("should need resetting if backup key is missing locally and user forgot 4S key", async () => {
const deviceListener = await createAndStart();
mockClient.getAccountDataFromServer.mockResolvedValue({
disabled: false,
});
mockKeyBackupFromServer(mockClient, true);
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
mockClient.isKeyBackupKeyStored.mockResolvedValue({});
@@ -1341,9 +1362,7 @@ describe("DeviceListener", () => {
it("should need resetting if backup key is missing locally and in 4s", async () => {
const deviceListener = await createAndStart();
mockClient.getAccountDataFromServer.mockResolvedValue({
disabled: false,
});
mockKeyBackupFromServer(mockClient, true);
mockCrypto.getSessionBackupPrivateKey.mockResolvedValue(null);
mockClient.isKeyBackupKeyStored.mockResolvedValue(null);
@@ -1421,3 +1440,16 @@ describe("DeviceListener", () => {
});
});
});
function mockKeyBackupFromServer(client: MockedObject<MatrixClient>, enabled: boolean) {
client.getAccountDataFromServer.mockImplementation(async (eventType: string) => {
switch (eventType) {
case ACCOUNT_DATA_KEY_M_KEY_BACKUP:
return { enabled } as any;
case ACCOUNT_DATA_KEY_M_KEY_BACKUP_DISABLED_UNSTABLE:
return { disabled: !enabled } as any;
default:
return null;
}
});
}
@@ -128,6 +128,10 @@ describe("KeyStoragePanelViewModel", () => {
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
disabled: false,
});
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.key_backup", {
enabled: true,
});
});
it("should delete key storage when disabling", async () => {
@@ -145,5 +149,8 @@ describe("KeyStoragePanelViewModel", () => {
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
disabled: true,
});
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.key_backup", {
enabled: false,
});
});
});