diff --git a/spec/integ/crypto/rust-crypto.spec.ts b/spec/integ/crypto/rust-crypto.spec.ts index 311410febbe..c6b990fed0f 100644 --- a/spec/integ/crypto/rust-crypto.spec.ts +++ b/spec/integ/crypto/rust-crypto.spec.ts @@ -85,6 +85,44 @@ describe("MatrixClient.initRustCrypto", () => { ); }); + it("should create the meta db if given a storageKey", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + // No databases. + expect(await indexedDB.databases()).toHaveLength(0); + + await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) }); + + // should have two indexed 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 create the meta db if given a storagePassword", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + // No databases. + expect(await indexedDB.databases()).toHaveLength(0); + + await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" }); + + // should have two indexed 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", diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index c6507a0d145..90cd079912e 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -104,7 +104,7 @@ describe("initRustCrypto", () => { } as unknown as Mocked; } - it("passes through the store params", async () => { + it("passes through the store params (passphrase)", async () => { const mockStore = { free: jest.fn() } as unknown as StoreHandle; jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore); @@ -126,7 +126,30 @@ describe("initRustCrypto", () => { expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore); }); - it("suppresses the storePassphrase if storePrefix is unset", async () => { + it("passes through the store params (key)", async () => { + const mockStore = { free: jest.fn() } as unknown as StoreHandle; + jest.spyOn(StoreHandle, "openWithKey").mockResolvedValue(mockStore); + + const testOlmMachine = makeTestOlmMachine(); + jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine); + + const storeKey = new Uint8Array(32); + await initRustCrypto({ + logger, + http: {} as MatrixClient["http"], + userId: TEST_USER, + deviceId: TEST_DEVICE_ID, + secretStorage: {} as ServerSideSecretStorage, + cryptoCallbacks: {} as CryptoCallbacks, + storePrefix: "storePrefix", + storeKey: storeKey, + }); + + expect(StoreHandle.openWithKey).toHaveBeenCalledWith("storePrefix", storeKey); + expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore); + }); + + it("suppresses the storePassphrase and storeKey if storePrefix is unset", async () => { const mockStore = { free: jest.fn() } as unknown as StoreHandle; jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore); @@ -141,10 +164,11 @@ describe("initRustCrypto", () => { secretStorage: {} as ServerSideSecretStorage, cryptoCallbacks: {} as CryptoCallbacks, storePrefix: null, + storeKey: new Uint8Array(), storePassphrase: "storePassphrase", }); - expect(StoreHandle.open).toHaveBeenCalledWith(undefined, undefined); + expect(StoreHandle.open).toHaveBeenCalledWith(); expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore); }); diff --git a/src/client.ts b/src/client.ts index 27485d076f8..2e349213123 100644 --- a/src/client.ts +++ b/src/client.ts @@ -357,14 +357,14 @@ export interface ICreateClientOpts { deviceToImport?: IExportedDevice; /** - * Encryption key used for encrypting sensitive data (such as e2ee keys) in storage. + * Encryption key used for encrypting sensitive data (such as e2ee keys) in {@link ICreateClientOpts#cryptoStore}. * * This must be set to the same value every time the client is initialised for the same device. * - * If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation. - * - * No particular requirement is placed on the key data (it is fed into an HKDF to generate the actual encryption - * keys). + * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}), + * but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device + * previously used legacy crypto (so must be migrated), then this must still be provided, so that the + * data can be migrated from the legacy store. */ pickleKey?: string; @@ -2229,17 +2229,24 @@ export class MatrixClient extends TypedEventEmitter { + public async initRustCrypto( + args: { + useIndexedDB?: boolean; + storageKey?: Uint8Array; + storagePassword?: string; + } = {}, + ): Promise { if (this.cryptoBackend) { this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; @@ -2272,11 +2279,15 @@ export class MatrixClient extends TypedEventEmitter { + legacyMigrationProgressListener: (progress: number, total: number): void => { this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total); }, }); diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index 6defede1e95..dc9a42af743 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -64,13 +64,21 @@ export async function initRustCrypto(args: { storePrefix: string | null; /** - * A passphrase to use to encrypt the indexeddbs created by rust-crypto. + * A passphrase to use to encrypt the indexeddb created by rust-crypto. * - * Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs - * will be unencrypted. + * Ignored if `storePrefix` is null, or `storeKey` is set. If neither this nor `storeKey` is set + * (and `storePrefix` is not null), the indexeddb will be unencrypted. */ storePassphrase?: string; + /** + * A key to use to encrypt the indexeddb created by rust-crypto. + * + * Ignored if `storePrefix` is null. Otherwise, if it is set, it must be a 32-byte cryptographic key, which + * will be used to encrypt the indexeddb. See also `storePassphrase`. + */ + storeKey?: Uint8Array; + /** If defined, we will check if any data needs migrating from this store to the rust store. */ legacyCryptoStore?: CryptoStore; @@ -94,10 +102,16 @@ export async function initRustCrypto(args: { new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn(); logger.debug("Opening Rust CryptoStore"); - const storeHandle: StoreHandle = await StoreHandle.open( - args.storePrefix ?? undefined, - (args.storePrefix && args.storePassphrase) ?? undefined, - ); + let storeHandle; + if (args.storePrefix) { + if (args.storeKey) { + storeHandle = await StoreHandle.openWithKey(args.storePrefix, args.storeKey); + } else { + storeHandle = await StoreHandle.open(args.storePrefix, args.storePassphrase); + } + } else { + storeHandle = await StoreHandle.open(); + } if (args.legacyCryptoStore) { // We have a legacy crypto store, which we may need to migrate from.