Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate MatrixClient.{prepare,create}KeyBackupVersion in favour of new CryptoApi.resetKeyBackup API #3689

Merged
merged 4 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 164 additions & 4 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { logger } from "../../../src/logger";
import {
Category,
createClient,
CryptoEvent,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
Expand All @@ -61,9 +62,13 @@ import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { escapeRegExp } from "../../../src/utils";
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
import { flushPromises } from "../../test-utils/flushPromises";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import {
mockInitialApiRequests,
mockSetupCrossSigningRequests,
mockSetupMegolmBackupRequests,
} from "../../test-utils/mockEndpoints";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { CryptoCallbacks } from "../../../src/crypto-api";
import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";

afterEach(() => {
Expand Down Expand Up @@ -2197,11 +2202,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);

if (content.key) {
resolve(content.key);
}

return {};
},
{ overwriteRoutes: true },
Expand All @@ -2228,6 +2231,74 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
}

function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
return new Promise((resolve) => {
// Called when the megolm backup key is uploaded
fetchMock.put(
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
resolve(content.encrypted);
return {};
},
{ overwriteRoutes: true },
);
});
}

/**
* Add all mocks needed to set up cross-signing, key backup, 4S and then
* configure the account to have recovery.
*
* @param backupVersion - The version of the created backup
*/
async function bootstrapSecurity(backupVersion: string): Promise<void> {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests(backupVersion);

// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
const backupStatusUpdate = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});

const setupPromises = [
awaitCrossSigningKeyUpload("master"),
awaitCrossSigningKeyUpload("user_signing"),
awaitCrossSigningKeyUpload("self_signing"),
awaitMegolmBackupKeyUpload(),
];

// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
await aliceClient.getCrypto()!.bootstrapCrossSigning({});

// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey,
setupNewKeyBackup: true,
});

// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);

// Wait for the cross signing keys to be uploaded
await Promise.all(setupPromises);

// wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Finally ensure backup is working
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();

await backupStatusUpdate;
}

/**
* Send in the sync response the provided `secretStorageKey` into the account_data field
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
Expand Down Expand Up @@ -2385,6 +2456,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(userSigningKey[secretStorageKey]).toBeDefined();
expect(selfSigningKey[secretStorageKey]).toBeDefined();
});

oldBackendOnly("should create a new megolm backup", async () => {
const backupVersion = "abc";
await bootstrapSecurity(backupVersion);

// Expect a backup to be available and used
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
expect(activeBackup).toStrictEqual(backupVersion);
});

oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
// First set up 4S and key backup
const backupVersion = "1";
await bootstrapSecurity(backupVersion);

const currentVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
const currentBackupKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();

// we will call reset backup, it should delete the existing one, then setup a new one
// Let's mock for that

// Mock delete and replace the GET to return 404 as soon as called
const awaitDeleteCalled = new Promise<void>((resolve) => {
fetchMock.delete(
"express:/_matrix/client/v3/room_keys/version/:version",
(url: string, options: RequestInit) => {
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
},
{ overwriteRoutes: true },
);
resolve();
return {};
},
{ overwriteRoutes: true },
);
});

const newVersion = "2";
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
backupData.version = newVersion;
backupData.count = 0;
backupData.etag = "zer";

// update get call with new version
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
},
{ overwriteRoutes: true },
);

const newBackupStatusUpdate = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});

const newBackupUploadPromise = awaitMegolmBackupKeyUpload();

await aliceClient.getCrypto()!.resetKeyBackup();
await awaitDeleteCalled;
await newBackupStatusUpdate;
await newBackupUploadPromise;

const nextVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
const nextKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();

expect(nextVersion).toBeDefined();
expect(nextVersion).not.toEqual(currentVersion);
expect(nextKey).not.toEqual(currentBackupKey);

// Test deletion of the backup
await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// XXX Legacy crypto does not update 4S when deleting backup; should ensure that rust implem does it.
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull();
});
});

describe("Incoming verification in a DM", () => {
Expand Down
34 changes: 34 additions & 0 deletions spec/test-utils/mockEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ limitations under the License.

import fetchMock from "fetch-mock-jest";

import { KeyBackupInfo } from "../../src/crypto-api";

/**
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
*
Expand Down Expand Up @@ -56,3 +58,35 @@ export function mockSetupCrossSigningRequests(): void {
{},
);
}

/**
* Mock out requests to `/room_keys/version`.
*
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
* Once the POST is done, `GET /room_keys/version` will return the posted backup
* instead of 404.
*
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
});

fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
}
27 changes: 13 additions & 14 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3270,6 +3270,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Get information about the current key backup.
* @returns Information object from API or null
*
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
let res: IKeyBackupInfo;
Expand Down Expand Up @@ -3341,6 +3343,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa

/**
* Disable backing up of keys.
*
* @deprecated It should be unnecessary to disable key backup.
*/
public disableKeyBackup(): void {
if (!this.crypto) {
Expand All @@ -3360,6 +3364,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns Object that can be passed to createKeyBackupVersion and
* additionally has a 'recovery_key' member with the user-facing recovery key string.
*
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
*/
public async prepareKeyBackupVersion(
password?: string | Uint8Array | null,
Expand Down Expand Up @@ -3403,6 +3409,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param info - Info object from prepareKeyBackupVersion
* @returns Object with 'version' param indicating the version created
*
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
*/
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
if (!this.crypto) {
Expand Down Expand Up @@ -3448,24 +3456,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return res;
}

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

// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeyBackupVersion
// so this is symmetrical).
// TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version.
if (this.crypto.backupManager.version) {
this.crypto.backupManager.disableKeyBackup();
}

const path = utils.encodeUri("/room_keys/version/$version", {
$version: version,
});

await this.http.authedRequest(Method.Delete, path, undefined, undefined, { prefix: ClientPrefix.V3 });
await this.cryptoBackend.deleteKeyBackupVersion(version);
}

private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath;
Expand Down
18 changes: 18 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,24 @@ export interface CryptoApi {
* and trust information (as returned by {@link isKeyBackupTrusted}).
*/
checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null>;

/**
* Creates a new key backup version.
*
* If there are existing backups they will be replaced.
*
* The decryption key will be saved in Secret Storage (the {@link SecretStorageCallbacks.getSecretStorageKey} Crypto
* callback will be called)
* and the backup engine will be started.
*/
resetKeyBackup(): Promise<void>;

/**
* Deletes the given key backup.
*
* @param version - The backup version to delete.
*/
deleteKeyBackupVersion(version: string): Promise<void>;
}

/**
Expand Down
Loading
Loading