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.getCrossSigningStatus #3452

Merged
merged 18 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
79 changes: 52 additions & 27 deletions spec/integ/crypto/cross-signing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";

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

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

describe("bootstrapCrossSigning (before initialsync completes)", () => {
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", {});

// 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
*
* @return the IAuthDict
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
async function setupCrossSigning(): Promise<IAuthDict> {
// 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" }, {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to document these endpoint names, in the doc-comments for mockCrossSigningRequest. They form part of the interface of those functions, which the test is relying on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be clear, by "endpoint names", I mean things like 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",
},
{},
);

// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};

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

// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};
return authDict;
}

// now bootstrap cross signing, and check it resolves successfully
await aliceClient.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
describe("bootstrapCrossSigning (before initialsync completes)", () => {
it("publishes keys if none were yet published", async () => {
const authDict = await setupCrossSigning();

// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
Expand All @@ -114,4 +126,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
);
});
});

it("Get cross signing status for Alice", async () => {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
await setupCrossSigning();

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

// Expect the cross signing keys to be available
expect(crossSigningStatus).toStrictEqual({
publicKeyOnDevice: true,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
});
});
});
13 changes: 13 additions & 0 deletions src/@types/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,16 @@ export interface IOneTimeKey {
fallback?: boolean;
signatures?: ISignatures;
}

/**
* The result of a call to {@link Crypto.getCrossSigningStatus}
*/
export interface ICrossSigningStatus {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
publicKeyOnDevice: boolean;
florianduros marked this conversation as resolved.
Show resolved Hide resolved
privateKeysInSecretStorage: boolean;
privateKeysCachedLocally: {
masterKey: boolean;
selfSigningKey: boolean;
userSigningKey: boolean;
};
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}
12 changes: 12 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { ICrossSigningStatus } from "./@types/crypto";

/** Types of cross-signing key */
export enum CrossSigningKey {
Expand Down Expand Up @@ -185,6 +186,17 @@ export interface CryptoApi {
* @returns True if secret storage is ready to be used on this device
*/
isSecretStorageReady(): Promise<boolean>;

/**
* Check the cross signing status of this device
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* - if there is a public key on this device
* - if the private master, userSigning and selfSigning keys are store in the SecretStorage
* - if the private master, userSigning and selfSigning keys are cached locally
florianduros marked this conversation as resolved.
Show resolved Hide resolved

florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @returns the current cross signing status of this device
*/
getCrossSigningStatus(): Promise<ICrossSigningStatus>;
}

/**
Expand Down
32 changes: 31 additions & 1 deletion src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ limitations under the License.
import anotherjson from "another-json";
import { v4 as uuidv4 } from "uuid";

import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
import type {
ICrossSigningStatus,
IDeviceKeys,
IEventDecryptionResult,
IMegolmSessionData,
IOneTimeKey,
} from "../@types/crypto";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { EventType, ToDeviceMessageId } from "../@types/event";
import { TypedReEmitter } from "../ReEmitter";
Expand Down Expand Up @@ -744,6 +750,30 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
}

/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/
public async getCrossSigningStatus(): Promise<ICrossSigningStatus> {
const publicKeyOnDevice = Boolean(this.crossSigningInfo.getId());
const privateKeysInSecretStorage = Boolean(
await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage),
);
const cacheCallbacks = this.crossSigningInfo.getCacheCallbacks();
const masterKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("master"));
const selfSigningKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("self_signing"));
const userSigningKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("user_signing"));

return {
publicKeyOnDevice,
privateKeysInSecretStorage,
privateKeysCachedLocally: {
masterKey,
selfSigningKey,
userSigningKey,
},
};
}

/**
* Bootstrap cross-signing by creating keys if needed. If everything is already
* set up, then no changes are made, so this is safe to run to ensure
Expand Down
62 changes: 58 additions & 4 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";

import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { ICrossSigningStatus, IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api";
import { MatrixEvent } from "../models/event";
Expand All @@ -34,7 +34,7 @@ import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-a
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device";
import { ServerSideSecretStorage } from "../secret-storage";
import { SecretStorageKeyDescription, ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningKey } from "../crypto/api";
import { CrossSigningIdentity } from "./CrossSigningIdentity";

Expand Down Expand Up @@ -71,13 +71,13 @@ export class RustCrypto implements CryptoBackend {
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,

/** The local user's User ID. */
_userId: string,
private readonly userId: string,

/** The local user's Device ID. */
_deviceId: string,

/** Interface to server-side secret storage */
_secretStorage: ServerSideSecretStorage,
private readonly secretStorage: ServerSideSecretStorage,
) {
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
Expand Down Expand Up @@ -350,6 +350,60 @@ export class RustCrypto implements CryptoBackend {
return false;
}

/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/
public async getCrossSigningStatus(): Promise<ICrossSigningStatus> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const publicKeyOnDevice =
Boolean(userIdentity.masterKey) &&
Boolean(userIdentity.selfSigningKey) &&
Boolean(userIdentity.userSigningKey);
const privateKeysInSecretStorage = Boolean(await this.isStoredInSecretStorage(this.secretStorage));
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus();

return {
publicKeyOnDevice,
privateKeysInSecretStorage,
privateKeysCachedLocally: {
masterKey: crossSigningStatus.hasMaster,
userSigningKey: crossSigningStatus.hasUserSigning,
selfSigningKey: crossSigningStatus.hasSelfSigning,
},
};
}

/**
* Check whether the private keys exist in secret storage.
* XXX: This could be static, be we often seem to have an instance when we
* want to know this anyway...
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param secretStorage - The secret store using account data
* @returns map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
private async isStoredInSecretStorage(
secretStorage: ServerSideSecretStorage,
): Promise<Record<string, object> | null> {
// check what SSSS keys have encrypted the master key (if any)
const stored = (await secretStorage.isStored("m.cross_signing.master")) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s: Record<string, SecretStorageKeyDescription>): void {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
}
}
}
for (const type of ["self_signing", "user_signing"]) {
intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
}
return Object.keys(stored).length ? stored : null;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
Expand Down