Skip to content

Commit

Permalink
Groundwork for supporting migration from libolm to rust crypto. (#3977)
Browse files Browse the repository at this point in the history
* `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.
  • Loading branch information
richvdh authored Jan 3, 2024
1 parent c115e05 commit d030c83
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 118 deletions.
14 changes: 4 additions & 10 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
89 changes: 48 additions & 41 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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);
});
Expand All @@ -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");
});
Expand Down Expand Up @@ -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<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/query", {
device_keys: {
Expand All @@ -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]);
Expand Down Expand Up @@ -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<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});

const olmMachine = {
getIdentity: jest.fn(),
// Force the backup to be trusted by the olmMachine
Expand All @@ -981,7 +970,7 @@ describe("RustCrypto", () => {
const rustCrypto = new RustCrypto(
logger,
olmMachine,
mockHttpApi,
makeMatrixHttpApi(),
testData.TEST_USER_ID,
testData.TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
Expand Down Expand Up @@ -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
*
* just provides default arguments for initRustCrypto()
Expand All @@ -1051,7 +1049,16 @@ async function makeTestRustCrypto(
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): 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
Expand Down
22 changes: 12 additions & 10 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// needed.
this.logger.debug("Downloading Rust crypto library");
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(
this.logger,
this.http,
userId,
deviceId,
this.secretStorage,
this.cryptoCallbacks,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
this.pickleKey,
);

const rustCrypto = await RustCrypto.initRustCrypto({
logger: this.logger,
http: this.http,
userId: userId,
deviceId: deviceId,
secretStorage: this.secretStorage,
cryptoCallbacks: this.cryptoCallbacks,
storePrefix: useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
storePassphrase: this.pickleKey,
});
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);

this.cryptoBackend = rustCrypto;
Expand Down
35 changes: 18 additions & 17 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { sleep } from "../utils";
import { BackupDecryptor } from "../common-crypto/CryptoBackend";
import { IEncryptedPayload } from "../crypto/aes";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api";
import { IKeyBackupInfo } from "../crypto/keybackup";

/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
Expand Down Expand Up @@ -399,23 +400,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* @returns Information object from API or null if there is no active backup.
*/
public async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
try {
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;
}
}
return await requestKeyBackupVersion(this.http);
}

/**
Expand Down Expand Up @@ -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 =
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupSessionsRemaining
Expand Down
93 changes: 65 additions & 28 deletions src/rust-crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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,
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
userId: string,
Expand All @@ -50,20 +98,10 @@ export async function initRustCrypto(
storePrefix: string | null,
storePassphrase: string | undefined,
): Promise<RustCrypto> {
// 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,
);
Expand Down Expand Up @@ -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;
}
14 changes: 2 additions & 12 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,18 +315,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
*/
public async getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
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 {
Expand Down

0 comments on commit d030c83

Please sign in to comment.