From d030c83cee3524a8f6f221e2983f7537aaa23cb6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:09:17 +0000 Subject: [PATCH] Groundwork for supporting migration from libolm to rust crypto. (#3977) * `getOwnDeviceKeys`: use `olmMachine.identityKeys` This is simpler, and doesn't rely on us having done a device query to work. * Factor out `requestKeyBackupVersion` utility * Factor out `makeMatrixHttpApi` function * Convert `initRustCrypto` to take a params object * Improve logging in startup ... to help figure out what is taking so long. --- spec/integ/crypto/crypto.spec.ts | 14 +--- spec/unit/rust-crypto/rust-crypto.spec.ts | 89 ++++++++++++---------- src/client.ts | 22 +++--- src/rust-crypto/backup.ts | 35 ++++----- src/rust-crypto/index.ts | 93 ++++++++++++++++------- src/rust-crypto/rust-crypto.ts | 14 +--- 6 files changed, 149 insertions(+), 118 deletions(-) diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index c6a916bee1b..bac0c46315c 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -398,17 +398,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices"); }); - it("CryptoAPI.getOwnedDeviceKeys returns the correct values", async () => { - const homeserverUrl = aliceClient.getHomeserverUrl(); - - keyResponder = new E2EKeyResponder(homeserverUrl); - await startClientAndAwaitFirstSync(); - keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); - + it("CryptoAPI.getOwnDeviceKeys returns plausible values", async () => { const deviceKeys = await aliceClient.getCrypto()!.getOwnDeviceKeys(); - - expect(deviceKeys.curve25519).toEqual(keyReceiver.getDeviceKey()); - expect(deviceKeys.ed25519).toEqual(keyReceiver.getSigningKey()); + // We just check for a 43-character base64 string + expect(deviceKeys.curve25519).toMatch(/^[A-Za-z0-9+/]{43}$/); + expect(deviceKeys.ed25519).toMatch(/^[A-Za-z0-9+/]{43}$/); }); it("Alice receives a megolm message", async () => { diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 6cc7112635d..c9146c24677 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -84,16 +84,16 @@ describe("initRustCrypto", () => { const testOlmMachine = makeTestOlmMachine(); jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); - await initRustCrypto( + await initRustCrypto({ logger, - {} as MatrixClient["http"], - TEST_USER, - TEST_DEVICE_ID, - {} as ServerSideSecretStorage, - {} as CryptoCallbacks, - "storePrefix", - "storePassphrase", - ); + http: {} as MatrixClient["http"], + userId: TEST_USER, + deviceId: TEST_DEVICE_ID, + secretStorage: {} as ServerSideSecretStorage, + cryptoCallbacks: {} as CryptoCallbacks, + storePrefix: "storePrefix", + storePassphrase: "storePassphrase", + }); expect(OlmMachine.initialize).toHaveBeenCalledWith( expect.anything(), @@ -107,16 +107,16 @@ describe("initRustCrypto", () => { const testOlmMachine = makeTestOlmMachine(); jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); - await initRustCrypto( + await initRustCrypto({ logger, - {} as MatrixClient["http"], - TEST_USER, - TEST_DEVICE_ID, - {} as ServerSideSecretStorage, - {} as CryptoCallbacks, - null, - "storePassphrase", - ); + http: {} as MatrixClient["http"], + userId: TEST_USER, + deviceId: TEST_DEVICE_ID, + secretStorage: {} as ServerSideSecretStorage, + cryptoCallbacks: {} as CryptoCallbacks, + storePrefix: null, + storePassphrase: "storePassphrase", + }); expect(OlmMachine.initialize).toHaveBeenCalledWith(expect.anything(), expect.anything(), undefined, undefined); }); @@ -125,16 +125,16 @@ describe("initRustCrypto", () => { const testOlmMachine = makeTestOlmMachine() as OlmMachine; jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); - await initRustCrypto( + await initRustCrypto({ logger, - {} as MatrixClient["http"], - TEST_USER, - TEST_DEVICE_ID, - {} as ServerSideSecretStorage, - {} as CryptoCallbacks, - "storePrefix", - "storePassphrase", - ); + http: {} as MatrixClient["http"], + userId: TEST_USER, + deviceId: TEST_DEVICE_ID, + secretStorage: {} as ServerSideSecretStorage, + cryptoCallbacks: {} as CryptoCallbacks, + storePrefix: "storePrefix", + storePassphrase: "storePassphrase", + }); expect(testOlmMachine.getSecretsFromInbox).toHaveBeenCalledWith("m.megolm_backup.v1"); }); @@ -823,11 +823,6 @@ describe("RustCrypto", () => { it("should wait for a keys/query before returning devices", async () => { jest.useFakeTimers(); - const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter(), { - baseUrl: "http://server/", - prefix: "", - onlyData: true, - }); fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} }); fetchMock.post("path:/_matrix/client/v3/keys/query", { device_keys: { @@ -837,7 +832,7 @@ describe("RustCrypto", () => { }, }); - const rustCrypto = await makeTestRustCrypto(mockHttpApi, testData.TEST_USER_ID); + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), testData.TEST_USER_ID); // an attempt to fetch the device list should block const devicesPromise = rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]); @@ -963,12 +958,6 @@ describe("RustCrypto", () => { // Return the key backup fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); - const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter(), { - baseUrl: "http://server/", - prefix: "", - onlyData: true, - }); - const olmMachine = { getIdentity: jest.fn(), // Force the backup to be trusted by the olmMachine @@ -981,7 +970,7 @@ describe("RustCrypto", () => { const rustCrypto = new RustCrypto( logger, olmMachine, - mockHttpApi, + makeMatrixHttpApi(), testData.TEST_USER_ID, testData.TEST_DEVICE_ID, {} as ServerSideSecretStorage, @@ -1040,6 +1029,15 @@ describe("RustCrypto", () => { }); }); +/** Build a MatrixHttpApi instance */ +function makeMatrixHttpApi(): MatrixHttpApi { + return new MatrixHttpApi(new TypedEventEmitter(), { + baseUrl: "http://server/", + prefix: "", + onlyData: true, + }); +} + /** build a basic RustCrypto instance for testing * * just provides default arguments for initRustCrypto() @@ -1051,7 +1049,16 @@ async function makeTestRustCrypto( secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage, cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks, ): Promise { - return await initRustCrypto(logger, http, userId, deviceId, secretStorage, cryptoCallbacks, null, undefined); + return await initRustCrypto({ + logger, + http, + userId, + deviceId, + secretStorage, + cryptoCallbacks, + storePrefix: null, + storePassphrase: undefined, + }); } /** emulate account data, storing in memory diff --git a/src/client.ts b/src/client.ts index 73ed7bbba44..27e70c43520 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2316,17 +2316,19 @@ export class MatrixClient extends TypedEventEmitter { - try { - return await this.http.authedRequest( - Method.Get, - "/room_keys/version", - undefined, - undefined, - { - prefix: ClientPrefix.V3, - }, - ); - } catch (e) { - if ((e).errcode === "M_NOT_FOUND") { - return null; - } else { - throw e; - } - } + return await requestKeyBackupVersion(this.http); } /** @@ -567,6 +552,22 @@ export class RustBackupDecryptor implements BackupDecryptor { } } +export async function requestKeyBackupVersion( + http: MatrixHttpApi, +): Promise { + try { + return await http.authedRequest(Method.Get, "/room_keys/version", undefined, undefined, { + prefix: ClientPrefix.V3, + }); + } catch (e) { + if ((e).errcode === "M_NOT_FOUND") { + return null; + } else { + throw e; + } + } +} + export type RustBackupCryptoEvents = | CryptoEvent.KeyBackupStatus | CryptoEvent.KeyBackupSessionsRemaining diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index ea0bcd247a4..15778092b51 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -25,22 +25,70 @@ import { Logger } from "../logger"; /** * Create a new `RustCrypto` implementation * - * @param logger - A `Logger` instance that will be used for debug output. - * @param http - Low-level HTTP interface: used to make outgoing requests required by the rust SDK. - * We expect it to set the access token, etc. - * @param userId - The local user's User ID. - * @param deviceId - The local user's Device ID. - * @param secretStorage - Interface to server-side secret storage. - * @param cryptoCallbacks - Crypto callbacks provided by the application - * @param storePrefix - the prefix to use on the indexeddbs created by rust-crypto. - * If `null`, a memory store will be used. - * @param storePassphrase - a passphrase to use to encrypt the indexeddbs created by rust-crypto. - * Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs - * will be unencrypted. - * + * @param args - Parameter object * @internal */ -export async function initRustCrypto( +export async function initRustCrypto(args: { + /** A `Logger` instance that will be used for debug output. */ + logger: Logger; + + /** + * Low-level HTTP interface: used to make outgoing requests required by the rust SDK. + * We expect it to set the access token, etc. + */ + http: MatrixHttpApi; + + /** The local user's User ID. */ + userId: string; + + /** The local user's Device ID. */ + deviceId: string; + + /** Interface to server-side secret storage. */ + secretStorage: ServerSideSecretStorage; + + /** Crypto callbacks provided by the application. */ + cryptoCallbacks: ICryptoCallbacks; + + /** + * The prefix to use on the indexeddbs created by rust-crypto. + * If `null`, a memory store will be used. + */ + storePrefix: string | null; + + /** + * A passphrase to use to encrypt the indexeddbs created by rust-crypto. + * + * Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs + * will be unencrypted. + */ + storePassphrase?: string; +}): Promise { + const { logger } = args; + + // initialise the rust matrix-sdk-crypto-wasm, if it hasn't already been done + logger.debug("Initialising Rust crypto-sdk WASM artifact"); + await RustSdkCryptoJs.initAsync(); + + // enable tracing in the rust-sdk + new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn(); + + const rustCrypto = await initOlmMachine( + logger, + args.http, + args.userId, + args.deviceId, + args.secretStorage, + args.cryptoCallbacks, + args.storePrefix, + args.storePassphrase, + ); + + logger.debug("Completed rust crypto-sdk setup"); + return rustCrypto; +} + +async function initOlmMachine( logger: Logger, http: MatrixHttpApi, userId: string, @@ -50,20 +98,10 @@ export async function initRustCrypto( storePrefix: string | null, storePassphrase: string | undefined, ): Promise { - // initialise the rust matrix-sdk-crypto-wasm, if it hasn't already been done - await RustSdkCryptoJs.initAsync(); - - // enable tracing in the rust-sdk - new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn(); - - const u = new RustSdkCryptoJs.UserId(userId); - const d = new RustSdkCryptoJs.DeviceId(deviceId); - logger.info("Init OlmMachine"); - - // TODO: use the pickle key for the passphrase + logger.debug("Init OlmMachine"); const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize( - u, - d, + new RustSdkCryptoJs.UserId(userId), + new RustSdkCryptoJs.DeviceId(deviceId), storePrefix ?? undefined, (storePrefix && storePassphrase) ?? undefined, ); @@ -101,6 +139,5 @@ export async function initRustCrypto( // XXX: find a less hacky way to do this. await olmMachine.outgoingRequests(); - logger.info("Completed rust crypto-sdk setup"); return rustCrypto; } diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 67e44018fcd..d63b7d02c92 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -315,18 +315,8 @@ export class RustCrypto extends TypedEventEmitter { - const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice( - this.olmMachine.userId, - this.olmMachine.deviceId, - ); - // could be undefined if there is no such algorithm for that device. - if (device.curve25519Key && device.ed25519Key) { - return { - ed25519: device.ed25519Key.toBase64(), - curve25519: device.curve25519Key.toBase64(), - }; - } - throw new Error("Device keys not found"); + const keys = this.olmMachine.identityKeys; + return { ed25519: keys.ed25519.toBase64(), curve25519: keys.curve25519.toBase64() }; } public prepareToEncrypt(room: Room): void {