SecureBackupPanel: stop using deprecated APIs, and other fixes (#11644)

* SecureBackupPanel: replace `isKeyBackupTrusted`

`MatrixClient.isKeyBackupTrusted` -> `CryptoApi.isKeyBackupTrusted`

* SecureBackupPanel: replace `getKeyBackupEnabled`

`MatrixClient.getKeyBackupEnabled` -> `CryptoApi.getActiveSessionBackupVersion`

* SecureBackupPanel: replace `deleteKeyBackupVersion`

`MatrixClient.deleteKeyBackupVersion` -> `CryptoApi.deleteKeyBackupVersion`

* Do not show session count if we have no info

We shouldn't say "zero sessions to back up" if we don't know.

* SecureBackupPanel: distinguish between server and active backup
This commit is contained in:
Richard van der Hoff 2023-09-22 12:57:11 +02:00 committed by GitHub
parent 4f7d9da140
commit 11f258e62e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 39 deletions

View file

@ -16,10 +16,9 @@ limitations under the License.
*/ */
import React, { ReactNode } from "react"; 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 { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { logger } from "matrix-js-sdk/src/logger"; 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 type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -41,9 +40,34 @@ interface IState {
backupKeyWellFormed: boolean | null; backupKeyWellFormed: boolean | null;
secretStorageKeyInAccount: boolean | null; secretStorageKeyInAccount: boolean | null;
secretStorageReady: boolean | null; secretStorageReady: boolean | null;
backupInfo: IKeyBackupInfo | null;
backupSigStatus: TrustInfo | null; /** Information on the current key backup version, as returned by the server.
sessionsRemaining: number; *
* `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> { export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
@ -61,8 +85,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
secretStorageKeyInAccount: null, secretStorageKeyInAccount: null,
secretStorageReady: null, secretStorageReady: null,
backupInfo: null, backupInfo: null,
backupSigStatus: null, backupTrustInfo: undefined,
sessionsRemaining: 0, activeBackupVersion: null,
sessionsRemaining: null,
}; };
} }
@ -101,14 +126,19 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
this.setState({ loading: true }); this.setState({ loading: true });
this.getUpdatedDiagnostics(); this.getUpdatedDiagnostics();
try { try {
const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion(); const cli = MatrixClientPeg.safeGet();
const backupSigStatus = backupInfo ? await MatrixClientPeg.safeGet().isKeyBackupTrusted(backupInfo) : null; 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; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,
error: false, error: false,
backupInfo, backupInfo,
backupSigStatus, backupTrustInfo,
activeBackupVersion,
}); });
} catch (e) { } catch (e) {
logger.log("Unable to fetch key backup status", e); logger.log("Unable to fetch key backup status", e);
@ -117,7 +147,8 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
loading: false, loading: false,
error: true, error: true,
backupInfo: null, backupInfo: null,
backupSigStatus: null, backupTrustInfo: undefined,
activeBackupVersion: null,
}); });
} }
} }
@ -173,8 +204,10 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return; if (!proceed) return;
this.setState({ loading: true }); this.setState({ loading: true });
const versionToDelete = this.state.backupInfo!.version!;
MatrixClientPeg.safeGet() MatrixClientPeg.safeGet()
.deleteKeyBackupVersion(this.state.backupInfo!.version!) .getCrypto()
?.deleteKeyBackupVersion(versionToDelete)
.then(() => { .then(() => {
this.loadBackupStatus(); this.loadBackupStatus();
}); });
@ -209,7 +242,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
secretStorageKeyInAccount, secretStorageKeyInAccount,
secretStorageReady, secretStorageReady,
backupInfo, backupInfo,
backupSigStatus, backupTrustInfo,
sessionsRemaining, sessionsRemaining,
} = this.state; } = this.state;
@ -228,7 +261,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} else if (backupInfo) { } else if (backupInfo) {
let restoreButtonCaption = _t("Restore from Backup"); let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) { if (this.state.activeBackupVersion !== null) {
statusDescription = ( statusDescription = (
<SettingsSubsectionText> {_t("This session is backing up your keys.")}</SettingsSubsectionText> <SettingsSubsectionText> {_t("This session is backing up your keys.")}</SettingsSubsectionText>
); );
@ -253,7 +286,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} }
let uploadStatus: ReactNode; let uploadStatus: ReactNode;
if (!MatrixClientPeg.safeGet().getKeyBackupEnabled()) { if (sessionsRemaining === null) {
// No upload status to show when backup disabled. // No upload status to show when backup disabled.
uploadStatus = ""; uploadStatus = "";
} else if (sessionsRemaining > 0) { } else if (sessionsRemaining > 0) {
@ -271,19 +304,21 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} }
let trustedLocally: string | undefined; let trustedLocally: string | undefined;
if (backupSigStatus?.trusted_locally) { if (backupTrustInfo?.matchesDecryptionKey) {
trustedLocally = _t("This backup is trusted because it has been restored on this session"); trustedLocally = _t("This backup can be restored on this session");
} }
extraDetailsTableRows = ( extraDetailsTableRows = (
<> <>
<tr> <tr>
<th scope="row">{_t("Backup version:")}</th> <th scope="row">{_t("Latest backup version on server:")}</th>
<td>{backupInfo.version}</td> <td>
{backupInfo.version} ({_t("Algorithm:")} <code>{backupInfo.algorithm}</code>)
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">{_t("Algorithm:")}</th> <th scope="row">{_t("Active backup version:")}</th>
<td>{backupInfo.algorithm}</td> <td>{this.state.activeBackupVersion === null ? _t("None") : this.state.activeBackupVersion}</td>
</tr> </tr>
</> </>
); );

View file

@ -2138,9 +2138,10 @@
"Connect this session to Key Backup": "Connect this session to Key Backup", "Connect this session to Key Backup": "Connect this session to Key Backup",
"Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…", "Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…",
"All keys backed up": "All keys backed up", "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", "This backup can be restored on this session": "This backup can be restored on this session",
"Backup version:": "Backup version:", "Latest backup version on server:": "Latest backup version on server:",
"Algorithm:": "Algorithm:", "Algorithm:": "Algorithm:",
"Active backup version:": "Active backup version:",
"Your keys are <b>not being backed up from this session</b>.": "Your keys are <b>not being backed up from this session</b>.", "Your keys are <b>not being backed up from this session</b>.": "Your keys are <b>not being backed up from this session</b>.",
"Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "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", "Set up": "Set up",

View file

@ -36,11 +36,8 @@ describe("<SecureBackupPanel />", () => {
const client = getMockClientWithEventEmitter({ const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId), ...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(), ...mockClientMethodsCrypto(),
getKeyBackupEnabled: jest.fn(),
getKeyBackupVersion: jest.fn().mockReturnValue("1"), getKeyBackupVersion: jest.fn().mockReturnValue("1"),
isKeyBackupTrusted: jest.fn().mockResolvedValue(true),
getClientWellKnown: jest.fn(), getClientWellKnown: jest.fn(),
deleteKeyBackupVersion: jest.fn(),
}); });
const getComponent = () => render(<SecureBackupPanel />); const getComponent = () => render(<SecureBackupPanel />);
@ -53,15 +50,17 @@ describe("<SecureBackupPanel />", () => {
public_key: "1234", public_key: "1234",
}, },
}); });
client.isKeyBackupTrusted.mockResolvedValue({ Object.assign(client.getCrypto()!, {
usable: false, isKeyBackupTrusted: jest.fn().mockResolvedValue({
sigs: [], trusted: false,
matchesDecryptionKey: false,
}),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
}); });
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
client.deleteKeyBackupVersion.mockClear().mockResolvedValue();
client.getKeyBackupVersion.mockClear(); client.getKeyBackupVersion.mockClear();
client.isKeyBackupTrusted.mockClear();
mocked(accessSecretStorage).mockClear().mockResolvedValue(); mocked(accessSecretStorage).mockClear().mockResolvedValue();
}); });
@ -100,7 +99,7 @@ describe("<SecureBackupPanel />", () => {
}); });
it("displays when session is connected to key backup", async () => { it("displays when session is connected to key backup", async () => {
client.getKeyBackupEnabled.mockReturnValue(true); mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1");
getComponent(); getComponent();
// flush checkKeyBackup promise // flush checkKeyBackup promise
await flushPromises(); await flushPromises();
@ -125,7 +124,7 @@ describe("<SecureBackupPanel />", () => {
fireEvent.click(within(dialog).getByText("Cancel")); fireEvent.click(within(dialog).getByText("Cancel"));
expect(client.deleteKeyBackupVersion).not.toHaveBeenCalled(); expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled();
}); });
it("deletes backup after confirmation", async () => { it("deletes backup after confirmation", async () => {
@ -154,7 +153,7 @@ describe("<SecureBackupPanel />", () => {
fireEvent.click(within(dialog).getByTestId("dialog-primary-button")); fireEvent.click(within(dialog).getByTestId("dialog-primary-button"));
expect(client.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1");
// delete request // delete request
await flushPromises(); await flushPromises();
@ -169,7 +168,7 @@ describe("<SecureBackupPanel />", () => {
await flushPromises(); await flushPromises();
client.getKeyBackupVersion.mockClear(); client.getKeyBackupVersion.mockClear();
client.isKeyBackupTrusted.mockClear(); mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
fireEvent.click(screen.getByText("Reset")); fireEvent.click(screen.getByText("Reset"));
@ -179,6 +178,6 @@ describe("<SecureBackupPanel />", () => {
// backup status refreshed // backup status refreshed
expect(client.getKeyBackupVersion).toHaveBeenCalled(); expect(client.getKeyBackupVersion).toHaveBeenCalled();
expect(client.isKeyBackupTrusted).toHaveBeenCalled(); expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
}); });
}); });

View file

@ -140,20 +140,27 @@ exports[`<SecureBackupPanel /> suggests connecting session to key backup when ba
<th <th
scope="row" scope="row"
> >
Backup version: Latest backup version on server:
</th> </th>
<td> <td>
1 1
(
Algorithm:
<code>
test
</code>
)
</td> </td>
</tr> </tr>
<tr> <tr>
<th <th
scope="row" scope="row"
> >
Algorithm: Active backup version:
</th> </th>
<td> <td>
test None
</td> </td>
</tr> </tr>
</table> </table>