diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 18c2ee6e90a..a0416d01a55 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -16,10 +16,9 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { logger } from "matrix-js-sdk/src/logger"; +import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -41,9 +40,34 @@ interface IState { backupKeyWellFormed: boolean | null; secretStorageKeyInAccount: boolean | null; secretStorageReady: boolean | null; - backupInfo: IKeyBackupInfo | null; - backupSigStatus: TrustInfo | null; - sessionsRemaining: number; + + /** Information on the current key backup version, as returned by the server. + * + * `null` could mean any of: + * * we haven't yet requested the data from the server. + * * we were unable to reach the server. + * * the server returned key backup version data we didn't understand or was malformed. + * * there is actually no backup on the server. + */ + backupInfo: KeyBackupInfo | null; + + /** + * Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to + * decrypt it. + * + * `undefined` if `backupInfo` is null, or if crypto is not enabled in the client. + */ + backupTrustInfo: BackupTrustInfo | undefined; + + /** + * If key backup is currently enabled, the backup version we are backing up to. + */ + activeBackupVersion: string | null; + + /** + * Number of sessions remaining to be backed up. `null` if we have no information on this. + */ + sessionsRemaining: number | null; } export default class SecureBackupPanel extends React.PureComponent<{}, IState> { @@ -61,8 +85,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { secretStorageKeyInAccount: null, secretStorageReady: null, backupInfo: null, - backupSigStatus: null, - sessionsRemaining: 0, + backupTrustInfo: undefined, + activeBackupVersion: null, + sessionsRemaining: null, }; } @@ -101,14 +126,19 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { this.setState({ loading: true }); this.getUpdatedDiagnostics(); try { - const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion(); - const backupSigStatus = backupInfo ? await MatrixClientPeg.safeGet().isKeyBackupTrusted(backupInfo) : null; + const cli = MatrixClientPeg.safeGet(); + const backupInfo = await cli.getKeyBackupVersion(); + const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; + + const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; + if (this.unmounted) return; this.setState({ loading: false, error: false, backupInfo, - backupSigStatus, + backupTrustInfo, + activeBackupVersion, }); } catch (e) { logger.log("Unable to fetch key backup status", e); @@ -117,7 +147,8 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { loading: false, error: true, backupInfo: null, - backupSigStatus: null, + backupTrustInfo: undefined, + activeBackupVersion: null, }); } } @@ -173,8 +204,10 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { onFinished: (proceed) => { if (!proceed) return; this.setState({ loading: true }); + const versionToDelete = this.state.backupInfo!.version!; MatrixClientPeg.safeGet() - .deleteKeyBackupVersion(this.state.backupInfo!.version!) + .getCrypto() + ?.deleteKeyBackupVersion(versionToDelete) .then(() => { this.loadBackupStatus(); }); @@ -209,7 +242,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { secretStorageKeyInAccount, secretStorageReady, backupInfo, - backupSigStatus, + backupTrustInfo, sessionsRemaining, } = this.state; @@ -228,7 +261,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } else if (backupInfo) { let restoreButtonCaption = _t("Restore from Backup"); - if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) { + if (this.state.activeBackupVersion !== null) { statusDescription = ( ✅ {_t("This session is backing up your keys.")} ); @@ -253,7 +286,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } let uploadStatus: ReactNode; - if (!MatrixClientPeg.safeGet().getKeyBackupEnabled()) { + if (sessionsRemaining === null) { // No upload status to show when backup disabled. uploadStatus = ""; } else if (sessionsRemaining > 0) { @@ -271,19 +304,21 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } let trustedLocally: string | undefined; - if (backupSigStatus?.trusted_locally) { - trustedLocally = _t("This backup is trusted because it has been restored on this session"); + if (backupTrustInfo?.matchesDecryptionKey) { + trustedLocally = _t("This backup can be restored on this session"); } extraDetailsTableRows = ( <> - {_t("Backup version:")} - {backupInfo.version} + {_t("Latest backup version on server:")} + + {backupInfo.version} ({_t("Algorithm:")} {backupInfo.algorithm}) + - {_t("Algorithm:")} - {backupInfo.algorithm} + {_t("Active backup version:")} + {this.state.activeBackupVersion === null ? _t("None") : this.state.activeBackupVersion} ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92adb2fce5f..556d62f0879 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2138,9 +2138,10 @@ "Connect this session to Key Backup": "Connect this session to Key Backup", "Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…", "All keys backed up": "All keys backed up", - "This backup is trusted because it has been restored on this session": "This backup is trusted because it has been restored on this session", - "Backup version:": "Backup version:", + "This backup can be restored on this session": "This backup can be restored on this session", + "Latest backup version on server:": "Latest backup version on server:", "Algorithm:": "Algorithm:", + "Active backup version:": "Active backup version:", "Your keys are not being backed up from this session.": "Your keys are not being backed up from this session.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Set up": "Set up", diff --git a/test/components/views/settings/SecureBackupPanel-test.tsx b/test/components/views/settings/SecureBackupPanel-test.tsx index d5b28981f2b..cadd0353aab 100644 --- a/test/components/views/settings/SecureBackupPanel-test.tsx +++ b/test/components/views/settings/SecureBackupPanel-test.tsx @@ -36,11 +36,8 @@ describe("", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsCrypto(), - getKeyBackupEnabled: jest.fn(), getKeyBackupVersion: jest.fn().mockReturnValue("1"), - isKeyBackupTrusted: jest.fn().mockResolvedValue(true), getClientWellKnown: jest.fn(), - deleteKeyBackupVersion: jest.fn(), }); const getComponent = () => render(); @@ -53,15 +50,17 @@ describe("", () => { public_key: "1234", }, }); - client.isKeyBackupTrusted.mockResolvedValue({ - usable: false, - sigs: [], + Object.assign(client.getCrypto()!, { + isKeyBackupTrusted: jest.fn().mockResolvedValue({ + trusted: false, + matchesDecryptionKey: false, + }), + getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), + deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined), }); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); - client.deleteKeyBackupVersion.mockClear().mockResolvedValue(); client.getKeyBackupVersion.mockClear(); - client.isKeyBackupTrusted.mockClear(); mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); @@ -100,7 +99,7 @@ describe("", () => { }); it("displays when session is connected to key backup", async () => { - client.getKeyBackupEnabled.mockReturnValue(true); + mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1"); getComponent(); // flush checkKeyBackup promise await flushPromises(); @@ -125,7 +124,7 @@ describe("", () => { fireEvent.click(within(dialog).getByText("Cancel")); - expect(client.deleteKeyBackupVersion).not.toHaveBeenCalled(); + expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled(); }); it("deletes backup after confirmation", async () => { @@ -154,7 +153,7 @@ describe("", () => { fireEvent.click(within(dialog).getByTestId("dialog-primary-button")); - expect(client.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); + expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); // delete request await flushPromises(); @@ -169,7 +168,7 @@ describe("", () => { await flushPromises(); client.getKeyBackupVersion.mockClear(); - client.isKeyBackupTrusted.mockClear(); + mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); fireEvent.click(screen.getByText("Reset")); @@ -179,6 +178,6 @@ describe("", () => { // backup status refreshed expect(client.getKeyBackupVersion).toHaveBeenCalled(); - expect(client.isKeyBackupTrusted).toHaveBeenCalled(); + expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); }); }); diff --git a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap index dc00cd38e44..47a6c83f79c 100644 --- a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap @@ -140,20 +140,27 @@ exports[` suggests connecting session to key backup when ba - Backup version: + Latest backup version on server: 1 + ( + Algorithm: + + + test + + ) - Algorithm: + Active backup version: - test + None