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 16 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
102 changes: 78 additions & 24 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,56 @@ 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
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request
*/
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" }, {});
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",
},
{},
);
}

/**
* Create cross-signing keys, publish the keys
* Mock and bootstrap all the required steps
*
* @param authDict - The parameters which are submitted as the `auth` dict in a UIA request
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function mockCrossSigningRequests(authDict: IAuthDict): Promise<void> {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
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 () => {
// 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",
},
{},
);
mockSetupCrossSigningRequests();

// 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.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
await mockCrossSigningRequests(authDict);

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

describe("getCrossSigningStatus()", () => {
it("should return correct values without bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();

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

// Expect the cross signing keys to be unavailable
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: false,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
});
});

it("should return correct values after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();

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

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

// Expect the cross signing keys to be available
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: true,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
});
});
});
});
69 changes: 69 additions & 0 deletions spec/unit/rust-crypto/secret-storage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
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 { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";

describe("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => {
it("should return false when there is no secret storage master key", async () => {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
const secretStorage = {
isStored: jest.fn().mockReturnValue(false),
} as unknown as ServerSideSecretStorage;

const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});

it("should return false when there is no shared secret storage key between master, user signing and self signing keys", async () => {
const secretStorage = {
isStored: (type: string) => {
// Return different storage keys
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
else return { secretStorageKey2: {} };
},
} as unknown as ServerSideSecretStorage;

const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});

it("should return false when the secret storage master key is only shared by user signing the secret storage", async () => {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
const secretStorage = {
isStored: (type: string) => {
// Return different storage keys
if (type === "m.cross_signing.master" || type === "m.cross_signing.user_signing") {
return { secretStorageKey: {} };
} else {
return { secretStorageKey2: {} };
}
},
} as unknown as ServerSideSecretStorage;

const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});

it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
} as unknown as ServerSideSecretStorage;

const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeTruthy();
});
});
});
29 changes: 29 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ export interface CryptoApi {
* @returns True if secret storage is ready to be used on this device
*/
isSecretStorageReady(): Promise<boolean>;

/**
* Get the status of our cross-signing keys.
*
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and whether the private keys are in secret storage.
*/
getCrossSigningStatus(): Promise<CrossSigningStatus>;
}

/**
Expand Down Expand Up @@ -263,3 +270,25 @@ export class DeviceVerificationStatus {
}

export * from "./crypto-api/verification";

/**
* The result of a call to {@link CryptoApi.getCrossSigningStatus}.
*/
export interface CrossSigningStatus {
/**
* True if the public master, self signing and user signing keys are available on this device.
*/
publicKeysOnDevice: boolean;
/**
* True if the private keys are stored in the secret storage.
*/
privateKeysInSecretStorage: boolean;
/**
* True if the private keys are stored locally.
*/
privateKeysCachedLocally: {
masterKey: boolean;
selfSigningKey: boolean;
userSigningKey: boolean;
};
}
26 changes: 25 additions & 1 deletion src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ import {
ServerSideSecretStorageImpl,
} from "../secret-storage";
import { ISecretRequest } from "./SecretSharing";
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningStatus, DeviceVerificationStatus } from "../crypto-api";
import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";

Expand Down Expand Up @@ -744,6 +744,30 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
}

/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
const publicKeysOnDevice = 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 {
publicKeysOnDevice,
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
33 changes: 30 additions & 3 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import { RoomEncryptor } from "./RoomEncryptor";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { KeyClaimManager } from "./KeyClaimManager";
import { MapWithDefault } from "../utils";
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningStatus, DeviceVerificationStatus } from "../crypto-api";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
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";
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";

/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
Expand Down Expand Up @@ -71,13 +72,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 +351,32 @@ export class RustCrypto implements CryptoBackend {
return false;
}

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

return {
publicKeysOnDevice,
privateKeysInSecretStorage,
privateKeysCachedLocally: {
masterKey: Boolean(crossSigningStatus?.hasMaster),
userSigningKey: Boolean(crossSigningStatus?.hasUserSigning),
selfSigningKey: Boolean(crossSigningStatus?.hasSelfSigning),
},
};
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
Expand Down
42 changes: 42 additions & 0 deletions src/rust-crypto/secret-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
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 { ServerSideSecretStorage } from "../secret-storage";

/**
* Check that the private cross signing keys (master, self signing, user signing) are stored into the secret storage and encrypted with the secret storage key.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param secretStorage - The secret store using account data
* @returns True if one of the secret storage master keys is shared with the secret storage user signing and self signing keys.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
// Get the secret storage keys stored into the secret storage
florianduros marked this conversation as resolved.
Show resolved Hide resolved
const secretStorageMasterKeys = await secretStorage.isStored("m.cross_signing.master");

// Not stored keys
florianduros marked this conversation as resolved.
Show resolved Hide resolved
if (!secretStorageMasterKeys) return false;

// Get the user signing keys stored into the secret storage
const secretStorageUserSigningKeys = (await secretStorage.isStored(`m.cross_signing.user_signing`)) || {};
// Get the self signing keys stored into the secret storage
const secretStorageSelfSigningKeys = (await secretStorage.isStored(`m.cross_signing.self_signing`)) || {};

// Check that one of the secret storage master keys is shared with the secret storage user signing and self signing keys
florianduros marked this conversation as resolved.
Show resolved Hide resolved
return Object.keys(secretStorageMasterKeys).some(
(secretStorageKey) =>
secretStorageUserSigningKeys[secretStorageKey] && secretStorageSelfSigningKeys[secretStorageKey],
);
}