Skip to content

Commit c65e329

Browse files
authored
Deprecate MatrixClient.{prepare,create}KeyBackupVersion in favour of new CryptoApi.resetKeyBackup API (#3689)
* new resetKeyBackup API * add delete backup version test * code review * code review
1 parent 5ddd453 commit c65e329

File tree

7 files changed

+317
-22
lines changed

7 files changed

+317
-22
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 164 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { logger } from "../../../src/logger";
4040
import {
4141
Category,
4242
createClient,
43+
CryptoEvent,
4344
IClaimOTKsResult,
4445
IContent,
4546
IDownloadKeyResult,
@@ -61,9 +62,13 @@ import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
6162
import { escapeRegExp } from "../../../src/utils";
6263
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
6364
import { flushPromises } from "../../test-utils/flushPromises";
64-
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
65+
import {
66+
mockInitialApiRequests,
67+
mockSetupCrossSigningRequests,
68+
mockSetupMegolmBackupRequests,
69+
} from "../../test-utils/mockEndpoints";
6570
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
66-
import { CryptoCallbacks } from "../../../src/crypto-api";
71+
import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
6772
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
6873

6974
afterEach(() => {
@@ -2197,11 +2202,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
21972202
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
21982203
(url: string, options: RequestInit) => {
21992204
const content = JSON.parse(options.body as string);
2200-
22012205
if (content.key) {
22022206
resolve(content.key);
22032207
}
2204-
22052208
return {};
22062209
},
22072210
{ overwriteRoutes: true },
@@ -2228,6 +2231,74 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22282231
});
22292232
}
22302233

2234+
function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
2235+
return new Promise((resolve) => {
2236+
// Called when the megolm backup key is uploaded
2237+
fetchMock.put(
2238+
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
2239+
(url: string, options: RequestInit) => {
2240+
const content = JSON.parse(options.body as string);
2241+
resolve(content.encrypted);
2242+
return {};
2243+
},
2244+
{ overwriteRoutes: true },
2245+
);
2246+
});
2247+
}
2248+
2249+
/**
2250+
* Add all mocks needed to set up cross-signing, key backup, 4S and then
2251+
* configure the account to have recovery.
2252+
*
2253+
* @param backupVersion - The version of the created backup
2254+
*/
2255+
async function bootstrapSecurity(backupVersion: string): Promise<void> {
2256+
mockSetupCrossSigningRequests();
2257+
mockSetupMegolmBackupRequests(backupVersion);
2258+
2259+
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
2260+
const backupStatusUpdate = new Promise<void>((resolve) => {
2261+
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
2262+
if (enabled) {
2263+
resolve();
2264+
}
2265+
});
2266+
});
2267+
2268+
const setupPromises = [
2269+
awaitCrossSigningKeyUpload("master"),
2270+
awaitCrossSigningKeyUpload("user_signing"),
2271+
awaitCrossSigningKeyUpload("self_signing"),
2272+
awaitMegolmBackupKeyUpload(),
2273+
];
2274+
2275+
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
2276+
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
2277+
2278+
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
2279+
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({
2280+
setupNewSecretStorage: true,
2281+
createSecretStorageKey,
2282+
setupNewKeyBackup: true,
2283+
});
2284+
2285+
// Wait for the key to be uploaded in the account data
2286+
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2287+
2288+
// Return the newly created key in the sync response
2289+
sendSyncResponse(secretStorageKey);
2290+
2291+
// Wait for the cross signing keys to be uploaded
2292+
await Promise.all(setupPromises);
2293+
2294+
// wait for bootstrapSecretStorage to finished
2295+
await bootstrapPromise;
2296+
// Finally ensure backup is working
2297+
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2298+
2299+
await backupStatusUpdate;
2300+
}
2301+
22312302
/**
22322303
* Send in the sync response the provided `secretStorageKey` into the account_data field
22332304
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
@@ -2385,6 +2456,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23852456
expect(userSigningKey[secretStorageKey]).toBeDefined();
23862457
expect(selfSigningKey[secretStorageKey]).toBeDefined();
23872458
});
2459+
2460+
oldBackendOnly("should create a new megolm backup", async () => {
2461+
const backupVersion = "abc";
2462+
await bootstrapSecurity(backupVersion);
2463+
2464+
// Expect a backup to be available and used
2465+
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2466+
expect(activeBackup).toStrictEqual(backupVersion);
2467+
});
2468+
2469+
oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
2470+
// First set up 4S and key backup
2471+
const backupVersion = "1";
2472+
await bootstrapSecurity(backupVersion);
2473+
2474+
const currentVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2475+
const currentBackupKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
2476+
2477+
// we will call reset backup, it should delete the existing one, then setup a new one
2478+
// Let's mock for that
2479+
2480+
// Mock delete and replace the GET to return 404 as soon as called
2481+
const awaitDeleteCalled = new Promise<void>((resolve) => {
2482+
fetchMock.delete(
2483+
"express:/_matrix/client/v3/room_keys/version/:version",
2484+
(url: string, options: RequestInit) => {
2485+
fetchMock.get(
2486+
"path:/_matrix/client/v3/room_keys/version",
2487+
{
2488+
status: 404,
2489+
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
2490+
},
2491+
{ overwriteRoutes: true },
2492+
);
2493+
resolve();
2494+
return {};
2495+
},
2496+
{ overwriteRoutes: true },
2497+
);
2498+
});
2499+
2500+
const newVersion = "2";
2501+
fetchMock.post(
2502+
"path:/_matrix/client/v3/room_keys/version",
2503+
(url, request) => {
2504+
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
2505+
backupData.version = newVersion;
2506+
backupData.count = 0;
2507+
backupData.etag = "zer";
2508+
2509+
// update get call with new version
2510+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
2511+
overwriteRoutes: true,
2512+
});
2513+
return {
2514+
version: backupVersion,
2515+
};
2516+
},
2517+
{ overwriteRoutes: true },
2518+
);
2519+
2520+
const newBackupStatusUpdate = new Promise<void>((resolve) => {
2521+
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
2522+
if (enabled) {
2523+
resolve();
2524+
}
2525+
});
2526+
});
2527+
2528+
const newBackupUploadPromise = awaitMegolmBackupKeyUpload();
2529+
2530+
await aliceClient.getCrypto()!.resetKeyBackup();
2531+
await awaitDeleteCalled;
2532+
await newBackupStatusUpdate;
2533+
await newBackupUploadPromise;
2534+
2535+
const nextVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2536+
const nextKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
2537+
2538+
expect(nextVersion).toBeDefined();
2539+
expect(nextVersion).not.toEqual(currentVersion);
2540+
expect(nextKey).not.toEqual(currentBackupKey);
2541+
2542+
// Test deletion of the backup
2543+
await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!);
2544+
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2545+
// XXX Legacy crypto does not update 4S when deleting backup; should ensure that rust implem does it.
2546+
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull();
2547+
});
23882548
});
23892549

23902550
describe("Incoming verification in a DM", () => {

spec/test-utils/mockEndpoints.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
import fetchMock from "fetch-mock-jest";
1818

19+
import { KeyBackupInfo } from "../../src/crypto-api";
20+
1921
/**
2022
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
2123
*
@@ -56,3 +58,35 @@ export function mockSetupCrossSigningRequests(): void {
5658
{},
5759
);
5860
}
61+
62+
/**
63+
* Mock out requests to `/room_keys/version`.
64+
*
65+
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
66+
* Once the POST is done, `GET /room_keys/version` will return the posted backup
67+
* instead of 404.
68+
*
69+
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
70+
*/
71+
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
72+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
73+
status: 404,
74+
body: {
75+
errcode: "M_NOT_FOUND",
76+
error: "No current backup version",
77+
},
78+
});
79+
80+
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
81+
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
82+
backupData.version = backupVersion;
83+
backupData.count = 0;
84+
backupData.etag = "zer";
85+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
86+
overwriteRoutes: true,
87+
});
88+
return {
89+
version: backupVersion,
90+
};
91+
});
92+
}

src/client.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3270,6 +3270,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
32703270
/**
32713271
* Get information about the current key backup.
32723272
* @returns Information object from API or null
3273+
*
3274+
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
32733275
*/
32743276
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
32753277
let res: IKeyBackupInfo;
@@ -3341,6 +3343,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
33413343

33423344
/**
33433345
* Disable backing up of keys.
3346+
*
3347+
* @deprecated It should be unnecessary to disable key backup.
33443348
*/
33453349
public disableKeyBackup(): void {
33463350
if (!this.crypto) {
@@ -3360,6 +3364,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
33603364
*
33613365
* @returns Object that can be passed to createKeyBackupVersion and
33623366
* additionally has a 'recovery_key' member with the user-facing recovery key string.
3367+
*
3368+
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
33633369
*/
33643370
public async prepareKeyBackupVersion(
33653371
password?: string | Uint8Array | null,
@@ -3403,6 +3409,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
34033409
*
34043410
* @param info - Info object from prepareKeyBackupVersion
34053411
* @returns Object with 'version' param indicating the version created
3412+
*
3413+
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
34063414
*/
34073415
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
34083416
if (!this.crypto) {
@@ -3448,24 +3456,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
34483456
return res;
34493457
}
34503458

3459+
/**
3460+
* @deprecated Use {@link Crypto.CryptoApi.deleteKeyBackupVersion | `CryptoApi.deleteKeyBackupVersion`}.
3461+
*/
34513462
public async deleteKeyBackupVersion(version: string): Promise<void> {
3452-
if (!this.crypto) {
3463+
if (!this.cryptoBackend) {
34533464
throw new Error("End-to-end encryption disabled");
34543465
}
34553466

3456-
// If we're currently backing up to this backup... stop.
3457-
// (We start using it automatically in createKeyBackupVersion
3458-
// so this is symmetrical).
3459-
// TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version.
3460-
if (this.crypto.backupManager.version) {
3461-
this.crypto.backupManager.disableKeyBackup();
3462-
}
3463-
3464-
const path = utils.encodeUri("/room_keys/version/$version", {
3465-
$version: version,
3466-
});
3467-
3468-
await this.http.authedRequest(Method.Delete, path, undefined, undefined, { prefix: ClientPrefix.V3 });
3467+
await this.cryptoBackend.deleteKeyBackupVersion(version);
34693468
}
34703469

34713470
private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath;

src/crypto-api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,24 @@ export interface CryptoApi {
382382
* and trust information (as returned by {@link isKeyBackupTrusted}).
383383
*/
384384
checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null>;
385+
386+
/**
387+
* Creates a new key backup version.
388+
*
389+
* If there are existing backups they will be replaced.
390+
*
391+
* The decryption key will be saved in Secret Storage (the {@link SecretStorageCallbacks.getSecretStorageKey} Crypto
392+
* callback will be called)
393+
* and the backup engine will be started.
394+
*/
395+
resetKeyBackup(): Promise<void>;
396+
397+
/**
398+
* Deletes the given key backup.
399+
*
400+
* @param version - The backup version to delete.
401+
*/
402+
deleteKeyBackupVersion(version: string): Promise<void>;
385403
}
386404

387405
/**

0 commit comments

Comments
 (0)