diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 2c1e64353b..aa37dcb755 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -20,6 +20,7 @@ import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; +import { Device } from "matrix-js-sdk/src/models/device"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; @@ -94,7 +95,7 @@ export class SetupEncryptionStore extends EventEmitter { public async fetchKeyInfo(): Promise { if (!this.started) return; // bail if we were stopped const cli = MatrixClientPeg.safeGet(); - const keys = await cli.isSecretStored("m.cross_signing.master"); + const keys = await cli.secretStorage.isStored("m.cross_signing.master"); if (keys === null || Object.keys(keys).length === 0) { this.keyId = null; this.keyInfo = null; @@ -107,11 +108,17 @@ export class SetupEncryptionStore extends EventEmitter { // do we have any other verified devices which are E2EE which we can verify against? const dehydratedDevice = await cli.getDehydratedDevice(); const ownUserId = cli.getUserId()!; - this.hasDevicesToVerifyAgainst = await asyncSome(cli.getStoredDevicesForUser(ownUserId), async (device) => { - if (!device.getIdentityKey() || (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id)) { - return false; - } - const verificationStatus = await cli.getCrypto()?.getDeviceVerificationStatus(ownUserId, device.deviceId); + const crypto = cli.getCrypto()!; + const userDevices: Iterable = + (await crypto.getUserDeviceInfo([ownUserId])).get(ownUserId)?.values() ?? []; + this.hasDevicesToVerifyAgainst = await asyncSome(userDevices, async (device) => { + // ignore the dehydrated device + if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false; + + // ignore devices without an identity key + if (!device.getIdentityKey()) return false; + + const verificationStatus = await crypto.getDeviceVerificationStatus(ownUserId, device.deviceId); return !!verificationStatus?.signedByOwner; }); @@ -220,7 +227,7 @@ export class SetupEncryptionStore extends EventEmitter { // create new cross-signing keys once that succeeds. await accessSecretStorage(async (): Promise => { const cli = MatrixClientPeg.safeGet(); - await cli.bootstrapCrossSigning({ + await cli.getCrypto()?.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest): Promise => { const cachedPassword = SdkContextClass.instance.accountPasswordStore.getPassword(); diff --git a/test/stores/SetupEncryptionStore-test.ts b/test/stores/SetupEncryptionStore-test.ts index a1df872a69..afe9c69abc 100644 --- a/test/stores/SetupEncryptionStore-test.ts +++ b/test/stores/SetupEncryptionStore-test.ts @@ -16,12 +16,15 @@ limitations under the License. import { mocked, Mocked } from "jest-mock"; import { IBootstrapCrossSigningOpts } from "matrix-js-sdk/src/crypto"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, DeviceVerificationStatus, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorage } from "matrix-js-sdk/src/secret-storage"; +import { Device } from "matrix-js-sdk/src/models/device"; +import { IDehydratedDevice } from "matrix-js-sdk/src/crypto/dehydration"; import { SdkContextClass } from "../../src/contexts/SDKContext"; import { accessSecretStorage } from "../../src/SecurityManager"; import { SetupEncryptionStore } from "../../src/stores/SetupEncryptionStore"; -import { stubClient } from "../test-utils"; +import { emitPromise, stubClient } from "../test-utils"; jest.mock("../../src/SecurityManager", () => ({ accessSecretStorage: jest.fn(), @@ -30,10 +33,25 @@ jest.mock("../../src/SecurityManager", () => ({ describe("SetupEncryptionStore", () => { const cachedPassword = "p4assword"; let client: Mocked; + let mockCrypto: Mocked; + let mockSecretStorage: Mocked; let setupEncryptionStore: SetupEncryptionStore; beforeEach(() => { client = mocked(stubClient()); + mockCrypto = { + bootstrapCrossSigning: jest.fn(), + getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), + getUserDeviceInfo: jest.fn(), + getDeviceVerificationStatus: jest.fn(), + } as unknown as Mocked; + client.getCrypto.mockReturnValue(mockCrypto); + + mockSecretStorage = { + isStored: jest.fn(), + } as unknown as Mocked; + Object.defineProperty(client, "secretStorage", { value: mockSecretStorage }); + setupEncryptionStore = new SetupEncryptionStore(); SdkContextClass.instance.accountPasswordStore.setPassword(cachedPassword); }); @@ -42,10 +60,83 @@ describe("SetupEncryptionStore", () => { SdkContextClass.instance.accountPasswordStore.clearPassword(); }); + describe("start", () => { + it("should fetch cross-signing and device info", async () => { + const fakeKey = {} as SecretStorageKeyDescriptionAesV1; + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: fakeKey }); + + const fakeDevice = new Device({ deviceId: "deviceId", userId: "", algorithms: [], keys: new Map() }); + mockCrypto.getUserDeviceInfo.mockResolvedValue( + new Map([[client.getSafeUserId(), new Map([[fakeDevice.deviceId, fakeDevice]])]]), + ); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + + // our fake device is not signed, so we can't verify against it + expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(false); + + expect(setupEncryptionStore.keyId).toEqual("sskeyid"); + expect(setupEncryptionStore.keyInfo).toBe(fakeKey); + }); + + it("should spot a signed device", async () => { + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); + + const fakeDevice = new Device({ + deviceId: "deviceId", + userId: "", + algorithms: [], + keys: new Map([["curve25519:deviceId", "identityKey"]]), + }); + mockCrypto.getUserDeviceInfo.mockResolvedValue( + new Map([[client.getSafeUserId(), new Map([[fakeDevice.deviceId, fakeDevice]])]]), + ); + mockCrypto.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ signedByOwner: true }), + ); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + + expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(true); + }); + + it("should ignore the dehydrated device", async () => { + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); + + client.getDehydratedDevice.mockResolvedValue({ device_id: "dehydrated" } as IDehydratedDevice); + + const fakeDevice = new Device({ + deviceId: "dehydrated", + userId: "", + algorithms: [], + keys: new Map([["curve25519:dehydrated", "identityKey"]]), + }); + mockCrypto.getUserDeviceInfo.mockResolvedValue( + new Map([[client.getSafeUserId(), new Map([[fakeDevice.deviceId, fakeDevice]])]]), + ); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + + expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(false); + expect(mockCrypto.getDeviceVerificationStatus).not.toHaveBeenCalled(); + }); + + it("should correctly handle getUserDeviceInfo() returning an empty map", async () => { + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); + mockCrypto.getUserDeviceInfo.mockResolvedValue(new Map()); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(false); + }); + }); + it("resetConfirm should work with a cached account password", async () => { const makeRequest = jest.fn(); - client.hasSecretStorageKey.mockResolvedValue(true); - client.bootstrapCrossSigning.mockImplementation(async (opts: IBootstrapCrossSigningOpts) => { + mockCrypto.bootstrapCrossSigning.mockImplementation(async (opts: IBootstrapCrossSigningOpts) => { await opts?.authUploadDeviceSigningKeys?.(makeRequest); }); mocked(accessSecretStorage).mockImplementation(async (func?: () => Promise) => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index b339698615..9c08547538 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -239,6 +239,7 @@ export function createTestClient(): MatrixClient { setDeviceVerified: jest.fn(), joinRoom: jest.fn(), getSyncStateData: jest.fn(), + getDehydratedDevice: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client);