Skip to content

Commit 7fe2d79

Browse files
committed
Store cross signing keys in secret storage
1 parent 9c6d5a6 commit 7fe2d79

File tree

5 files changed

+200
-74
lines changed

5 files changed

+200
-74
lines changed

spec/integ/crypto/cross-signing.spec.ts

+5-47
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import "fake-indexeddb/auto";
1919
import { IDBFactory } from "fake-indexeddb";
2020

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

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

64-
/**
65-
* Mock the requests needed to set up cross signing
66-
*
67-
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
68-
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
69-
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
70-
*/
71-
function mockSetupCrossSigningRequests(): void {
72-
// have account_data requests return an empty object
73-
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
74-
75-
// we expect a request to upload signatures for our device ...
76-
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
77-
78-
// ... and one to upload the cross-signing keys (with UIA)
79-
fetchMock.post(
80-
// legacy crypto uses /unstable/; /v3/ is correct
81-
{
82-
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
83-
name: "upload-keys",
84-
},
85-
{},
86-
);
87-
}
88-
89-
/**
90-
* Create cross-signing keys, publish the keys
91-
* Mock and bootstrap all the required steps
92-
*
93-
* @param authDict - The parameters to as the `auth` dict in the key upload request.
94-
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
95-
*/
96-
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
97-
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
98-
await makeRequest(authDict);
99-
};
100-
101-
// now bootstrap cross signing, and check it resolves successfully
102-
await aliceClient.getCrypto()?.bootstrapCrossSigning({
103-
authUploadDeviceSigningKeys: uiaCallback,
104-
});
105-
}
106-
10765
describe("bootstrapCrossSigning (before initialsync completes)", () => {
10866
it("publishes keys if none were yet published", async () => {
10967
mockSetupCrossSigningRequests();
11068

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

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

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

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

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

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

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

spec/integ/crypto/crypto.spec.ts

+76-9
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ import { escapeRegExp } from "../../../src/utils";
5151
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
5252
import { flushPromises } from "../../test-utils/flushPromises";
5353
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
54-
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
54+
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
55+
import { mockSetupCrossSigningRequests } from "../../test-utils/cross-signing";
56+
import { CryptoCallbacks } from "../../../src/crypto-api";
5557

5658
const ROOM_ID = "!room:id";
5759

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

535+
/**
536+
* Create the {@link CryptoCallbacks}
537+
*/
538+
function createCryptoCallbacks(): CryptoCallbacks {
539+
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
540+
let cachedKey: { keyId: string; key: Uint8Array };
541+
const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => {
542+
cachedKey = {
543+
keyId,
544+
key,
545+
};
546+
};
547+
548+
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
549+
550+
return {
551+
cacheSecretStorageKey,
552+
getSecretStorageKey,
553+
};
554+
}
555+
533556
beforeEach(async () => {
534557
// anything that we don't have a specific matcher for silently returns a 404
535558
fetchMock.catch(404);
@@ -541,6 +564,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
541564
userId: "@alice:localhost",
542565
accessToken: "akjgkrgjs",
543566
deviceId: "xzcvb",
567+
cryptoCallbacks: createCryptoCallbacks(),
544568
});
545569

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

21852209
/**
2186-
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
2210+
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)`
21872211
* Resolved when a key is uploaded (ie in `body.content.key`)
21882212
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
21892213
*/
2190-
function awaitKeyStoredInAccountData(): Promise<string> {
2214+
function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
21912215
return new Promise((resolve) => {
21922216
// This url is called multiple times during the secret storage bootstrap process
21932217
// When we received the newly generated key, we return it
21942218
fetchMock.put(
2195-
"express:/_matrix/client/r0/user/:userId/account_data/:type",
2219+
"express:/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)",
21962220
(url: string, options: RequestInit) => {
21972221
const content = JSON.parse(options.body as string);
21982222

@@ -2202,7 +2226,25 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22022226

22032227
return {};
22042228
},
2205-
{ overwriteRoutes: true },
2229+
);
2230+
});
2231+
}
2232+
2233+
/**
2234+
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/m.cross_signing.master`
2235+
* Resolved when the cross signing master key is uploaded
2236+
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
2237+
*/
2238+
function awaitCrossSigningMasterKeyUpload(): Promise<Record<string, {}>> {
2239+
return new Promise((resolve) => {
2240+
// Called when the cross signing key master key is uploaded
2241+
fetchMock.put(
2242+
"express:/_matrix/client/r0/user/:userId/account_data/m.cross_signing.master",
2243+
(url: string, options: RequestInit) => {
2244+
const content = JSON.parse(options.body as string);
2245+
resolve(content.encrypted);
2246+
return {};
2247+
},
22062248
);
22072249
});
22082250
}
@@ -2258,7 +2300,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22582300
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
22592301

22602302
// Wait for the key to be uploaded in the account data
2261-
const secretStorageKey = await awaitKeyStoredInAccountData();
2303+
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
22622304

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

22812323
// Wait for the key to be uploaded in the account data
2282-
const secretStorageKey = await awaitKeyStoredInAccountData();
2324+
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
22832325

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

23052347
// Wait for the key to be uploaded in the account data
2306-
let secretStorageKey = await awaitKeyStoredInAccountData();
2348+
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
23072349

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

23192361
// Wait for the key to be uploaded in the account data
2320-
secretStorageKey = await awaitKeyStoredInAccountData();
2362+
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
23212363

23222364
// Return the newly created key in the sync response
23232365
sendSyncResponse(secretStorageKey);
@@ -2329,5 +2371,30 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23292371
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
23302372
},
23312373
);
2374+
2375+
newBackendOnly("should upload cross signing master key", async () => {
2376+
mockSetupCrossSigningRequests();
2377+
2378+
await aliceClient.getCrypto()?.bootstrapCrossSigning({});
2379+
2380+
const bootstrapPromise = aliceClient
2381+
.getCrypto()!
2382+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
2383+
2384+
// Wait for the key to be uploaded in the account data
2385+
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2386+
2387+
// Return the newly created key in the sync response
2388+
sendSyncResponse(secretStorageKey);
2389+
2390+
// Wait for the cross signing key to be uploaded
2391+
const crossSigningKey = await awaitCrossSigningMasterKeyUpload();
2392+
2393+
// Finally, wait for bootstrapSecretStorage to finished
2394+
await bootstrapPromise;
2395+
2396+
// Expect the cross signing master key to be uploaded and to be encrypted with `secretStorageKey`
2397+
expect(crossSigningKey[secretStorageKey]).toBeDefined();
2398+
});
23322399
});
23332400
});

spec/test-utils/cross-signing.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import fetchMock from "fetch-mock-jest";
18+
19+
import { IAuthDict, MatrixClient, UIAuthCallback } from "../../src";
20+
21+
/**
22+
* Mock the requests needed to set up cross signing
23+
*
24+
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
25+
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
26+
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
27+
*/
28+
export function mockSetupCrossSigningRequests(): void {
29+
// have account_data requests return an empty object
30+
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
31+
32+
// we expect a request to upload signatures for our device ...
33+
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
34+
35+
// ... and one to upload the cross-signing keys (with UIA)
36+
fetchMock.post(
37+
// legacy crypto uses /unstable/; /v3/ is correct
38+
{
39+
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
40+
name: "upload-keys",
41+
},
42+
{},
43+
);
44+
}
45+
46+
/**
47+
* Create cross-signing keys and publish the keys
48+
*
49+
* @param matrixClient - The matrixClient to bootstrap.
50+
* @param authDict - The parameters to as the `auth` dict in the key upload request.
51+
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
52+
*/
53+
export async function bootstrapCrossSigning(matrixClient: MatrixClient, authDict: IAuthDict): Promise<void> {
54+
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
55+
await makeRequest(authDict);
56+
};
57+
58+
// now bootstrap cross signing, and check it resolves successfully
59+
await matrixClient.getCrypto()?.bootstrapCrossSigning({
60+
authUploadDeviceSigningKeys: uiaCallback,
61+
});
62+
}

src/client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ export interface ICreateClientOpts {
367367
*/
368368
useE2eForGroupCall?: boolean;
369369

370+
/**
371+
* Crypto callbacks provided by the application
372+
*/
370373
cryptoCallbacks?: ICryptoCallbacks;
371374

372375
/**

0 commit comments

Comments
 (0)