Skip to content

Commit 3f7af18

Browse files
Implement CryptoApi.checkKeyBackupAndEnable (#3633)
* Implement `CryptoApi.checkKeyBackup` * Deprecate `MatrixClient.enableKeyBackup`. * fix integ test * more tests --------- Co-authored-by: valere <[email protected]>
1 parent 16ddcb0 commit 3f7af18

File tree

8 files changed

+343
-18
lines changed

8 files changed

+343
-18
lines changed

spec/integ/crypto/megolm-backup.spec.ts

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
176176
expect(event.getContent()).toEqual("testytest");
177177
});
178178

179-
oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () {
179+
it("getActiveSessionBackupVersion() should give correct result", async function () {
180180
// 404 means that there is no active backup
181181
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
182182

@@ -187,7 +187,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
187187
// tell Alice to trust the dummy device that signed the backup
188188
await waitForDeviceList();
189189
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
190-
await aliceClient.checkKeyBackup();
190+
await aliceCrypto.checkKeyBackupAndEnable();
191191

192192
// At this point there is no backup
193193
let backupStatus: string | null;
@@ -201,9 +201,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
201201
overwriteRoutes: true,
202202
});
203203

204-
const checked = await aliceClient.checkKeyBackup();
204+
const checked = await aliceCrypto.checkKeyBackupAndEnable();
205205
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
206-
expect(checked?.trustInfo?.usable).toBeFalsy();
206+
expect(checked?.trustInfo?.trusted).toBeFalsy();
207207

208208
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
209209
expect(backupStatus).toBeNull();
@@ -222,8 +222,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
222222
});
223223
});
224224

225-
const validCheck = await aliceClient.checkKeyBackup();
226-
expect(validCheck?.trustInfo?.usable).toStrictEqual(true);
225+
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
226+
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
227227

228228
await backupPromise;
229229

@@ -286,6 +286,128 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
286286
});
287287
});
288288

289+
describe("checkKeyBackupAndEnable", () => {
290+
it("enables a backup signed by a trusted device", async () => {
291+
aliceClient = await initTestClient();
292+
const aliceCrypto = aliceClient.getCrypto()!;
293+
294+
// tell Alice to trust the dummy device that signed the backup
295+
await aliceClient.startClient();
296+
await waitForDeviceList();
297+
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
298+
299+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
300+
301+
const result = await aliceCrypto.checkKeyBackupAndEnable();
302+
expect(result).toBeTruthy();
303+
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
304+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
305+
});
306+
307+
it("does not enable a backup signed by an untrusted device", async () => {
308+
aliceClient = await initTestClient();
309+
const aliceCrypto = aliceClient.getCrypto()!;
310+
311+
// download the device list, to match the trusted case
312+
await aliceClient.startClient();
313+
await waitForDeviceList();
314+
315+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
316+
317+
const result = await aliceCrypto.checkKeyBackupAndEnable();
318+
expect(result).toBeTruthy();
319+
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
320+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
321+
});
322+
323+
it("disables backup when a new untrusted backup is available", async () => {
324+
aliceClient = await initTestClient();
325+
const aliceCrypto = aliceClient.getCrypto()!;
326+
327+
// tell Alice to trust the dummy device that signed the backup
328+
await aliceClient.startClient();
329+
await waitForDeviceList();
330+
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
331+
332+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
333+
334+
const result = await aliceCrypto.checkKeyBackupAndEnable();
335+
expect(result).toBeTruthy();
336+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
337+
338+
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
339+
delete unsignedBackup.auth_data.signatures;
340+
unsignedBackup.version = "2";
341+
342+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
343+
overwriteRoutes: true,
344+
});
345+
346+
await aliceCrypto.checkKeyBackupAndEnable();
347+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
348+
});
349+
350+
it("switches backup when a new trusted backup is available", async () => {
351+
aliceClient = await initTestClient();
352+
const aliceCrypto = aliceClient.getCrypto()!;
353+
354+
// tell Alice to trust the dummy device that signed the backup
355+
await aliceClient.startClient();
356+
await waitForDeviceList();
357+
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
358+
359+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
360+
361+
const result = await aliceCrypto.checkKeyBackupAndEnable();
362+
expect(result).toBeTruthy();
363+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
364+
365+
const newBackupVersion = "2";
366+
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
367+
unsignedBackup.version = newBackupVersion;
368+
369+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
370+
overwriteRoutes: true,
371+
});
372+
373+
await aliceCrypto.checkKeyBackupAndEnable();
374+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
375+
});
376+
377+
it("Disables when backup is deleted", async () => {
378+
aliceClient = await initTestClient();
379+
const aliceCrypto = aliceClient.getCrypto()!;
380+
381+
// tell Alice to trust the dummy device that signed the backup
382+
await aliceClient.startClient();
383+
await waitForDeviceList();
384+
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
385+
386+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
387+
388+
const result = await aliceCrypto.checkKeyBackupAndEnable();
389+
expect(result).toBeTruthy();
390+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
391+
392+
fetchMock.get(
393+
"path:/_matrix/client/v3/room_keys/version",
394+
{
395+
status: 404,
396+
body: {
397+
errcode: "M_NOT_FOUND",
398+
error: "No backup found",
399+
},
400+
},
401+
{
402+
overwriteRoutes: true,
403+
},
404+
);
405+
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
406+
expect(noResult).toBeNull();
407+
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
408+
});
409+
});
410+
289411
/** make sure that the client knows about the dummy device */
290412
async function waitForDeviceList(): Promise<void> {
291413
// Completing the initial sync will make the device list download outdated device lists (of which our own

spec/test-utils/test-data/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,4 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
115115
}
116116
}
117117
}
118-
};
118+
};

src/client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2252,6 +2252,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
22522252
this.reEmitter.reEmit(rustCrypto, [
22532253
CryptoEvent.VerificationRequestReceived,
22542254
CryptoEvent.UserTrustStatusChanged,
2255+
CryptoEvent.KeyBackupStatus,
22552256
]);
22562257
}
22572258

@@ -3251,6 +3252,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
32513252
* getKeyBackupVersion) in backupInfo and
32523253
* trust information (as returned by isKeyBackupTrusted)
32533254
* in trustInfo.
3255+
*
3256+
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
32543257
*/
32553258
public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
32563259
if (!this.crypto) {
@@ -3320,6 +3323,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
33203323
*
33213324
* @param info - Backup information object as returned by getKeyBackupVersion
33223325
* @returns Promise which resolves when complete.
3326+
*
3327+
* @deprecated Do not call this directly. Instead call {@link CryptoApi.checkKeyBackupAndEnable}.
33233328
*/
33243329
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
33253330
if (!this.crypto) {

src/crypto-api.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { DeviceMap } from "./models/device";
2020
import { UIAuthCallback } from "./interactive-auth";
2121
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
2222
import { VerificationRequest } from "./crypto-api/verification";
23-
import { BackupTrustInfo, KeyBackupInfo } from "./crypto-api/keybackup";
23+
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
2424
import { ISignatures } from "./@types/signed";
2525

2626
/**
@@ -350,6 +350,17 @@ export interface CryptoApi {
350350
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
351351
*/
352352
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
353+
354+
/**
355+
* Force a re-check of the key backup and enable/disable it as appropriate.
356+
*
357+
* Fetches the current backup information from the server. If there is a backup, and it is trusted, starts
358+
* backing up to it; otherwise, disables backups.
359+
*
360+
* @returns `null` if there is no backup on the server. Otherwise, data on the backup as returned by the server,
361+
* and trust information (as returned by {@link isKeyBackupTrusted}).
362+
*/
363+
checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null>;
353364
}
354365

355366
/**

src/crypto-api/keybackup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ export interface BackupTrustInfo {
6060
*/
6161
readonly matchesDecryptionKey: boolean;
6262
}
63+
64+
/**
65+
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
66+
*/
67+
export interface KeyBackupCheck {
68+
backupInfo: KeyBackupInfo;
69+
trustInfo: BackupTrustInfo;
70+
}

src/crypto/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
CrossSigningStatus,
9393
DeviceVerificationStatus,
9494
ImportRoomKeysOpts,
95+
KeyBackupCheck,
9596
KeyBackupInfo,
9697
VerificationRequest as CryptoApiVerificationRequest,
9798
} from "../crypto-api";
@@ -1304,6 +1305,20 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
13041305
return backupTrustInfoFromLegacyTrustInfo(trustInfo);
13051306
}
13061307

1308+
/**
1309+
* Force a re-check of the key backup and enable/disable it as appropriate.
1310+
*
1311+
* Implementation of {@link CryptoApi.checkKeyBackupAndEnable}.
1312+
*/
1313+
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
1314+
const checkResult = await this.backupManager.checkKeyBackup();
1315+
if (!checkResult || !checkResult.backupInfo) return null;
1316+
return {
1317+
backupInfo: checkResult.backupInfo,
1318+
trustInfo: backupTrustInfoFromLegacyTrustInfo(checkResult.trustInfo),
1319+
};
1320+
}
1321+
13071322
/**
13081323
* Checks that a given cross-signing private key matches a given public key.
13091324
* This can be used by the getCrossSigningKey callback to verify that the

0 commit comments

Comments
 (0)