1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

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.
This commit is contained in:
Richard van der Hoff
2024-01-03 11:09:17 +00:00
committed by GitHub
parent c115e055c6
commit d030c83cee
6 changed files with 149 additions and 118 deletions

View File

@ -398,17 +398,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices"); expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices");
}); });
it("CryptoAPI.getOwnedDeviceKeys returns the correct values", async () => { it("CryptoAPI.getOwnDeviceKeys returns plausible values", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
keyResponder = new E2EKeyResponder(homeserverUrl);
await startClientAndAwaitFirstSync();
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
const deviceKeys = await aliceClient.getCrypto()!.getOwnDeviceKeys(); const deviceKeys = await aliceClient.getCrypto()!.getOwnDeviceKeys();
// We just check for a 43-character base64 string
expect(deviceKeys.curve25519).toEqual(keyReceiver.getDeviceKey()); expect(deviceKeys.curve25519).toMatch(/^[A-Za-z0-9+/]{43}$/);
expect(deviceKeys.ed25519).toEqual(keyReceiver.getSigningKey()); expect(deviceKeys.ed25519).toMatch(/^[A-Za-z0-9+/]{43}$/);
}); });
it("Alice receives a megolm message", async () => { it("Alice receives a megolm message", async () => {

View File

@ -84,16 +84,16 @@ describe("initRustCrypto", () => {
const testOlmMachine = makeTestOlmMachine(); const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto( await initRustCrypto({
logger, logger,
{} as MatrixClient["http"], http: {} as MatrixClient["http"],
TEST_USER, userId: TEST_USER,
TEST_DEVICE_ID, deviceId: TEST_DEVICE_ID,
{} as ServerSideSecretStorage, secretStorage: {} as ServerSideSecretStorage,
{} as CryptoCallbacks, cryptoCallbacks: {} as CryptoCallbacks,
"storePrefix", storePrefix: "storePrefix",
"storePassphrase", storePassphrase: "storePassphrase",
); });
expect(OlmMachine.initialize).toHaveBeenCalledWith( expect(OlmMachine.initialize).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
@ -107,16 +107,16 @@ describe("initRustCrypto", () => {
const testOlmMachine = makeTestOlmMachine(); const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto( await initRustCrypto({
logger, logger,
{} as MatrixClient["http"], http: {} as MatrixClient["http"],
TEST_USER, userId: TEST_USER,
TEST_DEVICE_ID, deviceId: TEST_DEVICE_ID,
{} as ServerSideSecretStorage, secretStorage: {} as ServerSideSecretStorage,
{} as CryptoCallbacks, cryptoCallbacks: {} as CryptoCallbacks,
null, storePrefix: null,
"storePassphrase", storePassphrase: "storePassphrase",
); });
expect(OlmMachine.initialize).toHaveBeenCalledWith(expect.anything(), expect.anything(), undefined, undefined); expect(OlmMachine.initialize).toHaveBeenCalledWith(expect.anything(), expect.anything(), undefined, undefined);
}); });
@ -125,16 +125,16 @@ describe("initRustCrypto", () => {
const testOlmMachine = makeTestOlmMachine() as OlmMachine; const testOlmMachine = makeTestOlmMachine() as OlmMachine;
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine); jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto( await initRustCrypto({
logger, logger,
{} as MatrixClient["http"], http: {} as MatrixClient["http"],
TEST_USER, userId: TEST_USER,
TEST_DEVICE_ID, deviceId: TEST_DEVICE_ID,
{} as ServerSideSecretStorage, secretStorage: {} as ServerSideSecretStorage,
{} as CryptoCallbacks, cryptoCallbacks: {} as CryptoCallbacks,
"storePrefix", storePrefix: "storePrefix",
"storePassphrase", storePassphrase: "storePassphrase",
); });
expect(testOlmMachine.getSecretsFromInbox).toHaveBeenCalledWith("m.megolm_backup.v1"); 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 () => { it("should wait for a keys/query before returning devices", async () => {
jest.useFakeTimers(); jest.useFakeTimers();
const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
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/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", { fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: { 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 // an attempt to fetch the device list should block
const devicesPromise = rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]); const devicesPromise = rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
@ -963,12 +958,6 @@ describe("RustCrypto", () => {
// Return the key backup // Return the key backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});
const olmMachine = { const olmMachine = {
getIdentity: jest.fn(), getIdentity: jest.fn(),
// Force the backup to be trusted by the olmMachine // Force the backup to be trusted by the olmMachine
@ -981,7 +970,7 @@ describe("RustCrypto", () => {
const rustCrypto = new RustCrypto( const rustCrypto = new RustCrypto(
logger, logger,
olmMachine, olmMachine,
mockHttpApi, makeMatrixHttpApi(),
testData.TEST_USER_ID, testData.TEST_USER_ID,
testData.TEST_DEVICE_ID, testData.TEST_DEVICE_ID,
{} as ServerSideSecretStorage, {} as ServerSideSecretStorage,
@ -1040,6 +1029,15 @@ describe("RustCrypto", () => {
}); });
}); });
/** Build a MatrixHttpApi instance */
function makeMatrixHttpApi(): MatrixHttpApi<IHttpOpts & { onlyData: true }> {
return new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});
}
/** build a basic RustCrypto instance for testing /** build a basic RustCrypto instance for testing
* *
* just provides default arguments for initRustCrypto() * just provides default arguments for initRustCrypto()
@ -1051,7 +1049,16 @@ async function makeTestRustCrypto(
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage, secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks, cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> { ): Promise<RustCrypto> {
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 /** emulate account data, storing in memory

View File

@ -2316,17 +2316,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// importing rust-crypto will download the webassembly, so we delay it until we know it will be // importing rust-crypto will download the webassembly, so we delay it until we know it will be
// needed. // needed.
this.logger.debug("Downloading Rust crypto library");
const RustCrypto = await import("./rust-crypto"); const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(
this.logger, const rustCrypto = await RustCrypto.initRustCrypto({
this.http, logger: this.logger,
userId, http: this.http,
deviceId, userId: userId,
this.secretStorage, deviceId: deviceId,
this.cryptoCallbacks, secretStorage: this.secretStorage,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null, cryptoCallbacks: this.cryptoCallbacks,
this.pickleKey, storePrefix: useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
); storePassphrase: this.pickleKey,
});
rustCrypto.setSupportedVerificationMethods(this.verificationMethods); rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
this.cryptoBackend = rustCrypto; this.cryptoBackend = rustCrypto;

View File

@ -35,6 +35,7 @@ import { sleep } from "../utils";
import { BackupDecryptor } from "../common-crypto/CryptoBackend"; import { BackupDecryptor } from "../common-crypto/CryptoBackend";
import { IEncryptedPayload } from "../crypto/aes"; import { IEncryptedPayload } from "../crypto/aes";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api";
import { IKeyBackupInfo } from "../crypto/keybackup";
/** Authentification of the backup info, depends on algorithm */ /** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"]; type AuthData = KeyBackupInfo["auth_data"];
@ -399,23 +400,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* @returns Information object from API or null if there is no active backup. * @returns Information object from API or null if there is no active backup.
*/ */
public async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> { public async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
try { return await requestKeyBackupVersion(this.http);
return await this.http.authedRequest<KeyBackupInfo>(
Method.Get,
"/room_keys/version",
undefined,
undefined,
{
prefix: ClientPrefix.V3,
},
);
} catch (e) {
if ((<MatrixError>e).errcode === "M_NOT_FOUND") {
return null;
} else {
throw e;
}
}
} }
/** /**
@ -567,6 +552,22 @@ export class RustBackupDecryptor implements BackupDecryptor {
} }
} }
export async function requestKeyBackupVersion(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
): Promise<IKeyBackupInfo | null> {
try {
return await http.authedRequest<KeyBackupInfo>(Method.Get, "/room_keys/version", undefined, undefined, {
prefix: ClientPrefix.V3,
});
} catch (e) {
if ((<MatrixError>e).errcode === "M_NOT_FOUND") {
return null;
} else {
throw e;
}
}
}
export type RustBackupCryptoEvents = export type RustBackupCryptoEvents =
| CryptoEvent.KeyBackupStatus | CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupSessionsRemaining | CryptoEvent.KeyBackupSessionsRemaining

View File

@ -25,22 +25,70 @@ import { Logger } from "../logger";
/** /**
* Create a new `RustCrypto` implementation * Create a new `RustCrypto` implementation
* *
* @param logger - A `Logger` instance that will be used for debug output. * @param args - Parameter object
* @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.
*
* @internal * @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<IHttpOpts & { onlyData: true }>;
/** 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<RustCrypto> {
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, logger: Logger,
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>, http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
userId: string, userId: string,
@ -50,20 +98,10 @@ export async function initRustCrypto(
storePrefix: string | null, storePrefix: string | null,
storePassphrase: string | undefined, storePassphrase: string | undefined,
): Promise<RustCrypto> { ): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-wasm, if it hasn't already been done logger.debug("Init OlmMachine");
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
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize( const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
u, new RustSdkCryptoJs.UserId(userId),
d, new RustSdkCryptoJs.DeviceId(deviceId),
storePrefix ?? undefined, storePrefix ?? undefined,
(storePrefix && storePassphrase) ?? undefined, (storePrefix && storePassphrase) ?? undefined,
); );
@ -101,6 +139,5 @@ export async function initRustCrypto(
// XXX: find a less hacky way to do this. // XXX: find a less hacky way to do this.
await olmMachine.outgoingRequests(); await olmMachine.outgoingRequests();
logger.info("Completed rust crypto-sdk setup");
return rustCrypto; return rustCrypto;
} }

View File

@ -315,18 +315,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#getOwnDeviceKeys}. * Implementation of {@link CryptoApi#getOwnDeviceKeys}.
*/ */
public async getOwnDeviceKeys(): Promise<OwnDeviceKeys> { public async getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice( const keys = this.olmMachine.identityKeys;
this.olmMachine.userId, return { ed25519: keys.ed25519.toBase64(), curve25519: keys.curve25519.toBase64() };
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");
} }
public prepareToEncrypt(room: Room): void { public prepareToEncrypt(room: Room): void {