1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Implement CryptoApi.checkKeyBackupAndEnable (#3633)

* Implement `CryptoApi.checkKeyBackup`

* Deprecate `MatrixClient.enableKeyBackup`.

* fix integ test

* more tests

---------

Co-authored-by: valere <valeref@matrix.org>
This commit is contained in:
Richard van der Hoff
2023-08-09 10:59:03 +01:00
committed by GitHub
parent 16ddcb0ed0
commit 3f7af189e4
8 changed files with 343 additions and 18 deletions

View File

@@ -176,7 +176,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(event.getContent()).toEqual("testytest");
});
oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () {
it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
@@ -187,7 +187,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.checkKeyBackup();
await aliceCrypto.checkKeyBackupAndEnable();
// At this point there is no backup
let backupStatus: string | null;
@@ -201,9 +201,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
overwriteRoutes: true,
});
const checked = await aliceClient.checkKeyBackup();
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
expect(checked?.trustInfo?.usable).toBeFalsy();
expect(checked?.trustInfo?.trusted).toBeFalsy();
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();
@@ -222,8 +222,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
});
const validCheck = await aliceClient.checkKeyBackup();
expect(validCheck?.trustInfo?.usable).toStrictEqual(true);
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
await backupPromise;
@@ -286,6 +286,128 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
});
describe("checkKeyBackupAndEnable", () => {
it("enables a backup signed by a trusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
});
it("does not enable a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted case
await aliceClient.startClient();
await waitForDeviceList();
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("disables backup when a new untrusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("switches backup when a new trusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const newBackupVersion = "2";
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
unsignedBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
});
it("Disables when backup is deleted", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
},
{
overwriteRoutes: true,
},
);
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
});
/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own

View File

@@ -2252,6 +2252,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.reEmitter.reEmit(rustCrypto, [
CryptoEvent.VerificationRequestReceived,
CryptoEvent.UserTrustStatusChanged,
CryptoEvent.KeyBackupStatus,
]);
}
@@ -3251,6 +3252,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
*/
public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
if (!this.crypto) {
@@ -3320,6 +3323,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param info - Backup information object as returned by getKeyBackupVersion
* @returns Promise which resolves when complete.
*
* @deprecated Do not call this directly. Instead call {@link CryptoApi.checkKeyBackupAndEnable}.
*/
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
if (!this.crypto) {

View File

@@ -20,7 +20,7 @@ import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { BackupTrustInfo, KeyBackupInfo } from "./crypto-api/keybackup";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
import { ISignatures } from "./@types/signed";
/**
@@ -350,6 +350,17 @@ export interface CryptoApi {
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
*/
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
/**
* Force a re-check of the key backup and enable/disable it as appropriate.
*
* Fetches the current backup information from the server. If there is a backup, and it is trusted, starts
* backing up to it; otherwise, disables backups.
*
* @returns `null` if there is no backup on the server. Otherwise, data on the backup as returned by the server,
* and trust information (as returned by {@link isKeyBackupTrusted}).
*/
checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null>;
}
/**

View File

@@ -60,3 +60,11 @@ export interface BackupTrustInfo {
*/
readonly matchesDecryptionKey: boolean;
}
/**
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
*/
export interface KeyBackupCheck {
backupInfo: KeyBackupInfo;
trustInfo: BackupTrustInfo;
}

View File

@@ -92,6 +92,7 @@ import {
CrossSigningStatus,
DeviceVerificationStatus,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest as CryptoApiVerificationRequest,
} from "../crypto-api";
@@ -1304,6 +1305,20 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return backupTrustInfoFromLegacyTrustInfo(trustInfo);
}
/**
* Force a re-check of the key backup and enable/disable it as appropriate.
*
* Implementation of {@link CryptoApi.checkKeyBackupAndEnable}.
*/
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
const checkResult = await this.backupManager.checkKeyBackup();
if (!checkResult || !checkResult.backupInfo) return null;
return {
backupInfo: checkResult.backupInfo,
trustInfo: backupTrustInfoFromLegacyTrustInfo(checkResult.trustInfo),
};
}
/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the

View File

@@ -17,20 +17,33 @@ limitations under the License.
import { OlmMachine, SignatureVerification } from "@matrix-org/matrix-sdk-crypto-wasm";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { BackupTrustInfo, Curve25519AuthData, KeyBackupInfo } from "../crypto-api/keybackup";
import { BackupTrustInfo, Curve25519AuthData, KeyBackupCheck, KeyBackupInfo } from "../crypto-api/keybackup";
import { logger } from "../logger";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
/**
* @internal
*/
export class RustBackupManager {
public constructor(private readonly olmMachine: OlmMachine) {}
export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents, RustBackupCryptoEventMap> {
/** Have we checked if there is a backup on the server which we can use */
private checkedForBackup = false;
private activeBackupVersion: string | null = null;
public constructor(
private readonly olmMachine: OlmMachine,
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
) {
super();
}
/**
* Get the backup version we are currently backing up to, if any
*/
public async getActiveBackupVersion(): Promise<string | null> {
// TODO stub
return null;
if (!this.olmMachine.isBackupEnabled()) return null;
return this.activeBackupVersion;
}
/**
@@ -52,4 +65,138 @@ export class RustBackupManager {
trusted: signatureVerification.trusted(),
};
}
/**
* Re-check the key backup and enable/disable it as appropriate.
*
* @param force - whether we should force a re-check even if one has already happened.
*/
public checkKeyBackupAndEnable(force: boolean): Promise<KeyBackupCheck | null> {
if (!force && this.checkedForBackup) {
return Promise.resolve(null);
}
// make sure there is only one check going on at a time
if (!this.keyBackupCheckInProgress) {
this.keyBackupCheckInProgress = this.doCheckKeyBackup().finally(() => {
this.keyBackupCheckInProgress = null;
});
}
return this.keyBackupCheckInProgress;
}
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
/** Helper for `checkKeyBackup` */
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
logger.log("Checking key backup status...");
let backupInfo: KeyBackupInfo | null = null;
try {
backupInfo = await this.requestKeyBackupVersion();
} catch (e) {
logger.warn("Error checking for active key backup", e);
return null;
}
this.checkedForBackup = true;
if (backupInfo && !backupInfo.version) {
logger.warn("active backup lacks a useful 'version'; ignoring it");
}
const activeVersion = await this.getActiveBackupVersion();
if (!backupInfo) {
if (activeVersion !== null) {
logger.log("No key backup present on server: disabling key backup");
await this.disableKeyBackup();
} else {
logger.log("No key backup present on server: not enabling key backup");
}
return null;
}
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (!trustInfo.trusted) {
if (activeVersion !== null) {
logger.log("Key backup present on server but not trusted: disabling key backup");
await this.disableKeyBackup();
} else {
logger.log("Key backup present on server but not trusted: not enabling key backup");
}
} else {
if (activeVersion === null) {
logger.log(`Found usable key backup v${backupInfo.version}: enabling key backups`);
await this.enableKeyBackup(backupInfo);
} else if (activeVersion !== backupInfo.version) {
logger.log(`On backup version ${activeVersion} but found version ${backupInfo.version}: switching.`);
await this.disableKeyBackup();
await this.enableKeyBackup(backupInfo);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
// because we don't remember what backup version we uploaded keys to:
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log(`Backup version ${backupInfo.version} still current`);
}
}
return { backupInfo, trustInfo };
}
private async enableKeyBackup(backupInfo: KeyBackupInfo): Promise<void> {
// we know for certain it must be a Curve25519 key, because we have verified it and only Curve25519
// keys can be verified.
//
// we also checked it has a valid `version`.
await this.olmMachine.enableBackupV1(
(backupInfo.auth_data as Curve25519AuthData).public_key,
backupInfo.version!,
);
this.activeBackupVersion = backupInfo.version!;
this.emit(CryptoEvent.KeyBackupStatus, true);
// TODO: kick off an upload loop
}
private async disableKeyBackup(): Promise<void> {
await this.olmMachine.disableBackup();
this.activeBackupVersion = null;
this.emit(CryptoEvent.KeyBackupStatus, false);
}
private async scheduleAllGroupSessionsForBackup(): Promise<void> {
// TODO stub
}
/**
* Get information about the current key backup from the server
*
* @returns Information object from API or null if there is no active backup.
*/
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
try {
return await this.http.authedRequest<KeyBackupInfo>(
Method.Get,
"/room_keys/version",
undefined,
undefined,
{
prefix: ClientPrefix.V3,
},
);
} catch (e) {
if ((<MatrixError>e).errcode === "M_NOT_FOUND") {
return null;
} else {
throw e;
}
}
}
}
export type RustBackupCryptoEvents = CryptoEvent.KeyBackupStatus;
export type RustBackupCryptoEventMap = {
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
};

View File

@@ -42,6 +42,7 @@ import {
GeneratedSecretStorageKey,
ImportRoomKeyProgressData,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest,
} from "../crypto-api";
@@ -58,7 +59,8 @@ import { RustVerificationRequest, verificationMethodIdentifierToMethod } from ".
import { EventType, MsgType } from "../@types/event";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { RustBackupManager } from "./backup";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup";
import { TypedReEmitter } from "../ReEmitter";
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
@@ -84,8 +86,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private keyClaimManager: KeyClaimManager;
private outgoingRequestProcessor: OutgoingRequestProcessor;
private crossSigningIdentity: CrossSigningIdentity;
private readonly backupManager: RustBackupManager;
public readonly backupManager: RustBackupManager;
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this);
public constructor(
/** The `OlmMachine` from the underlying rust crypto sdk. */
@@ -114,7 +117,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.eventDecryptor = new EventDecryptor(olmMachine);
this.backupManager = new RustBackupManager(olmMachine);
this.backupManager = new RustBackupManager(olmMachine, http);
this.reemitter.reEmit(this.backupManager, [CryptoEvent.KeyBackupStatus]);
// Fire if the cross signing keys are imported from the secret storage
const onCrossSigningKeysImport = (): void => {
@@ -819,6 +824,15 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return await this.backupManager.isKeyBackupTrusted(info);
}
/**
* Force a re-check of the key backup and enable/disable it as appropriate.
*
* Implementation of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
*/
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
return await this.backupManager.checkKeyBackupAndEnable(true);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
@@ -1231,7 +1245,10 @@ class EventDecryptor {
}
}
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged;
type RustCryptoEvents =
| CryptoEvent.VerificationRequestReceived
| CryptoEvent.UserTrustStatusChanged
| RustBackupCryptoEvents;
type RustCryptoEventMap = {
/**
@@ -1243,4 +1260,4 @@ type RustCryptoEventMap = {
* Fires when the cross signing keys are imported during {@link CryptoApi#bootstrapCrossSigning}
*/
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserTrustLevel) => void;
};
} & RustBackupCryptoEventMap;