diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index 3e03461e5..04ade62c1 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -22,7 +22,7 @@ import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test- import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src"; import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints"; import { encryptAES } from "../../../src/crypto/aes"; -import { CryptoCallbacks } from "../../../src/crypto-api"; +import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; @@ -288,4 +288,55 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s expect(isCrossSigningReady).toBeTruthy(); }); }); + + describe("getCrossSigningKeyId", () => { + /** + * Intercept /keys/device_signing/upload request and return the cross signing keys + * https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload + * + * @returns the cross signing keys + */ + function awaitCrossSigningKeysUpload() { + return new Promise((resolve) => { + fetchMock.post( + // legacy crypto uses /unstable/; /v3/ is correct + { + url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"), + name: "upload-keys", + }, + (url, options) => { + const content = JSON.parse(options.body as string); + resolve(content); + return {}; + }, + // Override the routes define in `mockSetupCrossSigningRequests` + { overwriteRoutes: true }, + ); + }); + } + + it("should return the cross signing key id for each cross signing key", async () => { + mockSetupCrossSigningRequests(); + + // Intercept cross signing keys upload + const crossSigningKeysPromise = awaitCrossSigningKeysUpload(); + + // provide a UIA callback, so that the cross-signing keys are uploaded + const authDict = { type: "test" }; + await bootstrapCrossSigning(authDict); + // Get the cross signing keys + const crossSigningKeys = await crossSigningKeysPromise; + + const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0]; + + const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(); + expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key)); + + const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning); + expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key)); + + const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning); + expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key)); + }); + }); }); diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 04533eca3..52a69c3e0 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -168,7 +168,7 @@ describe("RustCrypto", () => { }); }); - it("getCrossSigningKeyId", async () => { + it("getCrossSigningKeyId when there is no cross signing keys", async () => { const rustCrypto = await makeTestRustCrypto(); await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null); }); diff --git a/src/client.ts b/src/client.ts index 6ca1a5ba1..376144bff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -210,7 +210,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications"; import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; import { CryptoBackend } from "./common-crypto/CryptoBackend"; import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants"; -import { BootstrapCrossSigningOpts, CryptoApi, ImportRoomKeysOpts } from "./crypto-api"; +import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CryptoApi, ImportRoomKeysOpts } from "./crypto-api"; import { DeviceInfoMap } from "./crypto/DeviceList"; import { AddSecretStorageKeyOpts, @@ -524,13 +524,8 @@ export interface Capabilities { [UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability; } -/* eslint-disable camelcase */ -export interface ICrossSigningKey { - keys: { [algorithm: string]: string }; - signatures?: ISignatures; - usage: string[]; - user_id: string; -} +/** @deprecated prefer {@link CrossSigningKeyInfo}. */ +export type ICrossSigningKey = CrossSigningKeyInfo; enum CrossSigningKeyType { MasterKey = "master_key", diff --git a/src/crypto-api.ts b/src/crypto-api.ts index b3ee1a6a4..9045cfe05 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -21,6 +21,7 @@ import { UIAuthCallback } from "./interactive-auth"; import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage"; import { VerificationRequest } from "./crypto-api/verification"; import { KeyBackupInfo } from "./crypto-api/keybackup"; +import { ISignatures } from "./@types/signed"; /** * Public interface to the cryptography parts of the js-sdk @@ -503,6 +504,17 @@ export enum CrossSigningKey { UserSigning = "user_signing", } +/** + * Information on one of the cross-signing keys. + * @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload + */ +export interface CrossSigningKeyInfo { + keys: { [algorithm: string]: string }; + signatures?: ISignatures; + usage: string[]; + user_id: string; +} + /** * Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase} */ diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 125309f95..a900e21a9 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -41,6 +41,7 @@ import { ImportRoomKeyProgressData, ImportRoomKeysOpts, VerificationRequest, + CrossSigningKeyInfo, } from "../crypto-api"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client"; @@ -417,8 +418,45 @@ export class RustCrypto extends TypedEventEmitter { - // TODO - return null; + const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity( + new RustSdkCryptoJs.UserId(this.userId), + ); + + const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus(); + const privateKeysOnDevice = + crossSigningStatus.hasMaster && crossSigningStatus.hasUserSigning && crossSigningStatus.hasSelfSigning; + + if (!userIdentity || !privateKeysOnDevice) { + // The public or private keys are not available on this device + return null; + } + + if (!userIdentity.isVerified()) { + // We have both public and private keys, but they don't match! + return null; + } + + let key: string; + switch (type) { + case CrossSigningKey.Master: + key = userIdentity.masterKey; + break; + case CrossSigningKey.SelfSigning: + key = userIdentity.selfSigningKey; + break; + case CrossSigningKey.UserSigning: + key = userIdentity.userSigningKey; + break; + default: + // Unknown type + return null; + } + + const parsedKey: CrossSigningKeyInfo = JSON.parse(key); + // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } + // We assume only a single key, and we want the bare form without type + // prefix, so we select the values. + return Object.values(parsedKey.keys)[0]; } /**