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

New CryptoApi.getDeviceVerificationStatus api #3287

Merged
merged 8 commits into from
Apr 18, 2023
38 changes: 38 additions & 0 deletions spec/integ/matrix-client-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";

import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
Expand All @@ -24,6 +25,7 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";

describe("MatrixClient", function () {
const userId = "@alice:localhost";
Expand Down Expand Up @@ -1412,6 +1414,42 @@ describe("MatrixClient", function () {
await client!.uploadKeys();
});
});

describe("getCryptoTrustCrossSignedDevices", () => {
it("should throw if e2e is disabled", () => {
expect(() => client!.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled");
});

it("should proxy to the crypto backend", async () => {
const mockBackend = {
getTrustCrossSignedDevices: jest.fn().mockReturnValue(true),
} as unknown as Mocked<CryptoBackend>;
client!["cryptoBackend"] = mockBackend;

expect(client!.getCryptoTrustCrossSignedDevices()).toBe(true);
mockBackend.getTrustCrossSignedDevices.mockReturnValue(false);
expect(client!.getCryptoTrustCrossSignedDevices()).toBe(false);
});
});

describe("setCryptoTrustCrossSignedDevices", () => {
it("should throw if e2e is disabled", () => {
expect(() => client!.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled");
});

it("should proxy to the crypto backend", async () => {
const mockBackend = {
setTrustCrossSignedDevices: jest.fn(),
} as unknown as Mocked<CryptoBackend>;
client!["cryptoBackend"] = mockBackend;

client!.setCryptoTrustCrossSignedDevices(true);
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true);

client!.setCryptoTrustCrossSignedDevices(false);
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false);
});
});
});

function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
Expand Down
17 changes: 17 additions & 0 deletions spec/unit/crypto/verification/sas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,30 @@ describe("SAS verification", function () {
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();

const bobDeviceVerificationStatus = (await alice.client
.getCrypto()!
.getDeviceVerificationStatus("@bob:example.com", "Dynabook"))!;
expect(bobDeviceVerificationStatus.localVerified).toBe(true);
expect(bobDeviceVerificationStatus.crossSigningVerified).toBe(false);

const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy();

const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();

const aliceDeviceVerificationStatus = (await bob.client
.getCrypto()!
.getDeviceVerificationStatus("@alice:example.com", "Osborne2"))!;
expect(aliceDeviceVerificationStatus.localVerified).toBe(true);
expect(aliceDeviceVerificationStatus.crossSigningVerified).toBe(false);

const unknownDeviceVerificationStatus = await bob.client
.getCrypto()!
.getDeviceVerificationStatus("@alice:example.com", "xyz");
expect(unknownDeviceVerificationStatus).toBe(null);
});
});

Expand Down
51 changes: 50 additions & 1 deletion spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022-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.
Expand Down Expand Up @@ -230,4 +230,53 @@ describe("RustCrypto", () => {
expect(res.encrypted).toBeTruthy();
});
});

describe("get|setTrustCrossSignedDevices", () => {
let rustCrypto: RustCrypto;

beforeEach(async () => {
rustCrypto = await initRustCrypto({} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
});

it("should be true by default", () => {
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
});

it("should be easily turn-off-and-on-able", () => {
rustCrypto.setTrustCrossSignedDevices(false);
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(false);
rustCrypto.setTrustCrossSignedDevices(true);
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
});
});

describe("getDeviceVerificationStatus", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;

beforeEach(() => {
olmMachine = {
getDevice: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(olmMachine, {} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
});

it("should call getDevice", async () => {
olmMachine.getDevice.mockResolvedValue({
isCrossSigningTrusted: jest.fn().mockReturnValue(false),
isLocallyTrusted: jest.fn().mockReturnValue(false),
} as unknown as RustSdkCryptoJs.Device);
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
expect(olmMachine.getDevice.mock.calls[0][0].toString()).toEqual("@user:domain");
expect(olmMachine.getDevice.mock.calls[0][1].toString()).toEqual("device");
expect(res?.crossSigningVerified).toBe(false);
expect(res?.localVerified).toBe(false);
});

it("should return null for unknown device", async () => {
olmMachine.getDevice.mockResolvedValue(undefined);
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
expect(res).toBe(null);
});
});
});
18 changes: 12 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2620,12 +2620,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param userId - The ID of the user whose devices is to be checked.
* @param deviceId - The ID of the device to check
*
* @deprecated Use {@link CryptoApi.getDeviceVerificationStatus | `CryptoApi.getDeviceVerificationStatus`}
*/
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
if (!this.cryptoBackend) {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.cryptoBackend.checkDeviceTrust(userId, deviceId);
return this.crypto.checkDeviceTrust(userId, deviceId);
}

/**
Expand Down Expand Up @@ -2763,24 +2765,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Default: true
*
* @returns True if trusting cross-signed devices
*
* @deprecated Prefer {@link CryptoApi.getTrustCrossSignedDevices | `CryptoApi.getTrustCrossSignedDevices`}.
*/
public getCryptoTrustCrossSignedDevices(): boolean {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.getCryptoTrustCrossSignedDevices();
return this.cryptoBackend.getTrustCrossSignedDevices();
}

/**
* See getCryptoTrustCrossSignedDevices
*
* @param val - True to trust cross-signed devices
*
* @deprecated Prefer {@link CryptoApi.setTrustCrossSignedDevices | `CryptoApi.setTrustCrossSignedDevices`}.
*/
public setCryptoTrustCrossSignedDevices(val: boolean): void {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
this.crypto.setCryptoTrustCrossSignedDevices(val);
this.cryptoBackend.setTrustCrossSignedDevices(val);
}

/**
Expand Down
12 changes: 1 addition & 11 deletions src/common-crypto/CryptoBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import { MatrixEvent } from "../models/event";
import { Room } from "../models/room";
import { CryptoApi } from "../crypto-api";
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
import { UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api";
import { IEventDecryptionResult } from "../@types/crypto";

Expand Down Expand Up @@ -51,16 +51,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
*/
checkUserTrust(userId: string): UserTrustLevel;

/**
* Get the verification level for a given device
*
* TODO: define this better
*
* @param userId - user to be checked
* @param deviceId - device to be checked
*/
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;

/**
* Encrypt an event according to the configuration of the room.
*
Expand Down
71 changes: 71 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,75 @@ export interface CryptoApi {
* session export objects
*/
exportRoomKeys(): Promise<IMegolmSessionData[]>;

/**
* Set whether to trust other user's signatures of their devices.
*
* If false, devices will only be considered 'verified' if we have
* verified that device individually (effectively disabling cross-signing).
*
* `true` by default.
*
* @param val - the new value
*/
setTrustCrossSignedDevices(val: boolean): void;

/**
* Return whether we trust other user's signatures of their devices.
*
* @see {@link CryptoApi#setTrustCrossSignedDevices}
*
* @returns `true` if we trust cross-signed devices, otherwise `false`.
*/
getTrustCrossSignedDevices(): boolean;

/**
* Get the verification status of a given device.
*
* @param userId - The ID of the user whose device is to be checked.
* @param deviceId - The ID of the device to check
*
* @returns Verification status of the device, or `null` if the device is not known
*/
getDeviceVerificationStatus(userId: string, deviceId: string): Promise<DeviceVerificationStatus | null>;
}

export class DeviceVerificationStatus {
public constructor(
/**
* True if this device has been verified via cross signing.
*
* This does *not* take into account `trustCrossSignedDevices`.
*/
public readonly crossSigningVerified: boolean,

/**
* TODO: tofu magic wtf does this do?
*/
public readonly tofu: boolean,

/**
* True if the device has been marked as locally verified.
*/
public readonly localVerified: boolean,

/**
* True if the client has been configured to trust cross-signed devices via {@link CryptoApi#setTrustCrossSignedDevices}.
*/
private readonly trustCrossSignedDevices: boolean,
) {}

/**
* Check if we should consider this device "verified".
*
* A device is "verified" if either:
* * it has been manually marked as such via {@link MatrixClient#setDeviceVerified}.
* * it has been cross-signed with a verified signing key, **and** the client has been configured to trust
* cross-signed devices via {@link CryptoApi#setTrustCrossSignedDevices}.
*
* @returns true if this device is verified via any means.
*/
public isVerified(): boolean {
return this.localVerified || (this.trustCrossSignedDevices && this.crossSigningVerified);
}
}
21 changes: 5 additions & 16 deletions src/crypto/CrossSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ICryptoCallbacks } from ".";
import { ISignatures } from "../@types/signed";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
import { DeviceVerificationStatus } from "../crypto-api";

const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;

Expand Down Expand Up @@ -628,16 +629,11 @@ export class UserTrustLevel {
}

/**
* Represents the ways in which we trust a device
* Represents the ways in which we trust a device.
*
* @deprecated Use {@link DeviceVerificationStatus}.
*/
export class DeviceTrustLevel {
public constructor(
public readonly crossSigningVerified: boolean,
public readonly tofu: boolean,
private readonly localVerified: boolean,
private readonly trustCrossSignedDevices: boolean,
) {}

export class DeviceTrustLevel extends DeviceVerificationStatus {
public static fromUserTrustLevel(
userTrustLevel: UserTrustLevel,
localVerified: boolean,
Expand All @@ -651,13 +647,6 @@ export class DeviceTrustLevel {
);
}

/**
* @returns true if this device is verified via any means
*/
public isVerified(): boolean {
return Boolean(this.isLocallyVerified() || (this.trustCrossSignedDevices && this.isCrossSigningVerified()));
}

/**
* @returns true if this device is verified via cross signing
*/
Expand Down
Loading