diff --git a/package.json b/package.json index 128de735ea3..95ef03ad0f6 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", + "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", diff --git a/spec/integ/rust-crypto.spec.ts b/spec/integ/rust-crypto.spec.ts new file mode 100644 index 00000000000..e018c210268 --- /dev/null +++ b/spec/integ/rust-crypto.spec.ts @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; + +import { createClient } from "../../src"; + +afterEach(() => { + // reset fake-indexeddb after each test, to make sure we don't leak connections + // cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state + // eslint-disable-next-line no-global-assign + indexedDB = new IDBFactory(); +}); + +describe("MatrixClient.initRustCrypto", () => { + it("should raise if userId or deviceId is unknown", async () => { + const unknownUserClient = createClient({ + baseUrl: "http://test.server", + deviceId: "aliceDevice", + }); + await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId"); + + const unknownDeviceClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:test", + }); + await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId"); + }); + + it("should create the indexed dbs", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + // No databases. + expect(await indexedDB.databases()).toHaveLength(0); + + await matrixClient.initRustCrypto(); + + // should have two dbs now + const databaseNames = (await indexedDB.databases()).map((db) => db.name); + expect(databaseNames).toEqual( + expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]), + ); + }); + + it("should ignore a second call", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + await matrixClient.initRustCrypto(); + await matrixClient.initRustCrypto(); + }); +}); + +describe("MatrixClient.clearStores", () => { + it("should clear the indexeddbs", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + await matrixClient.initRustCrypto(); + expect(await indexedDB.databases()).toHaveLength(2); + await matrixClient.stopClient(); + + await matrixClient.clearStores(); + expect(await indexedDB.databases()).toHaveLength(0); + }); +}); diff --git a/src/client.ts b/src/client.ts index 83c4ac2cd12..31bf5145d26 100644 --- a/src/client.ts +++ b/src/client.ts @@ -211,6 +211,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; import { CryptoBackend } from "./common-crypto/CryptoBackend"; +import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants"; export type Store = IStore; @@ -1657,6 +1658,41 @@ export class MatrixClient extends TypedEventEmitter => { + let indexedDB: IDBFactory; + try { + indexedDB = global.indexedDB; + } catch (e) { + // No indexeddb support + return; + } + for (const dbname of [ + `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto`, + `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`, + ]) { + const prom = new Promise((resolve, reject) => { + logger.info(`Removing IndexedDB instance ${dbname}`); + const req = indexedDB.deleteDatabase(dbname); + req.onsuccess = (_): void => { + logger.info(`Removed IndexedDB instance ${dbname}`); + resolve(0); + }; + req.onerror = (e): void => { + logger.error(`Failed to remove IndexedDB instance ${dbname}: ${e}`); + reject(new Error(`Error clearing storage: ${e}`)); + }; + req.onblocked = (e): void => { + logger.info(`cannot yet remove IndexedDB instance ${dbname}`); + //reject(new Error(`Error clearing storage: ${e}`)); + }; + }); + await prom; + } + }; + promises.push(deleteRustSdkStore()); + return Promise.all(promises).then(); // .then to fix types } @@ -2051,6 +2087,46 @@ export class MatrixClient extends TypedEventEmitter { + if (this.cryptoBackend) { + logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return; + } + + const userId = this.getUserId(); + if (userId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown userId: ` + + `ensure userId is passed in createClient().`, + ); + } + const deviceId = this.getDeviceId(); + if (deviceId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown deviceId: ` + + `ensure deviceId is passed in createClient().`, + ); + } + + // importing rust-crypto will download the webassembly, so we delay it until we know it will be + // needed. + const RustCrypto = await import("./rust-crypto"); + this.cryptoBackend = await RustCrypto.initRustCrypto(userId, deviceId); + } + /** * Is end-to-end crypto enabled for this client. * @returns True if end-to-end is enabled. diff --git a/src/rust-crypto/constants.ts b/src/rust-crypto/constants.ts new file mode 100644 index 00000000000..9d72060c379 --- /dev/null +++ b/src/rust-crypto/constants.ts @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** The prefix used on indexeddbs created by rust-crypto */ +export const RUST_SDK_STORE_PREFIX = "matrix-js-sdk"; diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts new file mode 100644 index 00000000000..7485836cdc2 --- /dev/null +++ b/src/rust-crypto/index.ts @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { RustCrypto } from "./rust-crypto"; +import { logger } from "../logger"; +import { CryptoBackend } from "../common-crypto/CryptoBackend"; +import { RUST_SDK_STORE_PREFIX } from "./constants"; + +export async function initRustCrypto(userId: string, deviceId: string): Promise { + // initialise the rust matrix-sdk-crypto-js, 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 + const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass"); + const rustCrypto = new RustCrypto(olmMachine, userId, deviceId); + + 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 new file mode 100644 index 00000000000..07b7ea3b9d9 --- /dev/null +++ b/src/rust-crypto/rust-crypto.ts @@ -0,0 +1,60 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { IEventDecryptionResult } from "../@types/crypto"; +import { MatrixEvent } from "../models/event"; +import { CryptoBackend } from "../common-crypto/CryptoBackend"; + +// import { logger } from "../logger"; + +/** + * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. + */ +export class RustCrypto implements CryptoBackend { + public globalBlacklistUnverifiedDevices = false; + public globalErrorOnUnknownDevices = false; + + /** whether stop() has been called */ + private stopped = false; + + public constructor(private readonly olmMachine: RustSdkCryptoJs.OlmMachine, _userId: string, _deviceId: string) {} + + public stop(): void { + // stop() may be called multiple times, but attempting to close() the OlmMachine twice + // will cause an error. + if (this.stopped) { + return; + } + this.stopped = true; + + // make sure we close() the OlmMachine; doing so means that all the Rust objects will be + // cleaned up; in particular, the indexeddb connections will be closed, which means they + // can then be deleted. + this.olmMachine.close(); + } + + public async decryptEvent(event: MatrixEvent): Promise { + await this.olmMachine.decryptRoomEvent("event", new RustSdkCryptoJs.RoomId("room")); + throw new Error("not implemented"); + } + + public async userHasCrossSigningKeys(): Promise { + // TODO + return false; + } +} diff --git a/yarn.lock b/yarn.lock index c4d494579ad..79fc6cda9b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1432,6 +1432,11 @@ dependencies: lodash "^4.17.21" +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": + version "0.1.0-alpha.2" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" + integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== + "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"