Skip to content

Commit

Permalink
Store cross signing keys in secret storage
Browse files Browse the repository at this point in the history
  • Loading branch information
florianduros committed Jun 21, 2023
1 parent 9c6d5a6 commit 762c7a1
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 73 deletions.
52 changes: 5 additions & 47 deletions spec/integ/crypto/cross-signing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";

import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { createClient, MatrixClient, IAuthDict, UIAuthCallback } from "../../../src";
import { createClient, MatrixClient } from "../../../src";
import { bootstrapCrossSigning, mockSetupCrossSigningRequests } from "../../test-utils/cross-signing";

afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
Expand Down Expand Up @@ -61,56 +62,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
fetchMock.mockReset();
});

/**
* Mock the requests needed to set up cross signing
*
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});

// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});

// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
{},
);
}

/**
* Create cross-signing keys, publish the keys
* Mock and bootstrap all the required steps
*
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};

// now bootstrap cross signing, and check it resolves successfully
await aliceClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
}

describe("bootstrapCrossSigning (before initialsync completes)", () => {
it("publishes keys if none were yet published", async () => {
mockSetupCrossSigningRequests();

// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
await bootstrapCrossSigning(aliceClient, authDict);

// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
Expand Down Expand Up @@ -156,7 +114,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s

// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
await bootstrapCrossSigning(aliceClient, authDict);

const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();

Expand All @@ -180,7 +138,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s

it("should return true after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
await bootstrapCrossSigning({ type: "test" });
await bootstrapCrossSigning(aliceClient, { type: "test" });

const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();

Expand Down
84 changes: 76 additions & 8 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ 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";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { mockSetupCrossSigningRequests } from "../../test-utils/cross-signing";
import { CryptoCallbacks } from "../../../src/crypto-api";

const ROOM_ID = "!room:id";

Expand Down Expand Up @@ -530,6 +532,27 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
};
}

/**
* Create the {@link CryptoCallbacks}
*/
function createCryptoCallbacks(): CryptoCallbacks {
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
let cachedKey: { keyId: string; key: Uint8Array };
const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => {
cachedKey = {
keyId,
key,
};
};

const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);

return {
cacheSecretStorageKey,
getSecretStorageKey,
};
}

beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
Expand All @@ -541,6 +564,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
userId: "@alice:localhost",
accessToken: "akjgkrgjs",
deviceId: "xzcvb",
cryptoCallbacks: createCryptoCallbacks(),
});

/* set up listeners for /keys/upload and /sync */
Expand Down Expand Up @@ -2183,16 +2207,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});

/**
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)`
* 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> {
function awaitSecretStorageKeyStoredInAccountData(): 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",
"express:/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);

Expand All @@ -2207,6 +2231,25 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
}

/**
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/m.cross_signing.master`
* Resolved when the cross signing master key is uploaded
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/
function awaitCrossSigningMasterKeyUpload(): Promise<Record<string, {}>> {
return new Promise((resolve) => {
// Called when the cross signing key master key is uploaded
fetchMock.put(
"express:/_matrix/client/r0/user/:userId/account_data/m.cross_signing.master",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
resolve(content.encrypted);
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
Expand Down Expand Up @@ -2258,7 +2301,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

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

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
Expand All @@ -2279,7 +2322,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });

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

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
Expand All @@ -2303,7 +2346,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

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

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
Expand All @@ -2317,7 +2360,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

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

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
Expand All @@ -2329,5 +2372,30 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
},
);

newBackendOnly("should upload cross signing master key", async () => {
mockSetupCrossSigningRequests();

await aliceClient.getCrypto()?.bootstrapCrossSigning({});

const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

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

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

// Wait for the cross signing key to be uploaded
const crossSigningKey = await awaitCrossSigningMasterKeyUpload();

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

// Expect the cross signing master key to be uploaded and to be encrypted with `secretStorageKey`
expect(crossSigningKey[secretStorageKey]).toBeDefined();
});
});
});
62 changes: 62 additions & 0 deletions spec/test-utils/cross-signing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";

import { IAuthDict, MatrixClient, UIAuthCallback } from "../../src";

/**
* Mock the requests needed to set up cross signing
*
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});

// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});

// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
{},
);
}

/**
* Create cross-signing keys and publish the keys
*
* @param matrixClient - The matrixClient to bootstrap.
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
export async function bootstrapCrossSigning(matrixClient: MatrixClient, authDict: IAuthDict): Promise<void> {
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};

// now bootstrap cross signing, and check it resolves successfully
await matrixClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
}
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ export interface ICreateClientOpts {
*/
useE2eForGroupCall?: boolean;

/**
* Crypto callbacks provided by the application
*/
cryptoCallbacks?: ICryptoCallbacks;

/**
Expand Down
Loading

0 comments on commit 762c7a1

Please sign in to comment.