Skip to content

Commit

Permalink
initRustCrypto: allow app to pass in the store key directly (#4210)
Browse files Browse the repository at this point in the history
* `initRustCrypto`: allow app to pass in the store key directly

... instead of using the pickleKey. This allows us to avoid a slow PBKDF
operation.

* Fix link in doc-comment
  • Loading branch information
richvdh authored May 24, 2024
1 parent a81adf5 commit 36196ea
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 25 deletions.
38 changes: 38 additions & 0 deletions spec/integ/crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 27 additions & 3 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("initRustCrypto", () => {
} as unknown as Mocked<OlmMachine>;
}

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);

Expand All @@ -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);

Expand All @@ -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);
});

Expand Down
41 changes: 26 additions & 15 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -2229,17 +2229,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* An alternative to {@link initCrypto}.
*
* *WARNING*: this API is very experimental, should not be used in production, and may change without notice!
* Eventually it will be deprecated and `initCrypto` will do the same thing.
*
* @experimental
*
* @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
* 32 bytes of data, and must be the same each time the client is initialised for a given device.
* If both this and `storagePassword` are unspecified, the store will be unencrypted.
* @param args.storagePassword - An alternative to `storageKey`. A password which will be used to derive a key to
* encrypt the store with. Deriving a key from a password is (deliberately) a slow operation, so prefer
* to pass a `storageKey` directly where possible.
*
* @returns a Promise which will resolve when the crypto layer has been
* successfully initialised.
*/
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
public async initRustCrypto(
args: {
useIndexedDB?: boolean;
storageKey?: Uint8Array;
storagePassword?: string;
} = {},
): Promise<void> {
if (this.cryptoBackend) {
this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
Expand Down Expand Up @@ -2272,11 +2279,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: deviceId,
secretStorage: this.secretStorage,
cryptoCallbacks: this.cryptoCallbacks,
storePrefix: useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
storePassphrase: this.pickleKey,
storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX,
storeKey: args.storageKey,

// temporary compatibility hack: if there is no storageKey nor storagePassword, fall back to the pickleKey
storePassphrase: args.storagePassword ?? this.pickleKey,

legacyCryptoStore: this.cryptoStore,
legacyPickleKey: this.pickleKey ?? "DEFAULT_KEY",
legacyMigrationProgressListener: (progress, total) => {
legacyMigrationProgressListener: (progress: number, total: number): void => {
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
},
});
Expand Down
28 changes: 21 additions & 7 deletions src/rust-crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down

0 comments on commit 36196ea

Please sign in to comment.