Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ElementR: Add CryptoApi#bootstrapSecretStorage #3483

Merged
merged 21 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ad15eb
Add WIP bootstrapSecretStorage
florianduros Jun 16, 2023
ebe7bc2
Add new test if `createSecretStorageKey` is not set
florianduros Jun 16, 2023
900745f
Remove old comments
florianduros Jun 16, 2023
9ffc9d5
Add docs for `crypto-api.bootstrapSecretStorage`
florianduros Jun 16, 2023
3be57f4
Remove default parameter for `createSecretStorageKey`
florianduros Jun 16, 2023
e2ffbdb
Move `bootstrapSecretStorage` next to `isSecretStorageReady`
florianduros Jun 19, 2023
2ca8acf
Deprecate `bootstrapSecretStorage` in `MatrixClient`
florianduros Jun 19, 2023
9cc7eda
Update documentations
florianduros Jun 19, 2023
d744d6b
Raise error if missing `keyInfo`
florianduros Jun 19, 2023
18d629a
Update behavior around `setupNewSecretStorage`
florianduros Jun 19, 2023
5c24168
Move `ICreateSecretStorageOpts` to `rust-crypto`
florianduros Jun 19, 2023
4ce3c3c
Move `ICryptoCallbacks` to `rust-crypto`
florianduros Jun 19, 2023
5c72ebc
Update `bootstrapSecretStorage` documentation
florianduros Jun 19, 2023
ff7f2b5
Add partial `CryptoCallbacks` documentation
florianduros Jun 19, 2023
f01c20c
Fix typo
florianduros Jun 19, 2023
397a3f9
Merge branch 'develop' into florianduros/element-r/bootstrapSecretSto…
florianduros Jun 19, 2023
9a59e35
Review changes
florianduros Jun 20, 2023
be55709
Merge remote-tracking branch 'origin/florianduros/element-r/bootstrap…
florianduros Jun 20, 2023
2f40b87
Review changes
florianduros Jun 20, 2023
cbb98be
Merge branch 'develop' into florianduros/element-r/bootstrapSecretSto…
florianduros Jun 20, 2023
7cb87e5
Merge branch 'develop' into florianduros/element-r/bootstrapSecretSto…
florianduros Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ module.exports = {
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},
Expand Down
133 changes: 133 additions & 0 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { escapeRegExp } from "../../../src/utils";
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
import { flushPromises } from "../../test-utils/flushPromises";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";

const ROOM_ID = "!room:id";

Expand Down Expand Up @@ -402,6 +403,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;

const Olm = global.Olm;

Expand Down Expand Up @@ -2169,4 +2171,135 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
});

describe("bootstrapSecretStorage", () => {
/**
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
*/
const createSecretStorageKey = jest.fn().mockResolvedValue({
keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
});

/**
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
* Resolved when a key is uploaded (ie in `body.content.key`)
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/
function awaitKeyStoredInAccountData(): Promise<string> {
return new Promise((resolve) => {
// This url is called multiple times during the secret storage bootstrap process
// When we received the newly generated key, we return it
fetchMock.put(
"express:/_matrix/client/r0/user/:userId/account_data/:type",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);

if (content.key) {
resolve(content.key);
}

return {};
},
);
});
}

/**
* Send in the sync response the provided `secretStorageKey` into the account_data field
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
* https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3sync
* @param secretStorageKey
*/
function sendSyncResponse(secretStorageKey: string) {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.default_key",
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
// Needed for secretStorage.getKey or secretStorage.hasKey
{
type: `m.secret_storage.key.${secretStorageKey}`,
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
}

beforeEach(async () => {
createSecretStorageKey.mockClear();

expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
});

newBackendOnly("should do no nothing if setupNewSecretStorage is not set", async () => {
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });

// No key was created
expect(createSecretStorageKey).toHaveBeenCalledTimes(0);
});

newBackendOnly("should do no nothing if createSecretStorageKey is not set", async () => {
await aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true });

// No key was created
expect(createSecretStorageKey).toHaveBeenCalledTimes(0);
});

newBackendOnly("should create a new key", async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);

// Finally, wait for bootstrapSecretStorage to finished
await bootstrapPromise;

const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
// Check that the uploaded key in stored in the secret storage
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
// Check that the uploaded key is the default key
expect(defaultKeyId).toBe(secretStorageKey);
});

newBackendOnly("should do nothing if an AES key is already in the secret storage", async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);

// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;

// Call again bootstrapSecretStorage
await aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
});
});
});
7 changes: 5 additions & 2 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Mocked } from "jest-mock";

import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import { ICryptoCallbacks, IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
Expand Down Expand Up @@ -211,6 +211,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as ICryptoCallbacks,
);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});
Expand Down Expand Up @@ -333,6 +334,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as ICryptoCallbacks,
);
});

Expand Down Expand Up @@ -400,6 +402,7 @@ async function makeTestRustCrypto(
userId: string = TEST_USER,
deviceId: string = TEST_DEVICE_ID,
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: ICryptoCallbacks = {} as ICryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage);
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
}
8 changes: 7 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,13 @@ 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.
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
const rustCrypto = await RustCrypto.initRustCrypto(
this.http,
userId,
deviceId,
this.secretStorage,
this.cryptoCallbacks,
);
this.cryptoBackend = rustCrypto;

// attach the event listeners needed by RustCrypto
Expand Down
13 changes: 13 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { AddSecretStorageKeyOpts } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { ICreateSecretStorageOpts } from "./crypto/api";
florianduros marked this conversation as resolved.
Show resolved Hide resolved

/** Types of cross-signing key */
export enum CrossSigningKey {
Expand Down Expand Up @@ -229,6 +230,18 @@ export interface CryptoApi {
*/
createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey>;

/**
* Bootstrap the secret storage by creating a new secret storage key and store it in the secret storage.
*
* - Do nothing if an AES key is already stored in the secret storage;
* - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`.
* - Store this key in the secret storage and set it as the default key.
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
*
* @param opts - Options object.
*/
bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void>;
florianduros marked this conversation as resolved.
Show resolved Hide resolved

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Device/User verification
Expand Down
5 changes: 4 additions & 1 deletion src/rust-crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { logger } from "../logger";
import { RUST_SDK_STORE_PREFIX } from "./constants";
import { IHttpOpts, MatrixHttpApi } from "../http-api";
import { ServerSideSecretStorage } from "../secret-storage";
import { ICryptoCallbacks } from "../crypto";

/**
* Create a new `RustCrypto` implementation
Expand All @@ -30,12 +31,14 @@ import { ServerSideSecretStorage } from "../secret-storage";
* @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 called during the lifetime of the e2e
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
export async function initRustCrypto(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
userId: string,
deviceId: string,
secretStorage: ServerSideSecretStorage,
cryptoCallbacks: ICryptoCallbacks,
): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
await RustSdkCryptoJs.initAsync();
Expand All @@ -49,7 +52,7 @@ export async function initRustCrypto(

// 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, http, userId, deviceId, secretStorage);
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions),
);
Expand Down
48 changes: 46 additions & 2 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";

import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api";
import type { ICreateSecretStorageOpts, IEncryptedEventInfo } from "../crypto/api";
import { MatrixEvent } from "../models/event";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
Expand All @@ -43,12 +43,13 @@ import {
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device";
import { AddSecretStorageKeyOpts, ServerSideSecretStorage } from "../secret-storage";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningIdentity } from "./CrossSigningIdentity";
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
import { ICryptoCallbacks } from "../crypto";
florianduros marked this conversation as resolved.
Show resolved Hide resolved

/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
Expand Down Expand Up @@ -90,6 +91,9 @@ export class RustCrypto implements CryptoBackend {

/** Interface to server-side secret storage */
private readonly secretStorage: ServerSideSecretStorage,

/** Crypto callbacks called during the lifetime of the e2e */
florianduros marked this conversation as resolved.
Show resolved Hide resolved
private readonly cryptoCallbacks: ICryptoCallbacks,
) {
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
Expand Down Expand Up @@ -428,6 +432,46 @@ export class RustCrypto implements CryptoBackend {
};
}

/**
* Implementation of {@link CryptoApi#bootstrapSecretStorage}
*/
public async bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage,
}: ICreateSecretStorageOpts = {}): Promise<void> {
// If setupNewSecretStorage is not set, we stop
if (!setupNewSecretStorage || !createSecretStorageKey) return;
florianduros marked this conversation as resolved.
Show resolved Hide resolved

const secretStorageKeyTuple = await this.secretStorage.getKey();
florianduros marked this conversation as resolved.
Show resolved Hide resolved

if (secretStorageKeyTuple) {
const [, keyInfo] = secretStorageKeyTuple;

// If an AES Key is already stored in the secret storage
// We stop
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
return;
}
}

const recoveryKey = await createSecretStorageKey();

// keyInfo is required to continue
if (!recoveryKey.keyInfo) return;
florianduros marked this conversation as resolved.
Show resolved Hide resolved

const secretStorageKeyObject = await this.secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
recoveryKey.keyInfo,
);
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);

this.cryptoCallbacks.cacheSecretStorageKey?.(
secretStorageKeyObject.keyId,
secretStorageKeyObject.keyInfo,
recoveryKey.privateKey,
);
}

/**
* Returns to-device verification requests that are already in progress for the given user id.
*
Expand Down