Skip to content

Commit 98c1b34

Browse files
committed
new resetKeyBackup API
1 parent 5ddd453 commit 98c1b34

File tree

7 files changed

+302
-22
lines changed

7 files changed

+302
-22
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 152 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,68 @@ 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+
async function bootstrapSecurity(backupVersion: string): Promise<void> {
2250+
mockSetupCrossSigningRequests();
2251+
mockSetupMegolmBackupRequests(backupVersion);
2252+
2253+
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
2254+
const backupStatusUpdate = new Promise<void>((resolve) => {
2255+
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
2256+
if (enabled) {
2257+
resolve();
2258+
}
2259+
});
2260+
});
2261+
2262+
const setupPromises = [
2263+
awaitCrossSigningKeyUpload("master"),
2264+
awaitCrossSigningKeyUpload("user_signing"),
2265+
awaitCrossSigningKeyUpload("self_signing"),
2266+
awaitMegolmBackupKeyUpload(),
2267+
];
2268+
2269+
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
2270+
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
2271+
2272+
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
2273+
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({
2274+
setupNewSecretStorage: true,
2275+
createSecretStorageKey,
2276+
setupNewKeyBackup: true,
2277+
});
2278+
2279+
// Wait for the key to be uploaded in the account data
2280+
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2281+
2282+
// Return the newly created key in the sync response
2283+
sendSyncResponse(secretStorageKey);
2284+
2285+
// Wait for the cross signing keys to be uploaded
2286+
await Promise.all(setupPromises);
2287+
2288+
// wait for bootstrapSecretStorage to finished
2289+
await bootstrapPromise;
2290+
// Finally ensure backup is working
2291+
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2292+
2293+
await backupStatusUpdate;
2294+
}
2295+
22312296
/**
22322297
* Send in the sync response the provided `secretStorageKey` into the account_data field
22332298
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
@@ -2385,6 +2450,89 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23852450
expect(userSigningKey[secretStorageKey]).toBeDefined();
23862451
expect(selfSigningKey[secretStorageKey]).toBeDefined();
23872452
});
2453+
2454+
oldBackendOnly("should create a new megolm backup", async () => {
2455+
const backupVersion = "abc";
2456+
await bootstrapSecurity(backupVersion);
2457+
2458+
// Expect a backup to be available and used
2459+
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2460+
expect(activeBackup).toStrictEqual(backupVersion);
2461+
});
2462+
2463+
oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
2464+
// First set up recovery
2465+
const backupVersion = "1";
2466+
await bootstrapSecurity(backupVersion);
2467+
2468+
const currentVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2469+
const currentBackupKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
2470+
2471+
// we will call reset backup, it should delete the existing one, then setup a new one
2472+
// Let's mock for that
2473+
2474+
// Mock delete and replace the GET to return 404 as soon as called
2475+
const awaitDeleteCalled = new Promise<void>((resolve) => {
2476+
fetchMock.delete(
2477+
"express:/_matrix/client/v3/room_keys/version/:version",
2478+
(url: string, options: RequestInit) => {
2479+
fetchMock.get(
2480+
"path:/_matrix/client/v3/room_keys/version",
2481+
{
2482+
status: 404,
2483+
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
2484+
},
2485+
{ overwriteRoutes: true },
2486+
);
2487+
resolve();
2488+
return {};
2489+
},
2490+
{ overwriteRoutes: true },
2491+
);
2492+
});
2493+
2494+
const newVersion = "2";
2495+
fetchMock.post(
2496+
"path:/_matrix/client/v3/room_keys/version",
2497+
(url, request) => {
2498+
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
2499+
backupData.version = newVersion;
2500+
backupData.count = 0;
2501+
backupData.etag = "zer";
2502+
2503+
// update get call with new version
2504+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
2505+
overwriteRoutes: true,
2506+
});
2507+
return {
2508+
version: backupVersion,
2509+
};
2510+
},
2511+
{ overwriteRoutes: true },
2512+
);
2513+
2514+
const newBackupStatusUpdate = new Promise<void>((resolve) => {
2515+
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
2516+
if (enabled) {
2517+
resolve();
2518+
}
2519+
});
2520+
});
2521+
2522+
const new4SUpload = awaitMegolmBackupKeyUpload();
2523+
2524+
await aliceClient.getCrypto()!.resetKeyBackup();
2525+
await awaitDeleteCalled;
2526+
await newBackupStatusUpdate;
2527+
await new4SUpload;
2528+
2529+
const nextVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
2530+
const nextKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
2531+
2532+
expect(nextVersion).toBeDefined();
2533+
expect(nextVersion).not.toEqual(currentVersion);
2534+
expect(nextKey).not.toEqual(currentBackupKey);
2535+
});
23882536
});
23892537

23902538
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: 14 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,9 @@ 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+
*
3414+
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
34063415
*/
34073416
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
34083417
if (!this.crypto) {
@@ -3448,24 +3457,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
34483457
return res;
34493458
}
34503459

3460+
/**
3461+
* @deprecated Use {@link Crypto.CryptoApi.deleteKeyBackupVersion | `CryptoApi.deleteKeyBackupVersion`}.
3462+
*/
34513463
public async deleteKeyBackupVersion(version: string): Promise<void> {
3452-
if (!this.crypto) {
3464+
if (!this.cryptoBackend) {
34533465
throw new Error("End-to-end encryption disabled");
34543466
}
34553467

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 });
3468+
await this.cryptoBackend.deleteKeyBackupVersion(version);
34693469
}
34703470

34713471
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
@@ -233,6 +233,24 @@ export interface CryptoApi {
233233
*/
234234
bootstrapSecretStorage(opts: CreateSecretStorageOpts): Promise<void>;
235235

236+
/**
237+
* Creates a new key backup version.
238+
*
239+
* If there are existing backups they will be replaced.
240+
*
241+
* The decryption key will be saved in Secret Storage (the `SecretStorageCallbacks.getSecretStorageKey` Crypto
242+
* callback will be called)
243+
* and the backup engine will be started.
244+
*/
245+
resetKeyBackup(): Promise<void>;
246+
247+
/**
248+
* Deletes the given key backup.
249+
*
250+
* @param version - The backup version to delete.
251+
*/
252+
deleteKeyBackupVersion(version: string): Promise<void>;
253+
236254
/**
237255
* Get the status of our cross-signing keys.
238256
*

src/crypto/backup.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
2525
import { DeviceInfo } from "./deviceinfo";
2626
import { DeviceTrustLevel } from "./CrossSigning";
2727
import { keyFromPassphrase } from "./key_passphrase";
28-
import { safeSet, sleep } from "../utils";
28+
import { encodeUri, safeSet, sleep } from "../utils";
2929
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
3030
import { encodeRecoveryKey } from "./recoverykey";
3131
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
@@ -39,7 +39,7 @@ import {
3939
import { UnstableValue } from "../NamespacedValue";
4040
import { CryptoEvent } from "./index";
4141
import { crypto } from "./crypto";
42-
import { HTTPError, MatrixError } from "../http-api";
42+
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api";
4343
import { BackupTrustInfo } from "../crypto-api/keybackup";
4444

4545
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -224,6 +224,33 @@ export class BackupManager {
224224
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
225225
}
226226

227+
/**
228+
* Deletes all key backups.
229+
*
230+
* Will call the API to delete active backup until there is no more present.
231+
*/
232+
public async deleteAllKeyBackupVersions(): Promise<void> {
233+
// there could be several backup versions, delete all to be safe.
234+
let current = (await this.baseApis.getKeyBackupVersion())?.version ?? null;
235+
while (current != null) {
236+
await this.deleteKeyBackupVersion(current);
237+
this.disableKeyBackup();
238+
current = (await this.baseApis.getKeyBackupVersion())?.version ?? null;
239+
}
240+
}
241+
242+
/**
243+
* Deletes the given key backup.
244+
*
245+
* @param version - The backup version to delete.
246+
*/
247+
public async deleteKeyBackupVersion(version: string): Promise<void> {
248+
const path = encodeUri("/room_keys/version/$version", { $version: version });
249+
await this.baseApis.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
250+
prefix: ClientPrefix.V3,
251+
});
252+
}
253+
227254
/**
228255
* Check the server for an active key backup and
229256
* if one is present and has a valid signature from
@@ -333,7 +360,7 @@ export class BackupManager {
333360
};
334361

335362
if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) {
336-
logger.info("Key backup is absent or missing required data");
363+
logger.info(`Key backup is absent or missing required data: ${JSON.stringify(backupInfo)}`);
337364
return ret;
338365
}
339366

0 commit comments

Comments
 (0)