diff --git a/spec/integ/cross-signing.spec.ts b/spec/integ/cross-signing.spec.ts index 0b2068309..ba227d3f8 100644 --- a/spec/integ/cross-signing.spec.ts +++ b/spec/integ/cross-signing.spec.ts @@ -38,10 +38,6 @@ const TEST_DEVICE_ID = "xzcvb"; * to provide the most effective integration tests possible. */ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => { - // 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; - let aliceClient: MatrixClient; beforeEach(async () => { @@ -66,7 +62,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s }); describe("bootstrapCrossSigning (before initialsync completes)", () => { - oldBackendOnly("publishes keys if none were yet published", async () => { + it("publishes keys if none were yet published", async () => { // have account_data requests return an empty object fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {}); @@ -75,7 +71,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s // ... and one to upload the cross-signing keys (with UIA) fetchMock.post( - { url: "path:/_matrix/client/unstable/keys/device_signing/upload", name: "upload-keys" }, + // legacy crypto uses /unstable/; /v3/ is correct + { + url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"), + name: "upload-keys", + }, {}, ); diff --git a/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts new file mode 100644 index 000000000..d0fb8bd36 --- /dev/null +++ b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts @@ -0,0 +1,79 @@ +/* +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 { Mocked } from "jest-mock"; +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity"; +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; + +describe("CrossSigningIdentity", () => { + describe("bootstrapCrossSigning", () => { + /** the CrossSigningIdentity implementation under test */ + let crossSigning: CrossSigningIdentity; + + /** a mocked-up OlmMachine which crossSigning is connected to */ + let olmMachine: Mocked; + + /** A mock OutgoingRequestProcessor which crossSigning is connected to */ + let outgoingRequestProcessor: Mocked; + + beforeEach(async () => { + await RustSdkCryptoJs.initAsync(); + + olmMachine = { + crossSigningStatus: jest.fn(), + bootstrapCrossSigning: jest.fn(), + close: jest.fn(), + } as unknown as Mocked; + + outgoingRequestProcessor = { + makeOutgoingRequest: jest.fn(), + } as unknown as Mocked; + + crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor); + }); + + it("should do nothing if keys are present on-device and in secret storage", async () => { + olmMachine.crossSigningStatus.mockResolvedValue({ + hasMaster: true, + hasSelfSigning: true, + hasUserSigning: true, + }); + // TODO: secret storage + await crossSigning.bootstrapCrossSigning({}); + expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled(); + expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled(); + }); + + it("should call bootstrapCrossSigning if a reset is forced", async () => { + olmMachine.bootstrapCrossSigning.mockResolvedValue([]); + await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true }); + expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true); + }); + + it("should call bootstrapCrossSigning if we need new keys", async () => { + olmMachine.crossSigningStatus.mockResolvedValue({ + hasMaster: false, + hasSelfSigning: false, + hasUserSigning: false, + }); + olmMachine.bootstrapCrossSigning.mockResolvedValue([]); + await crossSigning.bootstrapCrossSigning({}); + expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true); + }); + }); +}); diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index a0c9a4776..71df210ad 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -103,9 +103,15 @@ describe("RustCrypto", () => { await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null); }); - it("bootstrapCrossSigning", async () => { + it("bootstrapCrossSigning delegates to CrossSigningIdentity", async () => { const rustCrypto = await makeTestRustCrypto(); + const mockCrossSigningIdentity = { + bootstrapCrossSigning: jest.fn().mockResolvedValue(undefined), + }; + // @ts-ignore private property + rustCrypto.crossSigningIdentity = mockCrossSigningIdentity; await rustCrypto.bootstrapCrossSigning({}); + expect(mockCrossSigningIdentity.bootstrapCrossSigning).toHaveBeenCalledWith({}); }); it("isSecretStorageReady", async () => { diff --git a/src/rust-crypto/CrossSigningIdentity.ts b/src/rust-crypto/CrossSigningIdentity.ts new file mode 100644 index 000000000..27eb62310 --- /dev/null +++ b/src/rust-crypto/CrossSigningIdentity.ts @@ -0,0 +1,101 @@ +/* +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 { OlmMachine, CrossSigningStatus } from "@matrix-org/matrix-sdk-crypto-js"; + +import { BootstrapCrossSigningOpts } from "../crypto-api"; +import { logger } from "../logger"; +import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { UIAuthCallback } from "../interactive-auth"; + +/** Manages the cross-signing keys for our own user. + */ +export class CrossSigningIdentity { + public constructor( + private readonly olmMachine: OlmMachine, + private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + ) {} + + /** + * Initialise our cross-signing keys by creating new keys if they do not exist, and uploading to the server + */ + public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { + if (opts.setupNewCrossSigning) { + await this.resetCrossSigning(opts.authUploadDeviceSigningKeys); + return; + } + + const olmDeviceStatus: CrossSigningStatus = await this.olmMachine.crossSigningStatus(); + const privateKeysInSecretStorage = false; // TODO + const olmDeviceHasKeys = + olmDeviceStatus.hasMaster && olmDeviceStatus.hasUserSigning && olmDeviceStatus.hasSelfSigning; + + // Log all relevant state for easier parsing of debug logs. + logger.log("bootStrapCrossSigning: starting", { + setupNewCrossSigning: opts.setupNewCrossSigning, + olmDeviceHasMaster: olmDeviceStatus.hasMaster, + olmDeviceHasUserSigning: olmDeviceStatus.hasUserSigning, + olmDeviceHasSelfSigning: olmDeviceStatus.hasSelfSigning, + privateKeysInSecretStorage, + }); + + if (!olmDeviceHasKeys && !privateKeysInSecretStorage) { + logger.log( + "bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys", + ); + await this.resetCrossSigning(opts.authUploadDeviceSigningKeys); + } else if (olmDeviceHasKeys) { + logger.log("bootStrapCrossSigning: Olm device has private keys: exporting to secret storage"); + await this.exportCrossSigningKeysToStorage(); + } else if (privateKeysInSecretStorage) { + logger.log( + "bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " + + "in secret storage, reading storage and caching locally", + ); + throw new Error("TODO"); + } + + // TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the + // server -- in which case we should call OlmDevice.bootstrap_cross_signing. How do we know? + logger.log("bootStrapCrossSigning: complete"); + } + + /** Reset our cross-signing keys + * + * This method will: + * * Tell the OlmMachine to create new keys + * * Upload the new public keys and the device signature to the server + * * Upload the private keys to SSSS, if it is set up + */ + private async resetCrossSigning(authUploadDeviceSigningKeys?: UIAuthCallback): Promise { + const outgoingRequests: Array = await this.olmMachine.bootstrapCrossSigning(true); + + logger.log("bootStrapCrossSigning: publishing keys to server"); + for (const req of outgoingRequests) { + await this.outgoingRequestProcessor.makeOutgoingRequest(req, authUploadDeviceSigningKeys); + } + await this.exportCrossSigningKeysToStorage(); + } + + /** + * Extract the cross-signing keys from the olm machine and save them to secret storage, if it is configured + * + * (If secret storage is *not* configured, we assume that the export will happen when it is set up) + */ + private async exportCrossSigningKeysToStorage(): Promise { + // TODO + } +} diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d96c916e4..acfb82dd6 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -36,6 +36,7 @@ import { IDownloadKeyResult, IQueryKeysRequest } from "../client"; import { Device, DeviceMap } from "../models/device"; import { ServerSideSecretStorage } from "../secret-storage"; import { CrossSigningKey } from "../crypto/api"; +import { CrossSigningIdentity } from "./CrossSigningIdentity"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -56,6 +57,7 @@ export class RustCrypto implements CryptoBackend { private eventDecryptor: EventDecryptor; private keyClaimManager: KeyClaimManager; private outgoingRequestProcessor: OutgoingRequestProcessor; + private crossSigningIdentity: CrossSigningIdentity; public constructor( /** The `OlmMachine` from the underlying rust crypto sdk. */ @@ -80,6 +82,7 @@ export class RustCrypto implements CryptoBackend { this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http); this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor); this.eventDecryptor = new EventDecryptor(olmMachine); + this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -337,7 +340,7 @@ export class RustCrypto implements CryptoBackend { * Implementation of {@link CryptoApi#boostrapCrossSigning} */ public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { - logger.log("Cross-signing ready"); + await this.crossSigningIdentity.bootstrapCrossSigning(opts); } /**