From ba1ca5cd4bcd2c2b4ce63672a119a51baec36de0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 16 Jan 2025 14:36:53 +0100 Subject: [PATCH] Remove tests using `MatrixClient.initLegacyCrypto` --- spec/integ/crypto/crypto.spec.ts | 370 ----- spec/integ/crypto/olm-encryption-spec.ts | 693 -------- spec/integ/devicelist-integ.spec.ts | 406 ----- spec/integ/matrix-client-methods.spec.ts | 130 +- spec/integ/matrix-client-syncing.spec.ts | 286 ---- spec/integ/sliding-sync-sdk.spec.ts | 62 +- spec/test-utils/test-utils.ts | 3 - spec/unit/crypto.spec.ts | 1467 ----------------- spec/unit/crypto/algorithms/megolm.spec.ts | 511 +----- spec/unit/crypto/backup.spec.ts | 579 +------ spec/unit/crypto/cross-signing.spec.ts | 1152 ------------- spec/unit/crypto/dehydration.spec.ts | 76 - spec/unit/crypto/secrets.spec.ts | 697 -------- spec/unit/crypto/verification/request.spec.ts | 80 - spec/unit/crypto/verification/sas.spec.ts | 520 +----- spec/unit/crypto/verification/util.ts | 129 -- 16 files changed, 7 insertions(+), 7154 deletions(-) delete mode 100644 spec/integ/crypto/olm-encryption-spec.ts delete mode 100644 spec/integ/devicelist-integ.spec.ts delete mode 100644 spec/unit/crypto.spec.ts delete mode 100644 spec/unit/crypto/cross-signing.spec.ts delete mode 100644 spec/unit/crypto/secrets.spec.ts delete mode 100644 spec/unit/crypto/verification/request.spec.ts delete mode 100644 spec/unit/crypto/verification/util.ts diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index d3f5e20f719..466a46e39a7 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -44,7 +44,6 @@ import { TEST_ROOM_ID as ROOM_ID, TEST_USER_ID, } from "../../test-utils/test-data"; -import { TestClient } from "../../TestClient"; import { logger } from "../../../src/logger"; import { Category, @@ -63,7 +62,6 @@ import { MatrixEventEvent, MsgType, PendingEventOrdering, - Room, RoomMember, RoomStateEvent, } from "../../../src/matrix"; @@ -97,7 +95,6 @@ import { encryptGroupSessionKey, encryptMegolmEvent, encryptMegolmEventRawPlainText, - encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys, } from "./olm-utils"; @@ -1730,64 +1727,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(event.getContent().body).toEqual("42"); }); - it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", async () => { - const testClient = new TestClient("@alice:localhost", "device2", "access_token2"); - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - const inboundGroupSession = new Olm.InboundGroupSession(); - inboundGroupSession.create(groupSession.session_key()); - const rawEvent = encryptMegolmEvent({ - senderKey: testSenderKey, - groupSession: groupSession, - room_id: ROOM_ID, - }); - await testClient.client.initLegacyCrypto(); - const keys = [ - { - room_id: ROOM_ID, - algorithm: "m.megolm.v1.aes-sha2", - session_id: groupSession.session_id(), - session_key: inboundGroupSession.export_session(0), - sender_key: testSenderKey, - forwarding_curve25519_key_chain: [], - sender_claimed_keys: {}, - }, - ]; - await testClient.client.importRoomKeys(keys, { untrusted: true }); - - const event1 = testUtils.mkEvent({ - event: true, - ...rawEvent, - room: ROOM_ID, - }); - await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true }); - expect(event1.isKeySourceUntrusted()).toBeTruthy(); - - const event2 = testUtils.mkEvent({ - type: "m.room_key", - content: { - room_id: ROOM_ID, - algorithm: "m.megolm.v1.aes-sha2", - session_id: groupSession.session_id(), - session_key: groupSession.session_key(), - }, - event: true, - }); - // @ts-ignore - private - event2.senderCurve25519Key = testSenderKey; - // @ts-ignore - private - testClient.client.crypto!.onRoomKeyEvent(event2); - - const event3 = testUtils.mkEvent({ - event: true, - ...rawEvent, - room: ROOM_ID, - }); - await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true }); - expect(event3.isKeySourceUntrusted()).toBeFalsy(); - testClient.stop(); - }); - it("Alice can decrypt a message with falsey content", async () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); @@ -1851,315 +1790,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(decryptedEvent.getClearContent()).toBeUndefined(); }); - oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => { - const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux"); - await beccaTestClient.client.initLegacyCrypto(); - - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - await beccaTestClient.start(); - - // if we're using the old crypto impl, stub out some methods in the device manager. - // TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic. - if (aliceClient.crypto) { - aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - aliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - aliceClient.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!; - } - - const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {}); - beccaTestClient.client.store.storeRoom(beccaRoom); - await beccaTestClient.client.setRoomEncryption(ROOM_ID, { algorithm: "m.megolm.v1.aes-sha2" }); - - const event = new MatrixEvent({ - type: "m.room.message", - sender: "@becca:localhost", - room_id: ROOM_ID, - event_id: "$1", - content: { - msgtype: "m.text", - body: "test message", - }, - }); - - await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - - const device = new DeviceInfo(beccaTestClient.client.deviceId!); - - // Create an olm session for Becca and Alice's devices - const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload(); - const aliceOtkId = Object.keys(aliceOtks)[0]; - const aliceOtk = aliceOtks[aliceOtkId]; - const p2pSession = new globalThis.Olm.Session(); - await beccaTestClient.client.crypto!.cryptoStore.doTxn( - "readonly", - [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => { - const account = new globalThis.Olm.Account(); - try { - account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!); - p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key); - } finally { - account.free(); - } - }); - }, - ); - - const content = event.getWireContent(); - const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey( - ROOM_ID, - content.sender_key, - content.session_id, - ); - const encryptedForwardedKey = encryptOlmEvent({ - sender: "@becca:localhost", - senderSigningKey: beccaTestClient.getSigningKey(), - senderKey: beccaTestClient.getDeviceKey(), - recipient: aliceClient.getUserId()!, - recipientCurve25519Key: keyReceiver.getDeviceKey(), - recipientEd25519Key: keyReceiver.getSigningKey(), - p2pSession: p2pSession, - plaincontent: { - "algorithm": "m.megolm.v1.aes-sha2", - "room_id": ROOM_ID, - "sender_key": content.sender_key, - "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key, - "session_id": content.session_id, - "session_key": groupSessionKey!.key, - "chain_index": groupSessionKey!.chain_index, - "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain, - "org.matrix.msc3061.shared_history": true, - }, - plaintype: "m.forwarded_room_key", - }); - - // Alice receives shared history - syncResponder.sendOrQueueSyncResponse({ - next_batch: 1, - to_device: { events: [encryptedForwardedKey] }, - }); - await syncPromise(aliceClient); - - // Alice is invited to the room by Becca - syncResponder.sendOrQueueSyncResponse({ - next_batch: 2, - rooms: { - invite: { - [ROOM_ID]: { - invite_state: { - events: [ - { - sender: "@becca:localhost", - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - { - sender: "@becca:localhost", - type: "m.room.member", - state_key: "@alice:localhost", - content: { - membership: KnownMembership.Invite, - }, - }, - ], - }, - }, - }, - }, - }); - await syncPromise(aliceClient); - - // Alice has joined the room - expectAliceKeyQuery({ device_keys: { "@becca:localhost": {} }, failures: {} }); - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@alice:localhost", "@becca:localhost"])); - await syncPromise(aliceClient); - - syncResponder.sendOrQueueSyncResponse({ - next_batch: 4, - rooms: { - join: { - [ROOM_ID]: { timeline: { events: [event.event] } }, - }, - }, - }); - await syncPromise(aliceClient); - - const room = aliceClient.getRoom(ROOM_ID)!; - const roomEvent = room.getLiveTimeline().getEvents()[0]; - expect(roomEvent.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(roomEvent); - expect(decryptedEvent.getContent().body).toEqual("test message"); - - await beccaTestClient.stop(); - }); - - oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => { - const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux"); - await beccaTestClient.client.initLegacyCrypto(); - - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - - await beccaTestClient.start(); - - const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {}); - beccaTestClient.client.store.storeRoom(beccaRoom); - await beccaTestClient.client.setRoomEncryption(ROOM_ID, { algorithm: "m.megolm.v1.aes-sha2" }); - - const event = new MatrixEvent({ - type: "m.room.message", - sender: "@becca:localhost", - room_id: ROOM_ID, - event_id: "$1", - content: { - msgtype: "m.text", - body: "test message", - }, - }); - - await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - - const device = new DeviceInfo(beccaTestClient.client.deviceId!); - aliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - - // Create an olm session for Becca and Alice's devices - const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload(); - const aliceOtkId = Object.keys(aliceOtks)[0]; - const aliceOtk = aliceOtks[aliceOtkId]; - const p2pSession = new globalThis.Olm.Session(); - await beccaTestClient.client.crypto!.cryptoStore.doTxn( - "readonly", - [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => { - const account = new globalThis.Olm.Account(); - try { - account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!); - p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key); - } finally { - account.free(); - } - }); - }, - ); - - const content = event.getWireContent(); - const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey( - ROOM_ID, - content.sender_key, - content.session_id, - ); - const encryptedForwardedKey = encryptOlmEvent({ - sender: "@becca:localhost", - senderKey: beccaTestClient.getDeviceKey(), - senderSigningKey: beccaTestClient.getSigningKey(), - recipient: aliceClient.getUserId()!, - recipientCurve25519Key: keyReceiver.getDeviceKey(), - recipientEd25519Key: keyReceiver.getSigningKey(), - p2pSession: p2pSession, - plaincontent: { - "algorithm": "m.megolm.v1.aes-sha2", - "room_id": ROOM_ID, - "sender_key": content.sender_key, - "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key, - "session_id": content.session_id, - "session_key": groupSessionKey!.key, - "chain_index": groupSessionKey!.chain_index, - "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain, - "org.matrix.msc3061.shared_history": true, - }, - plaintype: "m.forwarded_room_key", - }); - - // Alice receives forwarded history from Becca - expectAliceKeyQuery({ device_keys: { "@becca:localhost": {} }, failures: {} }); - syncResponder.sendOrQueueSyncResponse({ - next_batch: 1, - to_device: { events: [encryptedForwardedKey] }, - }); - await syncPromise(aliceClient); - - // Alice is invited to the room by Charlie - syncResponder.sendOrQueueSyncResponse({ - next_batch: 2, - rooms: { - invite: { - [ROOM_ID]: { - invite_state: { - events: [ - { - sender: "@becca:localhost", - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - { - sender: "@charlie:localhost", - type: "m.room.member", - state_key: "@alice:localhost", - content: { - membership: KnownMembership.Invite, - }, - }, - ], - }, - }, - }, - }, - }); - await syncPromise(aliceClient); - - // Alice has joined the room - expectAliceKeyQuery({ device_keys: { "@becca:localhost": {}, "@charlie:localhost": {} }, failures: {} }); - syncResponder.sendOrQueueSyncResponse( - getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]), - ); - await syncPromise(aliceClient); - - // wait for the key/device downloads for becca and charlie to complete - await aliceClient.downloadKeys(["@becca:localhost", "@charlie:localhost"]); - - syncResponder.sendOrQueueSyncResponse({ - next_batch: 4, - rooms: { - join: { - [ROOM_ID]: { timeline: { events: [event.event] } }, - }, - }, - }); - await syncPromise(aliceClient); - - // Decryption should fail, because Alice hasn't received any keys she can trust - const room = aliceClient.getRoom(ROOM_ID)!; - const roomEvent = room.getLiveTimeline().getEvents()[0]; - expect(roomEvent.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(roomEvent); - expect(decryptedEvent.isDecryptionFailure()).toBe(true); - - await beccaTestClient.stop(); - }); - oldBackendOnly("allows sending an encrypted event as soon as room state arrives", async () => { /* Empirically, clients expect to be able to send encrypted events as soon as the * RoomStateEvent.NewMember notification is emitted, so test that works correctly. diff --git a/spec/integ/crypto/olm-encryption-spec.ts b/spec/integ/crypto/olm-encryption-spec.ts deleted file mode 100644 index 5b98c63936a..00000000000 --- a/spec/integ/crypto/olm-encryption-spec.ts +++ /dev/null @@ -1,693 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 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. -*/ - -/* This file consists of a set of integration tests which try to simulate - * communication via an Olm-encrypted room between two users, Alice and Bob. - * - * Note that megolm (group) conversation is not tested here. - * - * See also `crypto.spec.js`. - */ - -// load olm before the sdk if possible -import "../../olm-loader"; - -import type { Session } from "@matrix-org/olm"; -import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto"; -import { logger } from "../../../src/logger"; -import * as testUtils from "../../test-utils/test-utils"; -import { TestClient } from "../../TestClient"; -import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client"; -import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix"; -import { DeviceInfo } from "../../../src/crypto/deviceinfo"; -import { KnownMembership } from "../../../src/@types/membership"; - -let aliTestClient: TestClient; -const roomId = "!room:localhost"; -const aliUserId = "@ali:localhost"; -const aliDeviceId = "zxcvb"; -const aliAccessToken = "aseukfgwef"; -let bobTestClient: TestClient; -const bobUserId = "@bob:localhost"; -const bobDeviceId = "bvcxz"; -const bobAccessToken = "fewgfkuesa"; -let aliMessages: IContent[]; -let bobMessages: IContent[]; - -type OlmPayload = ReturnType; - -async function bobUploadsDeviceKeys(): Promise { - bobTestClient.expectDeviceKeyUpload(); - await bobTestClient.httpBackend.flushAllExpected(); - expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); -} - -/** - * Set an expectation that querier will query uploader's keys; then flush the http request. - * - * @returns resolves once the http request has completed. - */ -function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise { - // can't query keys before bob has uploaded them - expect(uploader.deviceKeys).toBeTruthy(); - - const uploaderKeys: Record = {}; - uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!; - querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) { - expect(content.device_keys![uploader.userId!]).toEqual([]); - const result: Record> = {}; - result[uploader.userId!] = uploaderKeys; - return { device_keys: result }; - }); - return querier.httpBackend.flush("/keys/query", 1); -} -const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient); -const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient); - -/** - * Set an expectation that ali will claim one of bob's keys; then flush the http request. - * - * @returns resolves once the http request has completed. - */ -async function expectAliClaimKeys(): Promise { - const keys = await bobTestClient.awaitOneTimeKeyUpload(); - aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) { - const claimType = content.one_time_keys![bobUserId][bobDeviceId]; - expect(claimType).toEqual("signed_curve25519"); - let keyId = ""; - for (keyId in keys) { - if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) { - if (keyId.indexOf(claimType + ":") === 0) { - break; - } - } - } - const result: Record>> = {}; - result[bobUserId] = {}; - result[bobUserId][bobDeviceId] = {}; - result[bobUserId][bobDeviceId][keyId] = keys[keyId]; - return { one_time_keys: result }; - }); - // it can take a while to process the key query, so give it some extra - // time, and make sure the claim actually happens rather than ploughing on - // confusingly. - const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500); - expect(r).toEqual(1); -} - -async function aliDownloadsKeys(): Promise { - // can't query keys before bob has uploaded them - expect(bobTestClient.getSigningKey()).toBeTruthy(); - - const p1 = async () => { - await aliTestClient.client.downloadKeys([bobUserId]); - const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId); - expect(devices.length).toEqual(1); - expect(devices[0].deviceId).toEqual("bvcxz"); - }; - const p2 = expectAliQueryKeys; - - // check that the localStorage is updated as we expect (not sure this is - // an integration test, but meh) - await Promise.all([p1(), p2()]); - await aliTestClient.client.crypto!.deviceList.saveIfDirty(); - // @ts-ignore - protected - aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const devices = data!.devices[bobUserId]!; - expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys); - expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED); - }); -} - -async function clientEnablesEncryption(client: MatrixClient): Promise { - await client.setRoomEncryption(roomId, { - algorithm: "m.olm.v1.curve25519-aes-sha2", - }); - expect(client.isRoomEncrypted(roomId)).toBeTruthy(); -} -const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client); -const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client); - -/** - * Ali sends a message, first claiming e2e keys. Set the expectations and - * check the results. - * - * @returns which resolves to the ciphertext for Bob's device. - */ -async function aliSendsFirstMessage(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, ciphertext] = await Promise.all([ - sendMessage(aliTestClient.client), - expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest), - ]); - return ciphertext; -} - -/** - * Ali sends a message without first claiming e2e keys. Set the expectations - * and check the results. - * - * @returns which resolves to the ciphertext for Bob's device. - */ -async function aliSendsMessage(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]); - return ciphertext; -} - -/** - * Bob sends a message, first querying (but not claiming) e2e keys. Set the - * expectations and check the results. - * - * @returns which resolves to the ciphertext for Ali's device. - */ -async function bobSendsReplyMessage(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, ciphertext] = await Promise.all([ - sendMessage(bobTestClient.client), - expectBobQueryKeys().then(expectBobSendMessageRequest), - ]); - return ciphertext; -} - -/** - * Set an expectation that Ali will send a message, and flush the request - * - * @returns which resolves to the ciphertext for Bob's device. - */ -async function expectAliSendMessageRequest(): Promise { - const content = await expectSendMessageRequest(aliTestClient.httpBackend); - aliMessages.push(content); - expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]); - const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()]; - expect(ciphertext).toBeTruthy(); - return ciphertext; -} - -/** - * Set an expectation that Bob will send a message, and flush the request - * - * @returns which resolves to the ciphertext for Bob's device. - */ -async function expectBobSendMessageRequest(): Promise { - const content = await expectSendMessageRequest(bobTestClient.httpBackend); - bobMessages.push(content); - const aliKeyId = "curve25519:" + aliDeviceId; - const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId]; - expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]); - const ciphertext = content.ciphertext[aliDeviceCurve25519Key]; - expect(ciphertext).toBeTruthy(); - return ciphertext; -} - -function sendMessage(client: MatrixClient): Promise { - return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" }); -} - -async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise { - const path = "/send/m.room.encrypted/"; - const prom = new Promise((resolve) => { - httpBackend.when("PUT", path).respond(200, function (_path, content) { - resolve(content); - return { - event_id: "asdfgh", - }; - }); - }); - - // it can take a while to process the key query - await httpBackend.flush(path, 1); - return prom; -} - -function aliRecvMessage(): Promise { - const message = bobMessages.shift()!; - return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message); -} - -function bobRecvMessage(): Promise { - const message = aliMessages.shift()!; - return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message); -} - -async function recvMessage( - httpBackend: TestClient["httpBackend"], - client: MatrixClient, - sender: string, - message: IContent, -): Promise { - const syncData = { - next_batch: "x", - rooms: { - join: { - [roomId]: { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: sender, - }), - ], - }, - }, - }, - }, - }; - httpBackend.when("GET", "/sync").respond(200, syncData); - - const eventPromise = new Promise((resolve) => { - const onEvent = function (event: MatrixEvent) { - // ignore the m.room.member events - if (event.getType() == "m.room.member") { - return; - } - logger.log(client.credentials.userId + " received event", event); - - client.removeListener(ClientEvent.Event, onEvent); - resolve(event); - }; - client.on(ClientEvent.Event, onEvent); - }); - - await httpBackend.flushAllExpected(); - - const preDecryptionEvent = await eventPromise; - expect(preDecryptionEvent.isEncrypted()).toBeTruthy(); - // it may still be being decrypted - const event = await testUtils.awaitDecryption(preDecryptionEvent); - expect(event.getType()).toEqual("m.room.message"); - expect(event.getContent()).toMatchObject({ - msgtype: "m.text", - body: "Hello, World", - }); - expect(event.isEncrypted()).toBeTruthy(); -} - -/** - * Send an initial sync response to the client (which just includes the member - * list for our test room). - * - * @returns which resolves when the sync has been flushed. - */ -function firstSync(testClient: TestClient): Promise { - // send a sync response including our test room. - const syncData = { - next_batch: "x", - rooms: { - join: { - [roomId]: { - state: { - events: [ - testUtils.mkMembership({ - mship: KnownMembership.Join, - user: aliUserId, - }), - testUtils.mkMembership({ - mship: KnownMembership.Join, - user: bobUserId, - }), - ], - }, - timeline: { - events: [], - }, - }, - }, - }, - }; - - testClient.httpBackend.when("GET", "/sync").respond(200, syncData); - return testClient.flushSync(); -} - -describe("MatrixClient crypto", () => { - if (!CRYPTO_ENABLED) { - return; - } - - beforeEach(async () => { - aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken); - await aliTestClient.client.initLegacyCrypto(); - - bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken); - await bobTestClient.client.initLegacyCrypto(); - - aliMessages = []; - bobMessages = []; - }); - - afterEach(() => { - aliTestClient.httpBackend.verifyNoOutstandingExpectation(); - bobTestClient.httpBackend.verifyNoOutstandingExpectation(); - - return Promise.all([aliTestClient.stop(), bobTestClient.stop()]); - }); - - it("Bob uploads device keys", bobUploadsDeviceKeys); - - it("handles failures to upload device keys", async () => { - // since device keys are uploaded asynchronously, there's not really much to do here other than fail the - // upload. - bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh")); - await bobTestClient.httpBackend.flushAllExpected(); - }); - - it("Ali downloads Bobs device keys", async () => { - await bobUploadsDeviceKeys(); - await aliDownloadsKeys(); - }); - - it("Ali gets keys with an invalid signature", async () => { - await bobUploadsDeviceKeys(); - // tamper bob's keys - const bobDeviceKeys = bobTestClient.deviceKeys!; - expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy(); - bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc"; - await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]); - const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId); - // should get an empty list - expect(devices).toEqual([]); - }); - - it("Ali gets keys with an incorrect userId", async () => { - const eveUserId = "@eve:localhost"; - - const bobDeviceKeys = { - algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], - device_id: "bvcxz", - keys: { - "ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q", - "curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ", - }, - user_id: "@eve:localhost", - signatures: { - "@eve:localhost": { - "ed25519:bvcxz": - "CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg", - }, - }, - }; - - const bobKeys: Record = {}; - bobKeys[bobDeviceId] = bobDeviceKeys; - aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } }); - - await Promise.all([ - aliTestClient.client.downloadKeys([bobUserId, eveUserId]), - aliTestClient.httpBackend.flush("/keys/query", 1), - ]); - const [bobDevices, eveDevices] = await Promise.all([ - aliTestClient.client.getStoredDevicesForUser(bobUserId), - aliTestClient.client.getStoredDevicesForUser(eveUserId), - ]); - // should get an empty list - expect(bobDevices).toEqual([]); - expect(eveDevices).toEqual([]); - }); - - it("Ali gets keys with an incorrect deviceId", async () => { - const bobDeviceKeys = { - algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], - device_id: "bad_device", - keys: { - "ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0", - "curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc", - }, - user_id: "@bob:localhost", - signatures: { - "@bob:localhost": { - "ed25519:bad_device": - "fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ", - }, - }, - }; - - const bobKeys: Record = {}; - bobKeys[bobDeviceId] = bobDeviceKeys; - aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } }); - - await Promise.all([ - aliTestClient.client.downloadKeys([bobUserId]), - aliTestClient.httpBackend.flush("/keys/query", 1), - ]); - const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId); - // should get an empty list - expect(devices).toEqual([]); - }); - - it("Bob starts his client and uploads device keys and one-time keys", async () => { - await bobTestClient.start(); - const keys = await bobTestClient.awaitOneTimeKeyUpload(); - expect(Object.keys(keys).length).toEqual(5); - expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); - }); - - it("Ali sends a message", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - await firstSync(aliTestClient); - await aliEnablesEncryption(); - await aliSendsFirstMessage(); - }); - - it("Bob receives a message", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - await firstSync(aliTestClient); - await aliEnablesEncryption(); - await aliSendsFirstMessage(); - await bobRecvMessage(); - }); - - it("Bob receives a message with a bogus sender", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - await firstSync(aliTestClient); - await aliEnablesEncryption(); - await aliSendsFirstMessage(); - const message = aliMessages.shift()!; - const syncData = { - next_batch: "x", - rooms: { - join: { - [roomId]: { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: "@bogus:sender", - }), - ], - }, - }, - }, - }, - }; - bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); - - const eventPromise = new Promise((resolve) => { - const onEvent = function (event: MatrixEvent) { - logger.log(bobUserId + " received event", event); - resolve(event); - }; - bobTestClient.client.once(ClientEvent.Event, onEvent); - }); - await bobTestClient.httpBackend.flushAllExpected(); - const preDecryptionEvent = await eventPromise; - expect(preDecryptionEvent.isEncrypted()).toBeTruthy(); - // it may still be being decrypted - const event = await testUtils.awaitDecryption(preDecryptionEvent); - expect(event.getType()).toEqual("m.room.message"); - expect(event.getContent().msgtype).toEqual("m.bad.encrypted"); - }); - - it("Ali blocks Bob's device", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - await firstSync(aliTestClient); - await aliEnablesEncryption(); - await aliDownloadsKeys(); - aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true); - const p1 = sendMessage(aliTestClient.client); - const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) { - // no unblocked devices, so the ciphertext should be empty - expect(sentContent.ciphertext).toEqual({}); - }); - await Promise.all([p1, p2]); - }); - - it("Bob receives two pre-key messages", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - await firstSync(aliTestClient); - await aliEnablesEncryption(); - await aliSendsFirstMessage(); - await bobRecvMessage(); - await aliSendsMessage(); - await bobRecvMessage(); - }); - - it("Bob replies to the message", async () => { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await bobTestClient.start(); - await firstSync(aliTestClient); - await firstSync(bobTestClient); - await aliEnablesEncryption(); - await aliSendsFirstMessage(); - bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {}); - await bobRecvMessage(); - await bobEnablesEncryption(); - const ciphertext = await bobSendsReplyMessage(); - expect(ciphertext.type).toEqual(1); - await aliRecvMessage(); - }); - - it("Ali does a key query when encryption is enabled", async () => { - // enabling encryption in the room should make alice download devices - // for both members. - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); - await aliTestClient.start(); - await firstSync(aliTestClient); - const syncData = { - next_batch: "2", - rooms: { - join: { - [roomId]: { - state: { - events: [ - testUtils.mkEvent({ - type: "m.room.encryption", - skey: "", - content: { - algorithm: "m.olm.v1.curve25519-aes-sha2", - }, - }), - ], - }, - }, - }, - }, - }; - - aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); - await aliTestClient.httpBackend.flush("/sync", 1); - aliTestClient.expectKeyQuery({ - device_keys: { - [bobUserId]: {}, - }, - failures: {}, - }); - await aliTestClient.httpBackend.flushAllExpected(); - }); - - it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => { - // Send a response which causes a key upload - const httpBackend = aliTestClient.httpBackend; - const syncDataEmpty = { - next_batch: "a", - device_one_time_keys_count: { - signed_curve25519: 0, - }, - }; - - // enqueue expectations: - // * Sync with empty one_time_keys => upload keys - - logger.log(aliTestClient + ": starting"); - httpBackend.when("GET", "/versions").respond(200, {}); - httpBackend.when("GET", "/pushrules").respond(200, {}); - httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); - aliTestClient.expectDeviceKeyUpload(); - - // we let the client do a very basic initial sync, which it needs before - // it will upload one-time keys. - httpBackend.when("GET", "/sync").respond(200, syncDataEmpty); - - await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]); - logger.log(aliTestClient + ": started"); - httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => { - expect(content.one_time_keys).toBeTruthy(); - expect(content.one_time_keys).not.toEqual({}); - expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1); - // cancel futher calls by telling the client - // we have more than we need - return { - one_time_key_counts: { - signed_curve25519: 70, - }, - }; - }); - await httpBackend.flushAllExpected(); - }); - - it("Checks for outgoing room key requests for a given event's session", async () => { - const eventA0 = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: { - algorithm: "m.megolm.v1.aes-sha2", - session_id: "sessionid", - sender_key: "senderkey", - }, - }); - const eventA1 = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: { - algorithm: "m.megolm.v1.aes-sha2", - session_id: "sessionid", - sender_key: "senderkey", - }, - }); - const eventB = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: { - algorithm: "m.megolm.v1.aes-sha2", - session_id: "othersessionid", - sender_key: "senderkey", - }, - }); - const nonEncryptedEvent = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: {}, - }); - - aliTestClient.client.crypto?.onSyncCompleted({}); - await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0); - expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull(); - expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull(); - expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull(); - }); -}); diff --git a/spec/integ/devicelist-integ.spec.ts b/spec/integ/devicelist-integ.spec.ts deleted file mode 100644 index ce741d8dc39..00000000000 --- a/spec/integ/devicelist-integ.spec.ts +++ /dev/null @@ -1,406 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 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 { TestClient } from "../TestClient"; -import * as testUtils from "../test-utils/test-utils"; -import { logger } from "../../src/logger"; -import { KnownMembership } from "../../src/@types/membership"; - -const ROOM_ID = "!room:id"; - -/** - * get a /sync response which contains a single e2e room (ROOM_ID), with the - * members given - * - * @returns sync response - */ -function getSyncResponse(roomMembers: string[]) { - const stateEvents = [ - testUtils.mkEvent({ - type: "m.room.encryption", - skey: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }), - ]; - - Array.prototype.push.apply( - stateEvents, - roomMembers.map((m) => - testUtils.mkMembership({ - mship: KnownMembership.Join, - sender: m, - }), - ), - ); - - const syncResponse = { - next_batch: 1, - rooms: { - join: { - [ROOM_ID]: { - state: { - events: stateEvents, - }, - }, - }, - }, - }; - - return syncResponse; -} - -describe("DeviceList management:", function () { - if (!globalThis.Olm) { - logger.warn("not running deviceList tests: Olm not present"); - return; - } - - let aliceTestClient: TestClient; - let sessionStoreBackend: Storage; - - async function createTestClient() { - const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend); - await testClient.client.initLegacyCrypto(); - return testClient; - } - - beforeEach(async function () { - // we create our own sessionStoreBackend so that we can use it for - // another TestClient. - sessionStoreBackend = new testUtils.MockStorageApi(); - - aliceTestClient = await createTestClient(); - }); - - afterEach(function () { - return aliceTestClient.stop(); - }); - - it("Alice shouldn't do a second /query for non-e2e-capable devices", function () { - aliceTestClient.expectKeyQuery({ - device_keys: { "@alice:localhost": {} }, - failures: {}, - }); - return aliceTestClient - .start() - .then(function () { - const syncResponse = getSyncResponse(["@bob:xyz"]); - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); - - return aliceTestClient.flushSync(); - }) - .then(function () { - logger.log("Forcing alice to download our device keys"); - - aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, { - device_keys: { - "@bob:xyz": {}, - }, - }); - - return Promise.all([ - aliceTestClient.client.downloadKeys(["@bob:xyz"]), - aliceTestClient.httpBackend.flush("/keys/query", 1), - ]); - }) - .then(function () { - logger.log("Telling alice to send a megolm message"); - - aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { - event_id: "$event_id", - }); - - return Promise.all([ - aliceTestClient.client.sendTextMessage(ROOM_ID, "test"), - - // the crypto stuff can take a while, so give the requests a whole second. - aliceTestClient.httpBackend.flushAllExpected({ - timeout: 1000, - }), - ]); - }); - }); - - it.skip("We should not get confused by out-of-order device query responses", () => { - // https://github.com/vector-im/element-web/issues/3126 - aliceTestClient.expectKeyQuery({ - device_keys: { "@alice:localhost": {} }, - failures: {}, - }); - return aliceTestClient - .start() - .then(() => { - aliceTestClient.httpBackend - .when("GET", "/sync") - .respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"])); - return aliceTestClient.flushSync(); - }) - .then(() => { - // to make sure the initial device queries are flushed out, we - // attempt to send a message. - - aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, { - device_keys: { - "@bob:xyz": {}, - "@chris:abc": {}, - }, - }); - - aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" }); - - return Promise.all([ - aliceTestClient.client.sendTextMessage(ROOM_ID, "test"), - aliceTestClient.httpBackend - .flush("/keys/query", 1) - .then(() => aliceTestClient.httpBackend.flush("/send/", 1)), - aliceTestClient.client.crypto!.deviceList.saveIfDirty(), - ]); - }) - .then(() => { - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - expect(data!.syncToken).toEqual(1); - }); - - // invalidate bob's and chris's device lists in separate syncs - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { - next_batch: "2", - device_lists: { - changed: ["@bob:xyz"], - }, - }); - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { - next_batch: "3", - device_lists: { - changed: ["@chris:abc"], - }, - }); - // flush both syncs - return aliceTestClient.flushSync().then(() => { - return aliceTestClient.flushSync(); - }); - }) - .then(() => { - // check that we don't yet have a request for chris's devices. - aliceTestClient.httpBackend - .when("POST", "/keys/query", { - device_keys: { - "@chris:abc": {}, - }, - token: "3", - }) - .respond(200, { - device_keys: { "@chris:abc": {} }, - }); - return aliceTestClient.httpBackend.flush("/keys/query", 1); - }) - .then((flushed) => { - expect(flushed).toEqual(0); - return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - }) - .then(() => { - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - if (bobStat != 1 && bobStat != 2) { - throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat); - } - const chrisStat = data!.trackingStatus["@chris:abc"]; - if (chrisStat != 1 && chrisStat != 2) { - throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat); - } - }); - - // now add an expectation for a query for bob's devices, and let - // it complete. - aliceTestClient.httpBackend - .when("POST", "/keys/query", { - device_keys: { - "@bob:xyz": {}, - }, - token: "2", - }) - .respond(200, { - device_keys: { "@bob:xyz": {} }, - }); - return aliceTestClient.httpBackend.flush("/keys/query", 1); - }) - .then((flushed) => { - expect(flushed).toEqual(1); - - // wait for the client to stop processing the response - return aliceTestClient.client.downloadKeys(["@bob:xyz"]); - }) - .then(() => { - return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - }) - .then(() => { - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - expect(bobStat).toEqual(3); - const chrisStat = data!.trackingStatus["@chris:abc"]; - if (chrisStat != 1 && chrisStat != 2) { - throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat); - } - }); - - // now let the query for chris's devices complete. - return aliceTestClient.httpBackend.flush("/keys/query", 1); - }) - .then((flushed) => { - expect(flushed).toEqual(1); - - // wait for the client to stop processing the response - return aliceTestClient.client.downloadKeys(["@chris:abc"]); - }) - .then(() => { - return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - }) - .then(() => { - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - const chrisStat = data!.trackingStatus["@bob:xyz"]; - - expect(bobStat).toEqual(3); - expect(chrisStat).toEqual(3); - expect(data!.syncToken).toEqual(3); - }); - }); - }); - - // https://github.com/vector-im/element-web/issues/4983 - describe("Alice should know she has stale device lists", () => { - beforeEach(async function () { - await aliceTestClient.start(); - - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"])); - await aliceTestClient.flushSync(); - - aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, { - device_keys: { - "@bob:xyz": {}, - }, - }); - await aliceTestClient.httpBackend.flush("/keys/query", 1); - await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - - // Alice should be tracking bob's device list - expect(bobStat).toBeGreaterThan(0); - }); - }); - - it("when Bob leaves", async function () { - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { - next_batch: 2, - device_lists: { - left: ["@bob:xyz"], - }, - rooms: { - join: { - [ROOM_ID]: { - timeline: { - events: [ - testUtils.mkMembership({ - mship: KnownMembership.Leave, - sender: "@bob:xyz", - }), - ], - }, - }, - }, - }, - }); - - await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - - // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual(0); - }); - }); - - it("when Alice leaves", async function () { - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { - next_batch: 2, - device_lists: { - left: ["@bob:xyz"], - }, - rooms: { - leave: { - [ROOM_ID]: { - timeline: { - events: [ - testUtils.mkMembership({ - mship: KnownMembership.Leave, - sender: "@bob:xyz", - }), - ], - }, - }, - }, - }, - }); - - await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - - // @ts-ignore accessing a protected field - aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - - // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual(0); - }); - }); - - it("when Bob leaves whilst Alice is offline", async function () { - aliceTestClient.stop(); - - const anotherTestClient = await createTestClient(); - - try { - await anotherTestClient.start(); - anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([])); - await anotherTestClient.flushSync(); - await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty(); - - // @ts-ignore accessing private property - anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data!.trackingStatus["@bob:xyz"]; - - // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual(0); - }); - } finally { - anotherTestClient.stop(); - } - }); - }); -}); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index e058426cbd7..11603f53432 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -17,7 +17,7 @@ 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"; +import { IStoredClientOpts, MatrixClient } from "../../src/client"; import { MatrixEvent } from "../../src/models/event"; import { Filter, @@ -644,126 +644,6 @@ describe("MatrixClient", function () { }); }); - describe("downloadKeys", function () { - if (!CRYPTO_ENABLED) { - return; - } - - beforeEach(function () { - // running initLegacyCrypto should trigger a key upload - httpBackend.when("POST", "/keys/upload").respond(200, {}); - return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]); - }); - - afterEach(() => { - client.stopClient(); - }); - - it("should do an HTTP request and then store the keys", function () { - const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78"; - // ed25519key = client.getDeviceEd25519Key(); - const borisKeys = { - dev1: { - algorithms: ["1"], - device_id: "dev1", - keys: { "ed25519:dev1": ed25519key }, - signatures: { - boris: { - "ed25519:dev1": - "RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" + - "JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw", - }, - }, - unsigned: { abc: "def" }, - user_id: "boris", - }, - }; - const chazKeys = { - dev2: { - algorithms: ["2"], - device_id: "dev2", - keys: { "ed25519:dev2": ed25519key }, - signatures: { - chaz: { - "ed25519:dev2": - "FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" + - "EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ", - }, - }, - unsigned: { ghi: "def" }, - user_id: "chaz", - }, - }; - - /* - function sign(o) { - var anotherjson = require('another-json'); - var b = JSON.parse(JSON.stringify(o)); - delete(b.signatures); - delete(b.unsigned); - return client.crypto.olmDevice.sign(anotherjson.stringify(b)); - }; - - logger.log("Ed25519: " + ed25519key); - logger.log("boris:", sign(borisKeys.dev1)); - logger.log("chaz:", sign(chazKeys.dev2)); - */ - - httpBackend - .when("POST", "/keys/query") - .check(function (req) { - expect(req.data).toEqual({ - device_keys: { - boris: [], - chaz: [], - }, - }); - }) - .respond(200, { - device_keys: { - boris: borisKeys, - chaz: chazKeys, - }, - }); - - const prom = client.downloadKeys(["boris", "chaz"]).then(function (res) { - assertObjectContains(res.get("boris")!.get("dev1")!, { - verified: 0, // DeviceVerification.UNVERIFIED - keys: { "ed25519:dev1": ed25519key }, - algorithms: ["1"], - unsigned: { abc: "def" }, - }); - - assertObjectContains(res.get("chaz")!.get("dev2")!, { - verified: 0, // DeviceVerification.UNVERIFIED - keys: { "ed25519:dev2": ed25519key }, - algorithms: ["2"], - unsigned: { ghi: "def" }, - }); - }); - - httpBackend.flush(""); - return prom; - }); - }); - - describe("deleteDevice", function () { - const auth = { identifier: 1 }; - it("should pass through an auth dict", function () { - httpBackend - .when("DELETE", "/_matrix/client/v3/devices/my_device") - .check(function (req) { - expect(req.data).toEqual({ auth: auth }); - }) - .respond(200); - - const prom = client.deleteDevice("my_device", auth); - - httpBackend.flush(""); - return prom; - }); - }); - describe("partitionThreadedEvents", function () { let room: Room; beforeEach(() => { @@ -2197,11 +2077,3 @@ const buildEventCreate = () => type: "m.room.create", unsigned: { age: 80126105 }, }); - -function assertObjectContains(obj: Record, expected: any): void { - for (const k in expected) { - if (expected.hasOwnProperty(k)) { - expect(obj[k]).toEqual(expected[k]); - } - } -} diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index e8b9d6e52f2..4d8962f2cf5 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -27,7 +27,6 @@ import { UNSTABLE_MSC2716_MARKER, MatrixClient, ClientEvent, - IndexedDBCryptoStore, ISyncResponse, IRoomEvent, IJoinedRoom, @@ -118,236 +117,6 @@ describe("MatrixClient syncing", () => { await httpBackend!.flushAllExpected(); }); - it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => { - await client!.initLegacyCrypto(); - - const roomId = "!cycles:example.org"; - - // First sync: an invite - const inviteSyncRoomSection = { - invite: { - [roomId]: { - invite_state: { - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Invite, - }, - }, - ], - }, - }, - }, - }; - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: inviteSyncRoomSection, - }); - - // Second sync: a leave (reject of some kind) - httpBackend!.when("POST", "/leave").respond(200, {}); - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: { - leave: { - [roomId]: { - account_data: { events: [] }, - ephemeral: { events: [] }, - state: { - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Leave, - }, - prev_content: { - membership: KnownMembership.Invite, - }, - // XXX: And other fields required on an event - }, - ], - }, - timeline: { - limited: false, - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Leave, - }, - prev_content: { - membership: KnownMembership.Invite, - }, - // XXX: And other fields required on an event - }, - ], - }, - }, - }, - }, - }); - - // Third sync: another invite - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: inviteSyncRoomSection, - }); - - // First fire: an initial invite - let fires = 0; - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - // Room, string, string - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Invite); - expect(oldMembership).toBeFalsy(); - - // Second fire: a leave - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Leave); - expect(oldMembership).toBe(KnownMembership.Invite); - - // Third/final fire: a second invite - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Invite); - expect(oldMembership).toBe(KnownMembership.Leave); - }); - }); - - // For maximum safety, "leave" the room after we register the handler - client!.leave(roomId); - }); - - // noinspection ES6MissingAwait - client!.startClient(); - await httpBackend!.flushAllExpected(); - - expect(fires).toBe(3); - }); - - it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => { - await client!.initLegacyCrypto(); - - const roomId = "!cycles:example.org"; - - // First sync: an knock - const knockSyncRoomSection = { - knock: { - [roomId]: { - knock_state: { - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Knock, - }, - }, - ], - }, - }, - }, - }; - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: knockSyncRoomSection, - }); - - // Second sync: a leave (reject of some kind) - httpBackend!.when("POST", "/leave").respond(200, {}); - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: { - leave: { - [roomId]: { - account_data: { events: [] }, - ephemeral: { events: [] }, - state: { - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Leave, - }, - prev_content: { - membership: KnownMembership.Knock, - }, - // XXX: And other fields required on an event - }, - ], - }, - timeline: { - limited: false, - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Leave, - }, - prev_content: { - membership: KnownMembership.Knock, - }, - // XXX: And other fields required on an event - }, - ], - }, - }, - }, - }, - }); - - // Third sync: another knock - httpBackend!.when("GET", "/sync").respond(200, { - ...syncData, - rooms: knockSyncRoomSection, - }); - - // First fire: an initial knock - let fires = 0; - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - // Room, string, string - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Knock); - expect(oldMembership).toBeFalsy(); - - // Second fire: a leave - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Leave); - expect(oldMembership).toBe(KnownMembership.Knock); - - // Third/final fire: a second knock - client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { - fires++; - expect(room.roomId).toBe(roomId); - expect(membership).toBe(KnownMembership.Knock); - expect(oldMembership).toBe(KnownMembership.Leave); - }); - }); - - // For maximum safety, "leave" the room after we register the handler - client!.leave(roomId); - }); - - // noinspection ES6MissingAwait - client!.startClient(); - await httpBackend!.flushAllExpected(); - - expect(fires).toBe(3); - }); - it("should honour lazyLoadMembers if user is not a guest", () => { httpBackend! .when("GET", "/sync") @@ -2570,61 +2339,6 @@ describe("MatrixClient syncing (IndexedDB version)", () => { presence: {}, }; - it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => { - const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, { - cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"), - }); - const idbHttpBackend = idbTestClient.httpBackend; - const idbClient = idbTestClient.client; - idbHttpBackend.when("GET", "/versions").respond(200, {}); - idbHttpBackend.when("GET", "/pushrules/").respond(200, {}); - idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); - - await idbClient.initLegacyCrypto(); - - const roomId = "!invite:example.org"; - - // First sync: an invite - const inviteSyncRoomSection = { - invite: { - [roomId]: { - invite_state: { - events: [ - { - type: "m.room.member", - state_key: selfUserId, - content: { - membership: KnownMembership.Invite, - }, - }, - ], - }, - }, - }, - }; - idbHttpBackend.when("GET", "/sync").respond(200, { - ...syncData, - rooms: inviteSyncRoomSection, - }); - - // First fire: an initial invite - let fires = 0; - idbClient.once(ClientEvent.Room, (room) => { - fires++; - expect(room.roomId).toBe(roomId); - }); - - // noinspection ES6MissingAwait - idbClient.startClient(); - await idbHttpBackend.flushAllExpected(); - - expect(fires).toBe(1); - - idbHttpBackend.verifyNoOutstandingExpectation(); - idbClient.stopClient(); - idbHttpBackend.stop(); - }); - it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => { const roomId = "!room:example.org"; diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 643f4f7e1ba..1865e17e344 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -117,18 +117,13 @@ describe("SlidingSyncSdk", () => { }; // assign client/httpBackend globals - const setupClient = async (testOpts?: Partial) => { + const setupClient = async (testOpts?: Partial) => { testOpts = testOpts || {}; const syncOpts: SyncApiOptions = {}; const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); httpBackend = testClient.httpBackend; client = testClient.client; mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0)); - if (testOpts.withCrypto) { - httpBackend!.when("GET", "/room_keys/version").respond(404, {}); - await client!.initLegacyCrypto(); - syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto; - } httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {}); sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts); }; @@ -627,61 +622,6 @@ describe("SlidingSyncSdk", () => { }); }); - describe("ExtensionE2EE", () => { - let ext: Extension; - - beforeAll(async () => { - await setupClient({ - withCrypto: true, - }); - const hasSynced = sdk!.sync(); - await httpBackend!.flushAllExpected(); - await hasSynced; - ext = findExtension("e2ee"); - }); - - afterAll(async () => { - // needed else we do some async operations in the background which can cause Jest to whine: - // "Cannot log after tests are done. Did you forget to wait for something async in your test?" - // Attempted to log "Saving device tracking data null"." - client!.crypto!.stop(); - }); - - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ - enabled: true, - }); - expect(ext.onRequest(false)).toEqual(undefined); - }); - - it("can update device lists", () => { - client!.crypto!.processDeviceLists = jest.fn(); - ext.onResponse({ - device_lists: { - changed: ["@alice:localhost"], - left: ["@bob:localhost"], - }, - }); - expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({ - changed: ["@alice:localhost"], - left: ["@bob:localhost"], - }); - }); - - it("can update OTK counts and unused fallback keys", () => { - client!.crypto!.processKeyCounts = jest.fn(); - ext.onResponse({ - device_one_time_keys_count: { - signed_curve25519: 42, - }, - device_unused_fallback_key_types: ["signed_curve25519"], - }); - expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [ - "signed_curve25519", - ]); - }); - }); - describe("ExtensionAccountData", () => { let ext: Extension; diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index d0c9abb2a5d..9cf9f782554 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -560,9 +560,6 @@ export const CRYPTO_BACKENDS: Record = {}; export type InitCrypto = (_: MatrixClient) => Promise; CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto(); -if (globalThis.Olm) { - CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto(); -} export const emitPromise = (e: EventEmitter, k: string): Promise => new Promise((r) => e.once(k, r)); diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts deleted file mode 100644 index 419bb530a66..00000000000 --- a/spec/unit/crypto.spec.ts +++ /dev/null @@ -1,1467 +0,0 @@ -import "../olm-loader"; -// eslint-disable-next-line no-restricted-imports -import { EventEmitter } from "events"; - -import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { IClaimOTKsResult, MatrixClient } from "../../src/client"; -import { Crypto } from "../../src/crypto"; -import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; -import { MockStorageApi } from "../MockStorageApi"; -import { TestClient } from "../TestClient"; -import { MatrixEvent } from "../../src/models/event"; -import { Room } from "../../src/models/room"; -import * as olmlib from "../../src/crypto/olmlib"; -import { sleep } from "../../src/utils"; -import { CRYPTO_ENABLED } from "../../src/client"; -import { DeviceInfo } from "../../src/crypto/deviceinfo"; -import { logger } from "../../src/logger"; -import { DeviceVerification, MemoryStore } from "../../src"; -import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager"; -import { RoomMember } from "../../src/models/room-member"; -import { IStore } from "../../src/store"; -import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; -import { EventShieldColour, EventShieldReason } from "../../src/crypto-api"; -import { UserTrustLevel } from "../../src/crypto/CrossSigning"; -import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; -import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend"; -import * as testData from "../test-utils/test-data"; -import { KnownMembership } from "../../src/@types/membership"; -import type { DeviceInfoMap } from "../../src/crypto/DeviceList"; - -const Olm = globalThis.Olm; - -function awaitEvent(emitter: EventEmitter, event: string): Promise { - return new Promise((resolve) => { - emitter.once(event, (result) => { - resolve(result); - }); - }); -} - -async function keyshareEventForEvent(client: MatrixClient, event: MatrixEvent, index?: number): Promise { - const roomId = event.getRoomId()!; - const eventContent = event.getWireContent(); - const key = await client.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - eventContent.sender_key, - eventContent.session_id, - index, - ); - const ksEvent = new MatrixEvent({ - type: "m.forwarded_room_key", - sender: client.getUserId()!, - content: { - "algorithm": olmlib.MEGOLM_ALGORITHM, - "room_id": roomId, - "sender_key": eventContent.sender_key, - "sender_claimed_ed25519_key": key?.sender_claimed_ed25519_key, - "session_id": eventContent.session_id, - "session_key": key?.key, - "chain_index": key?.chain_index, - "forwarding_curve25519_key_chain": key?.forwarding_curve25519_key_chain, - "org.matrix.msc3061.shared_history": true, - }, - }); - // make onRoomKeyEvent think this was an encrypted event - // @ts-ignore private property - ksEvent.senderCurve25519Key = "akey"; - ksEvent.getWireType = () => "m.room.encrypted"; - ksEvent.getWireContent = () => { - return { - algorithm: "m.olm.v1.curve25519-aes-sha2", - }; - }; - return ksEvent; -} - -function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent { - const roomId = event.getRoomId(); - const eventContent = event.getWireContent(); - const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id); - const ksEvent = new MatrixEvent({ - type: "m.room_key", - sender: client.getUserId()!, - content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: roomId, - session_id: eventContent.session_id, - session_key: key.key, - }, - }); - // make onRoomKeyEvent think this was an encrypted event - // @ts-ignore private property - ksEvent.senderCurve25519Key = event.getSenderKey(); - ksEvent.getWireType = () => "m.room.encrypted"; - ksEvent.getWireContent = () => { - return { - algorithm: "m.olm.v1.curve25519-aes-sha2", - }; - }; - return ksEvent; -} - -describe("Crypto", function () { - if (!CRYPTO_ENABLED) { - return; - } - - beforeAll(function () { - return Olm.init(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it("Crypto exposes the correct olm library version", function () { - expect(Crypto.getOlmVersion()[0]).toEqual(3); - }); - - it("getVersion() should return the current version of the olm library", async () => { - const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initLegacyCrypto(); - - const olmVersionTuple = Crypto.getOlmVersion(); - expect(client.getCrypto()?.getVersion()).toBe( - `Olm ${olmVersionTuple[0]}.${olmVersionTuple[1]}.${olmVersionTuple[2]}`, - ); - }); - - describe("encrypted events", function () { - it("provides encryption information for events from unverified senders", async function () { - const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initLegacyCrypto(); - - // unencrypted event - const event = { - getId: () => "$event_id", - getSender: () => "@bob:example.com", - getSenderKey: () => null, - getWireContent: () => { - return {}; - }, - } as unknown as MatrixEvent; - - let encryptionInfo = client.getEventEncryptionInfo(event); - expect(encryptionInfo.encrypted).toBeFalsy(); - - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toBe(null); - - // unknown sender (e.g. deleted device), forwarded megolm key (untrusted) - event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI"; - event.getWireContent = () => { - return { algorithm: olmlib.MEGOLM_ALGORITHM }; - }; - event.getForwardingCurve25519KeyChain = () => ["not empty"]; - event.isKeySourceUntrusted = () => true; - event.getClaimedEd25519Key = () => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - - encryptionInfo = client.getEventEncryptionInfo(event); - expect(encryptionInfo.encrypted).toBeTruthy(); - expect(encryptionInfo.authenticated).toBeFalsy(); - expect(encryptionInfo.sender).toBeFalsy(); - - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.GREY, - shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, - }); - - // known sender, megolm key from backup - event.getForwardingCurve25519KeyChain = () => []; - event.isKeySourceUntrusted = () => true; - const device = new DeviceInfo("FLIBBLE"); - device.keys["curve25519:FLIBBLE"] = "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI"; - device.keys["ed25519:FLIBBLE"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - client.crypto!.deviceList.getDeviceByIdentityKey = () => device; - - encryptionInfo = client.getEventEncryptionInfo(event); - expect(encryptionInfo.encrypted).toBeTruthy(); - expect(encryptionInfo.authenticated).toBeFalsy(); - expect(encryptionInfo.sender).toBeTruthy(); - expect(encryptionInfo.mismatchedSender).toBeFalsy(); - - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.GREY, - shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, - }); - - // known sender, trusted megolm key, but bad ed25519key - event.isKeySourceUntrusted = () => false; - device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; - - encryptionInfo = client.getEventEncryptionInfo(event); - expect(encryptionInfo.encrypted).toBeTruthy(); - expect(encryptionInfo.authenticated).toBeTruthy(); - expect(encryptionInfo.sender).toBeTruthy(); - expect(encryptionInfo.mismatchedSender).toBeTruthy(); - - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.RED, - shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY, - }); - - client.stopClient(); - }); - - describe("provides encryption information for events from verified senders", function () { - const testDeviceId = testData.BOB_TEST_DEVICE_ID; - const testDevice = testData.BOB_SIGNED_TEST_DEVICE_DATA; - - let client: MatrixClient; - beforeEach(async () => { - client = new TestClient("@alice:example.com", "deviceid").client; - await client.initLegacyCrypto(); - - // mock out the verification check - client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false); - }); - - afterEach(() => { - client.stopClient(); - }); - - async function buildEncryptedEvent( - decryptionResult: Partial = {}, - ): Promise { - const mockCryptoBackend = { - decryptEvent: async (event: MatrixEvent): Promise => { - return { - claimedEd25519Key: testDevice.keys["ed25519:" + testDeviceId], - clearEvent: { - room_id: "!room_id", - type: "m.room.message", - content: { body: "test" }, - }, - forwardingCurve25519KeyChain: [], - senderCurve25519Key: testDevice.keys["curve25519:" + testDeviceId], - ...decryptionResult, - }; - }, - } as unknown as CryptoBackend; - - const event = new MatrixEvent({ - event_id: "$event_id", - sender: testData.BOB_TEST_USER_ID, - type: "m.room.encrypted", - content: { algorithm: "m.megolm.v1.aes-sha2" }, - }); - await event.attemptDecryption(mockCryptoBackend); - return event; - } - - it("unknown device", async () => { - const event = await buildEncryptedEvent(); - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.GREY, - shieldReason: EventShieldReason.UNKNOWN_DEVICE, - }); - }); - - it("known but unsigned device", async () => { - client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, { - [testDeviceId]: { - keys: testDevice.keys, - algorithms: testDevice.algorithms, - verified: DeviceVerification.Unverified, - known: true, - }, - }); - - const event = await buildEncryptedEvent(); - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.RED, - shieldReason: EventShieldReason.UNSIGNED_DEVICE, - }); - }); - - describe("known and verified device", () => { - beforeEach(() => { - client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, { - [testDeviceId]: { - keys: testDevice.keys, - algorithms: testDevice.algorithms, - verified: DeviceVerification.Verified, - known: true, - }, - }); - }); - - it("regular key", async () => { - const event = await buildEncryptedEvent(); - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.NONE, - shieldReason: null, - }); - }); - - it("unauthenticated key", async () => { - const event = await buildEncryptedEvent({ untrusted: true }); - expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({ - shieldColour: EventShieldColour.GREY, - shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, - }); - }); - }); - }); - - it("doesn't throw an error when attempting to decrypt a redacted event", async () => { - const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initLegacyCrypto(); - - const event = new MatrixEvent({ - content: {}, - event_id: "$event_id", - room_id: "!room_id", - sender: "@bob:example.com", - type: "m.room.encrypted", - unsigned: { - redacted_because: { - content: {}, - event_id: "$redaction_event_id", - redacts: "$event_id", - room_id: "!room_id", - origin_server_ts: 1234567890, - sender: "@bob:example.com", - type: "m.room.redaction", - unsigned: {}, - }, - }, - }); - await event.attemptDecryption(client.crypto!); - expect(event.isDecryptionFailure()).toBeFalsy(); - // since the redaction event isn't encrypted, the redacted_because - // should be the same as in the original event - expect(event.getRedactionEvent()).toEqual(event.getUnsigned().redacted_because); - - client.stopClient(); - }); - }); - - describe("Session management", function () { - const otkResponse: IClaimOTKsResult = { - failures: {}, - one_time_keys: { - "@alice:home.server": { - aliceDevice: { - "signed_curve25519:FLIBBLE": { - key: "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI", - signatures: { - "@alice:home.server": { - "ed25519:aliceDevice": "totally a valid signature", - }, - }, - }, - }, - }, - }, - }; - - let crypto: Crypto; - let mockBaseApis: MatrixClient; - - let fakeEmitter: EventEmitter; - - beforeEach(async function () { - const mockStorage = new MockStorageApi() as unknown as Storage; - const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; - const cryptoStore = new MemoryCryptoStore(); - - cryptoStore.storeEndToEndDeviceData( - { - devices: { - "@bob:home.server": { - BOBDEVICE: { - algorithms: [], - verified: 1, - known: false, - keys: { - "curve25519:BOBDEVICE": "this is a key", - }, - }, - }, - }, - trackingStatus: {}, - }, - {}, - ); - - mockBaseApis = { - sendToDevice: jest.fn(), - getKeyBackupVersion: jest.fn(), - isGuest: jest.fn(), - emit: jest.fn(), - } as unknown as MatrixClient; - - fakeEmitter = new EventEmitter(); - - crypto = new Crypto(mockBaseApis, "@alice:home.server", "FLIBBLE", clientStore, cryptoStore, []); - crypto.registerEventHandlers(fakeEmitter as any); - await crypto.init(); - }); - - afterEach(async function () { - await crypto.stop(); - }); - - it("restarts wedged Olm sessions", async function () { - const prom = new Promise((resolve) => { - mockBaseApis.claimOneTimeKeys = function () { - resolve(); - return Promise.resolve(otkResponse); - }; - }); - - fakeEmitter.emit("toDeviceEvent", { - getId: jest.fn().mockReturnValue("$wedged"), - getType: jest.fn().mockReturnValue("m.room.message"), - getContent: jest.fn().mockReturnValue({ - msgtype: "m.bad.encrypted", - }), - getWireContent: jest.fn().mockReturnValue({ - algorithm: "m.olm.v1.curve25519-aes-sha2", - sender_key: "this is a key", - }), - getSender: jest.fn().mockReturnValue("@bob:home.server"), - }); - - await prom; - }); - }); - - describe("Key requests", function () { - let aliceClient: MatrixClient; - let secondAliceClient: MatrixClient; - let bobClient: MatrixClient; - let claraClient: MatrixClient; - - beforeEach(async function () { - aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client; - bobClient = new TestClient("@bob:example.com", "bobdevice").client; - claraClient = new TestClient("@clara:example.com", "claradevice").client; - await aliceClient.initLegacyCrypto(); - await secondAliceClient.initLegacyCrypto(); - await bobClient.initLegacyCrypto(); - await claraClient.initLegacyCrypto(); - }); - - afterEach(async function () { - aliceClient.stopClient(); - secondAliceClient.stopClient(); - bobClient.stopClient(); - claraClient.stopClient(); - }); - - it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - // Make Bob invited by Alice so Bob will accept Alice's forwarded keys - bobRoom.currentState.setStateEvents([ - new MatrixEvent({ - type: "m.room.member", - sender: "@alice:example.com", - room_id: roomId, - content: { membership: KnownMembership.Invite }, - state_key: "@bob:example.com", - }), - ]); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); - }), - ); - - const device = new DeviceInfo(aliceClient.deviceId!); - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const decryptEventsPromise = Promise.all( - events.map((ev) => { - return awaitEvent(ev, "Event.decrypted"); - }), - ); - - // keyshare the session key starting at the second message, so - // the first message can't be decrypted yet, but the second one - // can - let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1); - bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - await bobDecryptor.onRoomKeyEvent(ksEvent); - await decryptEventsPromise; - expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); - expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - - const cryptoStore = bobClient.crypto!.cryptoStore; - const eventContent = events[0].getWireContent(); - const senderKey = eventContent.sender_key; - const sessionId = eventContent.session_id; - const roomKeyRequestBody = { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: roomId, - sender_key: senderKey, - session_id: sessionId, - }; - // the room key request should still be there, since we haven't - // decrypted everything - expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined(); - - // keyshare the session key starting at the first message, so - // that it can now be decrypted - const decryptEventPromise = awaitEvent(events[0], "Event.decrypted"); - ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - await bobDecryptor.onRoomKeyEvent(ksEvent); - await decryptEventPromise; - expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); - expect(events[0].isKeySourceUntrusted()).toBeTruthy(); - await sleep(1); - // the room key request should still be there, since we've - // decrypted everything with an untrusted key - expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined(); - - // Now share a trusted room key event so Bob will re-decrypt the messages. - // Bob will backfill trust when they receive a trusted session with a higher - // index that connects to an untrusted session with a lower index. - const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]); - const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted"); - await bobDecryptor.onRoomKeyEvent(roomKeyEvent); - await trustedDecryptEventPromise; - expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); - expect(events[0].isKeySourceUntrusted()).toBeFalsy(); - await sleep(1); - // now the room key request should be gone, since there's - // no better key to wait for - expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy(); - }); - - it("should error if a forwarded room key lacks a content.sender_key", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - const event = new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }); - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private property - event.clearEvent = undefined; - // @ts-ignore private property - event.senderCurve25519Key = null; - // @ts-ignore private property - event.claimedEd25519Key = null; - try { - await bobClient.crypto!.decryptEvent(event); - } catch { - // we expect this to fail because we don't have the - // decryption keys yet - } - - const device = new DeviceInfo(aliceClient.deviceId!); - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const ksEvent = await keyshareEventForEvent(aliceClient, event, 1); - ksEvent.getContent().sender_key = undefined; // test - bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn(); - await bobDecryptor.onRoomKeyEvent(ksEvent); - expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); - }); - - it("creates a new keyshare request if we request a keyshare", async function () { - // make sure that cancelAndResend... creates a new keyshare request - // if there wasn't an already-existing one - const event = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - session_id: "sessionid", - sender_key: "senderkey", - }, - }); - await aliceClient.cancelAndResendEventRoomKeyRequest(event); - const cryptoStore = aliceClient.crypto!.cryptoStore; - const roomKeyRequestBody = { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: "!someroom", - session_id: "sessionid", - sender_key: "senderkey", - }; - expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined(); - }); - - it("uses a new txnid for re-requesting keys", async function () { - jest.useFakeTimers(); - - const event = new MatrixEvent({ - sender: "@bob:example.com", - room_id: "!someroom", - content: { - algorithm: olmlib.MEGOLM_ALGORITHM, - session_id: "sessionid", - sender_key: "senderkey", - }, - }); - // replace Alice's sendToDevice function with a mock - const aliceSendToDevice = jest.fn().mockResolvedValue(undefined); - aliceClient.sendToDevice = aliceSendToDevice; - aliceClient.startClient(); - - // make a room key request, and record the transaction ID for the - // sendToDevice call - await aliceClient.cancelAndResendEventRoomKeyRequest(event); - // key requests get queued until the sync has finished, but we don't - // let the client set up enough for that to happen, so gut-wrench a bit - // to force it to send now. - // @ts-ignore - aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests(); - jest.runAllTimers(); - await Promise.resolve(); - expect(aliceSendToDevice).toHaveBeenCalledTimes(1); - const txnId = aliceSendToDevice.mock.calls[0][2]; - - // give the room key request manager time to update the state - // of the request - await Promise.resolve(); - - // cancel and resend the room key request - await aliceClient.cancelAndResendEventRoomKeyRequest(event); - jest.runAllTimers(); - await Promise.resolve(); - // cancelAndResend will call sendToDevice twice: - // the first call to sendToDevice will be the cancellation - // the second call to sendToDevice will be the key request - expect(aliceSendToDevice).toHaveBeenCalledTimes(3); - expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId); - }); - - it("should accept forwarded keys it requested from one of its own user's other devices", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, secondAliceClient, "@alice:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - secondAliceClient.store.storeRoom(bobRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await secondAliceClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - await expect(secondAliceClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); - }), - ); - - const device = new DeviceInfo(aliceClient.deviceId!); - device.verified = DeviceInfo.DeviceVerification.VERIFIED; - secondAliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - secondAliceClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - - const cryptoStore = secondAliceClient.crypto!.cryptoStore; - const eventContent = events[0].getWireContent(); - const senderKey = eventContent.sender_key; - const sessionId = eventContent.session_id; - const roomKeyRequestBody = { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: roomId, - sender_key: senderKey, - session_id: sessionId, - }; - const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody); - expect(outgoingReq).toBeDefined(); - await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, { - state: RoomKeyRequestState.Sent, - }); - - const bobDecryptor = secondAliceClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const decryptEventsPromise = Promise.all( - events.map((ev) => { - return awaitEvent(ev, "Event.decrypted"); - }), - ); - const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await secondAliceClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - events[0].getWireContent().sender_key, - events[0].getWireContent().session_id, - ); - expect(key).not.toBeNull(); - await decryptEventsPromise; - expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); - expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - }); - - it("should accept forwarded keys from the user who invited it to the room", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {}); - // Make Bob invited by Clara - bobRoom.currentState.setStateEvents([ - new MatrixEvent({ - type: "m.room.member", - sender: "@clara:example.com", - room_id: roomId, - content: { membership: KnownMembership.Invite }, - state_key: "@bob:example.com", - }), - ]); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - claraClient.store.storeRoom(claraRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - await claraClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); - }), - ); - - const device = new DeviceInfo(claraClient.deviceId!); - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com"; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const decryptEventsPromise = Promise.all( - events.map((ev) => { - return awaitEvent(ev, "Event.decrypted"); - }), - ); - const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = claraClient.getUserId()!; - ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!); - await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - events[0].getWireContent().sender_key, - events[0].getWireContent().session_id, - ); - expect(key).not.toBeNull(); - await decryptEventsPromise; - expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); - expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - }); - - it("should not accept requested forwarded keys from other users", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); - }), - ); - - const cryptoStore = bobClient.crypto!.cryptoStore; - const eventContent = events[0].getWireContent(); - const senderKey = eventContent.sender_key; - const sessionId = eventContent.session_id; - const roomKeyRequestBody = { - algorithm: olmlib.MEGOLM_ALGORITHM, - room_id: roomId, - sender_key: senderKey, - session_id: sessionId, - }; - const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody); - expect(outgoingReq).toBeDefined(); - await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, { - state: RoomKeyRequestState.Sent, - }); - - const device = new DeviceInfo(aliceClient.deviceId!); - device.verified = DeviceInfo.DeviceVerification.VERIFIED; - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = aliceClient.getUserId()!; - ksEvent.sender = new RoomMember(roomId, aliceClient.getUserId()!); - await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - events[0].getWireContent().sender_key, - events[0].getWireContent().session_id, - ); - expect(key).toBeNull(); - }); - - it("should not accept unexpected forwarded keys for a room it's in", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - claraClient.store.storeRoom(claraRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - await claraClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); - }), - ); - - const device = new DeviceInfo(claraClient.deviceId!); - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = claraClient.getUserId()!; - ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!); - await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - events[0].getWireContent().sender_key, - events[0].getWireContent().session_id, - ); - expect(key).toBeNull(); - }); - - it("should park forwarded keys for a room it's not in", async function () { - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - const events = [ - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$1", - content: { - msgtype: "m.text", - body: "1", - }, - }), - new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$2", - content: { - msgtype: "m.text", - body: "2", - }, - }), - ]; - await Promise.all( - events.map(async (event) => { - // alice encrypts each event, and then bob tries to decrypt - // them without any keys, so that they'll be in pending - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - // remove keys from the event - // @ts-ignore private properties - event.clearEvent = undefined; - // @ts-ignore private properties - event.senderCurve25519Key = null; - // @ts-ignore private properties - event.claimedEd25519Key = null; - }), - ); - - const device = new DeviceInfo(aliceClient.deviceId!); - bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - - const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM); - - const content = events[0].getWireContent(); - - const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - await bobDecryptor.onRoomKeyEvent(ksEvent); - const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - content.sender_key, - content.session_id, - ); - expect(bobKey).toBeNull(); - - const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey( - roomId, - content.sender_key, - content.session_id, - ); - const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId); - expect(parked).toEqual([ - { - senderId: aliceClient.getUserId(), - senderKey: content.sender_key, - sessionId: content.session_id, - sessionKey: aliceKey!.key, - keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key }, - forwardingCurve25519KeyChain: ["akey"], - }, - ]); - }); - }); - - describe("Secret storage", function () { - it("creates secret storage even if there is no keyInfo", async function () { - jest.spyOn(logger, "debug").mockImplementation(() => {}); - jest.setTimeout(10000); - const client = new TestClient("@a:example.com", "dev").client; - await client.initLegacyCrypto(); - client.crypto!.isCrossSigningReady = async () => false; - client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); - client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null); - client.crypto!.baseApis.uploadKeySignatures = jest.fn(); - client.crypto!.baseApis.http.authedRequest = jest.fn(); - const createSecretStorageKey = async () => { - return { - keyInfo: undefined, // Returning undefined here used to cause a crash - privateKey: Uint8Array.of(32, 33), - }; - }; - await client.crypto!.bootstrapSecretStorage({ - createSecretStorageKey, - }); - client.stopClient(); - }); - }); - - describe("encryptAndSendToDevices", () => { - let client: TestClient; - let ensureOlmSessionsForDevices: jest.SpiedFunction; - let encryptMessageForDevice: jest.SpiedFunction; - const payload = { hello: "world" }; - let encryptedPayload: object; - - beforeEach(async () => { - ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices"); - ensureOlmSessionsForDevices.mockResolvedValue(new Map()); - encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); - encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => { - result.plaintext = { type: 0, body: JSON.stringify(payload) }; - }); - - client = new TestClient("@alice:example.org", "aliceweb"); - - // running initLegacyCrypto should trigger a key upload - client.httpBackend.when("POST", "/keys/upload").respond(200, {}); - await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]); - - encryptedPayload = { - algorithm: "m.olm.v1.curve25519-aes-sha2", - sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, - ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } }, - }; - }); - - afterEach(async () => { - ensureOlmSessionsForDevices.mockRestore(); - encryptMessageForDevice.mockRestore(); - await client.stop(); - }); - - it("encrypts and sends to devices", async () => { - client.httpBackend - .when("PUT", "/sendToDevice/m.room.encrypted") - .check((request) => { - const data = request.data; - delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; - delete data.messages["@bob:example.org"]["bobmobile"]["org.matrix.msgid"]; - delete data.messages["@carol:example.org"]["caroldesktop"]["org.matrix.msgid"]; - expect(data).toStrictEqual({ - messages: { - "@bob:example.org": { - bobweb: encryptedPayload, - bobmobile: encryptedPayload, - }, - "@carol:example.org": { - caroldesktop: encryptedPayload, - }, - }, - }); - }) - .respond(200, {}); - - await Promise.all([ - client.client.encryptAndSendToDevices( - [ - { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }, - { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobmobile") }, - { userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") }, - ], - payload, - ), - client.httpBackend.flushAllExpected(), - ]); - }); - - it("sends nothing to devices that couldn't be encrypted to", async () => { - encryptMessageForDevice.mockImplementation(async (...[result, , , , userId, device, payload]) => { - // Refuse to encrypt to Carol's desktop device - if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; - result.plaintext = { type: 0, body: JSON.stringify(payload) }; - }); - - client.httpBackend - .when("PUT", "/sendToDevice/m.room.encrypted") - .check((req) => { - const data = req.data; - delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; - // Carol is nowhere to be seen - expect(data).toStrictEqual({ - messages: { "@bob:example.org": { bobweb: encryptedPayload } }, - }); - }) - .respond(200, {}); - - await Promise.all([ - client.client.encryptAndSendToDevices( - [ - { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }, - { userId: "@carol:example.org", deviceInfo: new DeviceInfo("caroldesktop") }, - ], - payload, - ), - client.httpBackend.flushAllExpected(), - ]); - }); - - it("no-ops if no devices can be encrypted to", async () => { - // Refuse to encrypt to anybody - encryptMessageForDevice.mockResolvedValue(undefined); - - // Get the room keys version request out of the way - client.httpBackend.when("GET", "/room_keys/version").respond(404, {}); - await client.httpBackend.flush("/room_keys/version", 1); - - await client.client.encryptAndSendToDevices( - [{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobweb") }], - payload, - ); - client.httpBackend.verifyNoOutstandingRequests(); - }); - }); - - describe("encryptToDeviceMessages", () => { - let client: TestClient; - let ensureOlmSessionsForDevices: jest.SpiedFunction; - let encryptMessageForDevice: jest.SpiedFunction; - const payload = { hello: "world" }; - let encryptedPayload: object; - let crypto: Crypto; - - beforeEach(async () => { - ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices"); - ensureOlmSessionsForDevices.mockResolvedValue(new Map()); - encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); - encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => { - result.plaintext = { type: 0, body: JSON.stringify(payload) }; - }); - - client = new TestClient("@alice:example.org", "aliceweb"); - - // running initLegacyCrypto should trigger a key upload - client.httpBackend.when("POST", "/keys/upload").respond(200, {}); - await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]); - - encryptedPayload = { - algorithm: "m.olm.v1.curve25519-aes-sha2", - sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, - ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } }, - }; - - crypto = client.client.getCrypto() as Crypto; - }); - - afterEach(async () => { - ensureOlmSessionsForDevices.mockRestore(); - encryptMessageForDevice.mockRestore(); - await client.stop(); - }); - - it("returns encrypted batch where devices known", async () => { - const deviceInfoMap: DeviceInfoMap = new Map([ - [ - "@bob:example.org", - new Map([ - ["bobweb", new DeviceInfo("bobweb")], - ["bobmobile", new DeviceInfo("bobmobile")], - ]), - ], - ["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])], - ]); - jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap); - // const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false); - - const batch = await client.client.getCrypto()?.encryptToDeviceMessages( - "m.test.type", - [ - { userId: "@bob:example.org", deviceId: "bobweb" }, - { userId: "@bob:example.org", deviceId: "bobmobile" }, - { userId: "@carol:example.org", deviceId: "caroldesktop" }, - { userId: "@carol:example.org", deviceId: "carolmobile" }, // not known - ], - payload, - ); - expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith( - ["@bob:example.org", "@carol:example.org"], - false, - ); - expect(encryptMessageForDevice).toHaveBeenCalledTimes(3); - const expectedPayload = expect.objectContaining({ - ...encryptedPayload, - "org.matrix.msgid": expect.any(String), - "sender_key": expect.any(String), - }); - expect(batch?.eventType).toEqual("m.room.encrypted"); - expect(batch?.batch.length).toEqual(3); - expect(batch).toEqual({ - eventType: "m.room.encrypted", - batch: expect.arrayContaining([ - { - userId: "@bob:example.org", - deviceId: "bobweb", - payload: expectedPayload, - }, - { - userId: "@bob:example.org", - deviceId: "bobmobile", - payload: expectedPayload, - }, - { - userId: "@carol:example.org", - deviceId: "caroldesktop", - payload: expectedPayload, - }, - ]), - }); - }); - - it("returns empty batch if no devices known", async () => { - jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map()); - const batch = await crypto.encryptToDeviceMessages( - "m.test.type", - [ - { deviceId: "AAA", userId: "@user1:domain" }, - { deviceId: "BBB", userId: "@user1:domain" }, - { deviceId: "CCC", userId: "@user2:domain" }, - ], - payload, - ); - expect(batch?.eventType).toEqual("m.room.encrypted"); - expect(batch?.batch).toEqual([]); - }); - }); - - describe("checkSecretStoragePrivateKey", () => { - let client: TestClient; - - beforeEach(async () => { - client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initLegacyCrypto(); - }); - - afterEach(async () => { - await client.stop(); - }); - - it("should free PkDecryption", () => { - const free = jest.fn(); - jest.spyOn(Olm, "PkDecryption").mockImplementation( - () => - ({ - init_with_private_key: jest.fn(), - free, - }) as unknown as PkDecryption, - ); - client.client.checkSecretStoragePrivateKey(new Uint8Array(), ""); - expect(free).toHaveBeenCalled(); - }); - }); - - describe("checkCrossSigningPrivateKey", () => { - let client: TestClient; - - beforeEach(async () => { - client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initLegacyCrypto(); - }); - - afterEach(async () => { - await client.stop(); - }); - - it("should free PkSigning", () => { - const free = jest.fn(); - jest.spyOn(Olm, "PkSigning").mockImplementation( - () => - ({ - init_with_seed: jest.fn(), - free, - }) as unknown as PkSigning, - ); - client.client.checkCrossSigningPrivateKey(new Uint8Array(), ""); - expect(free).toHaveBeenCalled(); - }); - }); - - describe("start", () => { - let client: TestClient; - - beforeEach(async () => { - client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initLegacyCrypto(); - }); - - afterEach(async function () { - await client!.stop(); - }); - - // start() is a no-op nowadays, so there's not much to test here. - it("should complete successfully", async () => { - await client!.client.crypto!.start(); - }); - }); - - describe("setRoomEncryption", () => { - let mockClient: MatrixClient; - let mockRoomList: RoomList; - let clientStore: IStore; - let crypto: Crypto; - - beforeEach(async function () { - mockClient = {} as MatrixClient; - const mockStorage = new MockStorageApi() as unknown as Storage; - clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; - const cryptoStore = new MemoryCryptoStore(); - - mockRoomList = { - getRoomEncryption: jest.fn().mockReturnValue(null), - setRoomEncryption: jest.fn().mockResolvedValue(undefined), - } as unknown as RoomList; - - crypto = new Crypto(mockClient, "@alice:home.server", "FLIBBLE", clientStore, cryptoStore, []); - // @ts-ignore we are injecting a mock into a private property - crypto.roomList = mockRoomList; - }); - - it("should set the algorithm if called for a known room", async () => { - const room = new Room("!room:id", mockClient, "@my.user:id"); - await clientStore.storeRoom(room); - await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); - expect(mockRoomList!.setRoomEncryption).toHaveBeenCalledTimes(1); - expect(jest.mocked(mockRoomList!.setRoomEncryption).mock.calls[0][0]).toEqual("!room:id"); - }); - - it("should raise if called for an unknown room", async () => { - await expect(async () => { - await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); - }).rejects.toThrow(/unknown room/); - expect(mockRoomList!.setRoomEncryption).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 20d72702110..9fd840e938d 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked, MockedObject } from "jest-mock"; +import { MockedObject } from "jest-mock"; import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList"; import "../../../olm-loader"; @@ -26,17 +26,13 @@ import { OlmDevice } from "../../../../src/crypto/OlmDevice"; import { Crypto, IncomingRoomKeyRequest } from "../../../../src/crypto"; import { logger } from "../../../../src/logger"; import { MatrixEvent } from "../../../../src/models/event"; -import { TestClient } from "../../../TestClient"; import { Room } from "../../../../src/models/room"; import * as olmlib from "../../../../src/crypto/olmlib"; -import { TypedEventEmitter } from "../../../../src/models/typed-event-emitter"; -import { ClientEvent, MatrixClient, RoomMember } from "../../../../src"; -import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo"; +import { MatrixClient, RoomMember } from "../../../../src"; +import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning"; import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm"; -import { recursiveMapToObject } from "../../../../src/utils"; import { sleep } from "../../../../src/utils"; -import { KnownMembership } from "../../../../src/@types/membership"; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!; @@ -605,505 +601,4 @@ describe("MegolmDecryption", function () { expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before); }); }); - - it("notifies devices that have been blocked", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client; - const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client; - await Promise.all([ - aliceClient.initLegacyCrypto(), - bobClient1.initLegacyCrypto(), - bobClient2.initLegacyCrypto(), - ]); - const aliceDevice = aliceClient.crypto!.olmDevice; - const bobDevice1 = bobClient1.crypto!.olmDevice; - const bobDevice2 = bobClient2.crypto!.olmDevice; - - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const room = new Room(roomId, aliceClient, "@alice:example.com", {}); - - const bobMember = new RoomMember(roomId, "@bob:example.com"); - room.getEncryptionTargetMembers = async function () { - return [bobMember]; - }; - room.setBlacklistUnverifiedDevices(true); - aliceClient.store.storeRoom(room); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - - const BOB_DEVICES: Record = { - bobdevice1: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Dynabook": bobDevice1.deviceEd25519Key!, - "curve25519:Dynabook": bobDevice1.deviceCurve25519Key!, - }, - verified: 0, - known: false, - }, - bobdevice2: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Dynabook": bobDevice2.deviceEd25519Key!, - "curve25519:Dynabook": bobDevice2.deviceCurve25519Key!, - }, - verified: -1, - known: false, - }, - }; - - aliceClient.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); - aliceClient.crypto!.deviceList.downloadKeys = async function (userIds) { - // @ts-ignore short-circuiting private method - return this.getDevicesFromStore(userIds); - }; - - aliceClient.sendToDevice = jest.fn().mockResolvedValue({}); - - const event = new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$event", - content: { - msgtype: "m.text", - body: "secret", - }, - }); - await aliceClient.crypto!.encryptEvent(event, room); - - expect(aliceClient.sendToDevice).toHaveBeenCalled(); - const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; - expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); - delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"]; - delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"]; - delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"]; - delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"]; - expect(recursiveMapToObject(contentMap)).toStrictEqual({ - ["@bob:example.com"]: { - ["bobdevice1"]: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - code: "m.unverified", - reason: "The sender has disabled encrypting to unverified devices.", - sender_key: aliceDevice.deviceCurve25519Key, - }, - ["bobdevice2"]: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - code: "m.blacklisted", - reason: "The sender has blocked you.", - sender_key: aliceDevice.deviceCurve25519Key, - }, - }, - }); - - aliceClient.stopClient(); - bobClient1.stopClient(); - bobClient2.stopClient(); - }); - - it("does not block unverified devices when sending verification events", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); - const bobDevice = bobClient.crypto!.olmDevice; - - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const room = new Room(roomId, aliceClient, "@alice:example.com", {}); - - const bobMember = new RoomMember(roomId, "@bob:example.com"); - room.getEncryptionTargetMembers = async function () { - return [bobMember]; - }; - room.setBlacklistUnverifiedDevices(true); - aliceClient.store.storeRoom(room); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - - const BOB_DEVICES: Record = { - bobdevice: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:bobdevice": bobDevice.deviceEd25519Key!, - "curve25519:bobdevice": bobDevice.deviceCurve25519Key!, - }, - verified: 0, - known: true, - }, - }; - - aliceClient.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); - aliceClient.crypto!.deviceList.downloadKeys = async function (userIds) { - // @ts-ignore private - return this.getDevicesFromStore(userIds); - }; - - await bobDevice.generateOneTimeKeys(1); - const oneTimeKeys = await bobDevice.getOneTimeKeys(); - const signedOneTimeKeys: Record = {}; - for (const keyId in oneTimeKeys.curve25519) { - if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { - const k = { - key: oneTimeKeys.curve25519[keyId], - signatures: {}, - }; - signedOneTimeKeys["signed_curve25519:" + keyId] = k; - await bobClient.crypto!.signObject(k); - break; - } - } - - aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({ - one_time_keys: { - "@bob:example.com": { - bobdevice: signedOneTimeKeys, - }, - }, - failures: {}, - }); - - aliceClient.sendToDevice = jest.fn().mockResolvedValue({}); - - const event = new MatrixEvent({ - type: "m.key.verification.start", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$event", - content: { - from_device: "alicedevice", - method: "m.sas.v1", - transaction_id: "transactionid", - }, - }); - await aliceClient.crypto!.encryptEvent(event, room); - - expect(aliceClient.sendToDevice).toHaveBeenCalled(); - const [msgtype] = mocked(aliceClient.sendToDevice).mock.calls[0]; - expect(msgtype).toEqual("m.room.encrypted"); - - aliceClient.stopClient(); - bobClient.stopClient(); - }); - - it("notifies devices when unable to create olm session", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); - const aliceDevice = aliceClient.crypto!.olmDevice; - const bobDevice = bobClient.crypto!.olmDevice; - - const encryptionCfg = { - algorithm: "m.megolm.v1.aes-sha2", - }; - const roomId = "!someroom"; - const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); - const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); - aliceClient.store.storeRoom(aliceRoom); - bobClient.store.storeRoom(bobRoom); - await aliceClient.setRoomEncryption(roomId, encryptionCfg); - await bobClient.setRoomEncryption(roomId, encryptionCfg); - - aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([ - { - userId: "@alice:example.com", - membership: KnownMembership.Join, - }, - { - userId: "@bob:example.com", - membership: KnownMembership.Join, - }, - ]); - const BOB_DEVICES = { - bobdevice: { - user_id: "@bob:example.com", - device_id: "bobdevice", - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:bobdevice": bobDevice.deviceEd25519Key!, - "curve25519:bobdevice": bobDevice.deviceCurve25519Key!, - }, - known: true, - verified: 1, - }, - }; - - aliceClient.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); - aliceClient.crypto!.deviceList.downloadKeys = async function (userIds) { - // @ts-ignore private - return this.getDevicesFromStore(userIds); - }; - - aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({ - // Bob has no one-time keys - one_time_keys: {}, - failures: {}, - }); - - aliceClient.sendToDevice = jest.fn().mockResolvedValue({}); - - const event = new MatrixEvent({ - type: "m.room.message", - sender: "@alice:example.com", - room_id: roomId, - event_id: "$event", - content: {}, - }); - await aliceClient.crypto!.encryptEvent(event, aliceRoom); - - expect(aliceClient.sendToDevice).toHaveBeenCalled(); - const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; - expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); - delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"]; - expect(recursiveMapToObject(contentMap)).toStrictEqual({ - ["@bob:example.com"]: { - ["bobdevice"]: { - algorithm: "m.megolm.v1.aes-sha2", - code: "m.no_olm", - reason: "Unable to establish a secure channel.", - sender_key: aliceDevice.deviceCurve25519Key, - }, - }, - }); - - aliceClient.stopClient(); - bobClient.stopClient(); - }); - - it("throws an error describing why it doesn't have a key", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); - const bobDevice = bobClient.crypto!.olmDevice; - - const aliceEventEmitter = new TypedEventEmitter(); - aliceClient.crypto!.registerEventHandlers(aliceEventEmitter); - - const roomId = "!someroom"; - - aliceEventEmitter.emit( - ClientEvent.ToDeviceEvent, - new MatrixEvent({ - type: "m.room_key.withheld", - sender: "@bob:example.com", - content: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - session_id: "session_id1", - sender_key: bobDevice.deviceCurve25519Key, - code: "m.blacklisted", - reason: "You have been blocked", - }, - }), - ); - - await expect( - aliceClient.crypto!.decryptEvent( - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - event_id: "$event", - room_id: roomId, - content: { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "blablabla", - device_id: "bobdevice", - sender_key: bobDevice.deviceCurve25519Key, - session_id: "session_id1", - }, - }), - ), - ).rejects.toThrow("The sender has blocked you."); - - aliceEventEmitter.emit( - ClientEvent.ToDeviceEvent, - new MatrixEvent({ - type: "m.room_key.withheld", - sender: "@bob:example.com", - content: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - session_id: "session_id2", - sender_key: bobDevice.deviceCurve25519Key, - code: "m.blacklisted", - reason: "You have been blocked", - }, - }), - ); - - await expect( - aliceClient.crypto!.decryptEvent( - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - event_id: "$event", - room_id: roomId, - content: { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "blablabla", - device_id: "bobdevice", - sender_key: bobDevice.deviceCurve25519Key, - session_id: "session_id2", - }, - }), - ), - ).rejects.toThrow("The sender has blocked you."); - aliceClient.stopClient(); - bobClient.stopClient(); - }); - - it("throws an error describing the lack of an olm session", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); - - const aliceEventEmitter = new TypedEventEmitter(); - aliceClient.crypto!.registerEventHandlers(aliceEventEmitter); - - aliceClient.crypto!.downloadKeys = jest.fn(); - const bobDevice = bobClient.crypto!.olmDevice; - - const roomId = "!someroom"; - - const now = Date.now(); - - aliceEventEmitter.emit( - ClientEvent.ToDeviceEvent, - new MatrixEvent({ - type: "m.room_key.withheld", - sender: "@bob:example.com", - content: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - session_id: "session_id1", - sender_key: bobDevice.deviceCurve25519Key, - code: "m.no_olm", - reason: "Unable to establish a secure channel.", - }, - }), - ); - - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - - await expect( - aliceClient.crypto!.decryptEvent( - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - event_id: "$event", - room_id: roomId, - content: { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "blablabla", - device_id: "bobdevice", - sender_key: bobDevice.deviceCurve25519Key, - session_id: "session_id1", - }, - origin_server_ts: now, - }), - ), - ).rejects.toThrow("The sender was unable to establish a secure channel."); - - aliceEventEmitter.emit( - ClientEvent.ToDeviceEvent, - new MatrixEvent({ - type: "m.room_key.withheld", - sender: "@bob:example.com", - content: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: roomId, - session_id: "session_id2", - sender_key: bobDevice.deviceCurve25519Key, - code: "m.no_olm", - reason: "Unable to establish a secure channel.", - }, - }), - ); - - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - - await expect( - aliceClient.crypto!.decryptEvent( - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - event_id: "$event", - room_id: roomId, - content: { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "blablabla", - device_id: "bobdevice", - sender_key: bobDevice.deviceCurve25519Key, - session_id: "session_id2", - }, - origin_server_ts: now, - }), - ), - ).rejects.toThrow("The sender was unable to establish a secure channel."); - aliceClient.stopClient(); - bobClient.stopClient(); - }); - - it("throws an error to indicate a wedged olm session", async function () { - const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; - const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); - const aliceEventEmitter = new TypedEventEmitter(); - aliceClient.crypto!.registerEventHandlers(aliceEventEmitter); - - const bobDevice = bobClient.crypto!.olmDevice; - aliceClient.crypto!.downloadKeys = jest.fn(); - - const roomId = "!someroom"; - - const now = Date.now(); - - // pretend we got an event that we can't decrypt - aliceEventEmitter.emit( - ClientEvent.ToDeviceEvent, - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - content: { - msgtype: "m.bad.encrypted", - algorithm: "m.megolm.v1.aes-sha2", - session_id: "session_id", - sender_key: bobDevice.deviceCurve25519Key, - }, - }), - ); - - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - - await expect( - aliceClient.crypto!.decryptEvent( - new MatrixEvent({ - type: "m.room.encrypted", - sender: "@bob:example.com", - event_id: "$event", - room_id: roomId, - content: { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "blablabla", - device_id: "bobdevice", - sender_key: bobDevice.deviceCurve25519Key, - session_id: "session_id", - }, - origin_server_ts: now, - }), - ), - ).rejects.toThrow("The secure channel with the sender was corrupted."); - aliceClient.stopClient(); - bobClient.stopClient(); - }); }); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index c210d14c80b..6fc367959bf 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -25,13 +25,11 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store import * as testUtils from "../../test-utils/test-utils"; import { OlmDevice } from "../../../src/crypto/OlmDevice"; import { Crypto } from "../../../src/crypto"; -import { resetCrossSigningKeys } from "./crypto-utils"; import { BackupManager } from "../../../src/crypto/backup"; import { StubStore } from "../../../src/store/stub"; -import { IndexedDBCryptoStore, MatrixScheduler } from "../../../src"; +import { MatrixScheduler } from "../../../src"; import { CryptoStore } from "../../../src/crypto/store/base"; import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm"; -import { IKeyBackupInfo } from "../../../src/crypto/keybackup"; const Olm = globalThis.Olm; @@ -39,65 +37,6 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2 const ROOM_ID = "!ROOM:ID"; -const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc"; -const ENCRYPTED_EVENT = new MatrixEvent({ - type: "m.room.encrypted", - room_id: "!ROOM:ID", - content: { - algorithm: "m.megolm.v1.aes-sha2", - sender_key: "SENDER_CURVE25519", - session_id: SESSION_ID, - ciphertext: - "AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" + - "CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" + - "mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs", - }, - event_id: "$event1", - origin_server_ts: 1507753886000, -}); - -const CURVE25519_KEY_BACKUP_DATA = { - first_message_index: 0, - forwarded_count: 0, - is_verified: false, - session_data: { - ciphertext: - "2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" + - "6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" + - "Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" + - "SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" + - "Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" + - "ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" + - "4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" + - "C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" + - "Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" + - "QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" + - "iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg", - mac: "5lxYBHQU80M", - ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14", - }, -}; - -const AES256_KEY_BACKUP_DATA = { - first_message_index: 0, - forwarded_count: 0, - is_verified: false, - session_data: { - iv: "b3Jqqvm5S9QdmXrzssspLQ", - ciphertext: - "GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce" + - "7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd" + - "EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0" + - "WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r" + - "KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P" + - "vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K" + - "YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd" + - "fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA" + - "RgaDHkfzoA3g3aeQ", - mac: "uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU", - }, -}; - const CURVE25519_BACKUP_INFO = { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, version: "1", @@ -106,12 +45,6 @@ const CURVE25519_BACKUP_INFO = { }, }; -const AES256_BACKUP_INFO: IKeyBackupInfo = { - algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", - version: "1", - auth_data: {} as IKeyBackupInfo["auth_data"], -}; - const keys: Record = {}; function getCrossSigningKey(type: string) { @@ -229,22 +162,6 @@ describe("MegolmBackup", function () { ); }); - test("fail if given backup has no version", async () => { - const client = makeTestClient(cryptoStore); - await client.initLegacyCrypto(); - const data = { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, - }; - const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); - await client.getCrypto()!.storeSessionBackupPrivateKey(key, "1"); - await expect(client.restoreKeyBackupWithCache(undefined, undefined, data)).rejects.toThrow( - "Backup version must be defined", - ); - }); - it("automatically calls the key back up", function () { const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -293,499 +210,5 @@ describe("MegolmBackup", function () { expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled(); }); }); - - it("sends backups to the server (Curve25519 version)", function () { - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - const ibGroupSession = new Olm.InboundGroupSession(); - ibGroupSession.create(groupSession.session_key()); - - const client = makeTestClient(cryptoStore); - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: client, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - return client - .initLegacyCrypto() - .then(() => { - return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - cryptoStore.addEndToEndInboundGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - { - forwardingCurve25519KeyChain: undefined!, - keysClaimed: { - ed25519: "SENDER_ED25519", - }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice.pickleKey), - }, - txn, - ); - }); - }) - .then(async () => { - await client.enableKeyBackup({ - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - version: "1", - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, - }); - let numCalls = 0; - return new Promise((resolve, reject) => { - client.http.authedRequest = function (method, path, queryParams, data, opts): any { - ++numCalls; - expect(numCalls).toBeLessThanOrEqual(1); - if (numCalls >= 2) { - // exit out of retry loop if there's something wrong - reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}); - } - expect(method).toBe("PUT"); - expect(path).toBe("/room_keys/keys"); - expect(queryParams?.version).toBe("1"); - expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); - expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( - groupSession.session_id(), - ); - resolve(); - return Promise.resolve({}); - }; - client.crypto!.backupManager.backupGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - ); - }).then(() => { - expect(numCalls).toBe(1); - client.stopClient(); - }); - }); - }); - - it("sends backups to the server (AES-256 version)", function () { - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - const ibGroupSession = new Olm.InboundGroupSession(); - ibGroupSession.create(groupSession.session_key()); - - const client = makeTestClient(cryptoStore); - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: client, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - return client - .initLegacyCrypto() - .then(() => { - return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32)); - }) - .then(() => { - return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - cryptoStore.addEndToEndInboundGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - { - forwardingCurve25519KeyChain: undefined!, - keysClaimed: { - ed25519: "SENDER_ED25519", - }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice.pickleKey), - }, - txn, - ); - }); - }) - .then(async () => { - await client.enableKeyBackup({ - algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", - version: "1", - auth_data: { - iv: "PsCAtR7gMc4xBd9YS3A9Ow", - mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ", - }, - }); - let numCalls = 0; - return new Promise((resolve, reject) => { - client.http.authedRequest = function (method, path, queryParams, data, opts): any { - ++numCalls; - expect(numCalls).toBeLessThanOrEqual(1); - if (numCalls >= 2) { - // exit out of retry loop if there's something wrong - reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}); - } - expect(method).toBe("PUT"); - expect(path).toBe("/room_keys/keys"); - expect(queryParams?.version).toBe("1"); - expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); - expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( - groupSession.session_id(), - ); - resolve(); - return Promise.resolve({}); - }; - client.crypto!.backupManager.backupGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - ); - }).then(() => { - expect(numCalls).toBe(1); - client.stopClient(); - }); - }); - }); - - it("signs backups with the cross-signing master key", async function () { - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - const ibGroupSession = new Olm.InboundGroupSession(); - ibGroupSession.create(groupSession.session_key()); - - const client = makeTestClient(cryptoStore); - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: client, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - await client.initLegacyCrypto(); - client.uploadDeviceSigningKeys = async function (e) { - return {}; - }; - client.uploadKeySignatures = async function (e) { - return { failures: {} }; - }; - await resetCrossSigningKeys(client); - let numCalls = 0; - await Promise.all([ - new Promise((resolve, reject) => { - let backupInfo: Record | BodyInit | undefined; - client.http.authedRequest = function (method, path, queryParams, data, opts): any { - ++numCalls; - expect(numCalls).toBeLessThanOrEqual(2); - /* eslint-disable jest/no-conditional-expect */ - if (numCalls === 1) { - expect(method).toBe("POST"); - expect(path).toBe("/room_keys/version"); - try { - // make sure auth_data is signed by the master key - olmlib.pkVerify( - (data as Record).auth_data, - client.getCrossSigningId()!, - "@alice:bar", - ); - } catch (e) { - reject(e); - return Promise.resolve({}); - } - backupInfo = data; - return Promise.resolve({}); - } else if (numCalls === 2) { - expect(method).toBe("GET"); - expect(path).toBe("/room_keys/version"); - resolve(); - return Promise.resolve(backupInfo); - } else { - // exit out of retry loop if there's something wrong - reject(new Error("authedRequest called too many times")); - return Promise.resolve({}); - } - /* eslint-enable jest/no-conditional-expect */ - }; - }), - client.createKeyBackupVersion({ - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, - }), - ]); - expect(numCalls).toBe(2); - client.stopClient(); - }); - - it("retries when a backup fails", async function () { - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - const ibGroupSession = new Olm.InboundGroupSession(); - ibGroupSession.create(groupSession.session_key()); - - const scheduler = makeTestScheduler(); - const store = new StubStore(); - const client = new MatrixClient({ - baseUrl: "https://my.home.server", - idBaseUrl: "https://identity.server", - accessToken: "my.access.token", - fetchFn: jest.fn(), // NOP - store: store, - scheduler: scheduler, - userId: "@alice:bar", - deviceId: "device", - cryptoStore: cryptoStore, - }); - // initialising the crypto library will trigger a key upload request, which we can stub out - client.uploadKeysRequest = jest.fn(); - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: client, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - await client.initLegacyCrypto(); - await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - cryptoStore.addEndToEndInboundGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - { - forwardingCurve25519KeyChain: undefined!, - keysClaimed: { - ed25519: "SENDER_ED25519", - }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice.pickleKey), - }, - txn, - ); - }); - - await client.enableKeyBackup({ - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - version: "1", - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, - }); - let numCalls = 0; - - await new Promise((resolve, reject) => { - client.http.authedRequest = function (method, path, queryParams, data, opts): any { - ++numCalls; - expect(numCalls).toBeLessThanOrEqual(2); - if (numCalls >= 3) { - // exit out of retry loop if there's something wrong - reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}); - } - expect(method).toBe("PUT"); - expect(path).toBe("/room_keys/keys"); - expect(queryParams?.version).toBe("1"); - expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); - expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( - groupSession.session_id(), - ); - if (numCalls > 1) { - resolve(); - return Promise.resolve({}); - } else { - return Promise.reject(new Error("this is an expected failure")); - } - }; - return client.crypto!.backupManager.backupGroupSession( - "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", - groupSession.session_id(), - ); - }); - expect(numCalls).toBe(2); - client.stopClient(); - }); - }); - - describe("restore", function () { - let client: MatrixClient; - - beforeEach(function () { - client = makeTestClient(cryptoStore); - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: client, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - return client.initLegacyCrypto(); - }); - - afterEach(function () { - client.stopClient(); - }); - - it("can restore from backup (Curve25519 version)", function () { - client.http.authedRequest = function () { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); - }; - return client - .restoreKeyBackupWithRecoveryKey( - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - ROOM_ID, - SESSION_ID, - CURVE25519_BACKUP_INFO, - ) - .then(() => { - return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); - }) - .then((res) => { - expect(res.clearEvent.content).toEqual("testytest"); - expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted - }); - }); - - it("can restore from backup (AES-256 version)", function () { - client.http.authedRequest = function () { - return Promise.resolve(AES256_KEY_BACKUP_DATA); - }; - return client - .restoreKeyBackupWithRecoveryKey( - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - ROOM_ID, - SESSION_ID, - AES256_BACKUP_INFO, - ) - .then(() => { - return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); - }) - .then((res) => { - expect(res.clearEvent.content).toEqual("testytest"); - expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted - }); - }); - - it("can restore backup by room (Curve25519 version)", function () { - client.http.authedRequest = function () { - return Promise.resolve({ - rooms: { - [ROOM_ID]: { - sessions: { - [SESSION_ID]: CURVE25519_KEY_BACKUP_DATA, - }, - }, - }, - }); - }; - return client - .restoreKeyBackupWithRecoveryKey( - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null!, - null!, - CURVE25519_BACKUP_INFO, - ) - .then(() => { - return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); - }) - .then((res) => { - expect(res.clearEvent.content).toEqual("testytest"); - }); - }); - - it("has working cache functions", async function () { - const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); - await client.crypto!.storeSessionBackupPrivateKey(key); - const result = await client.crypto!.getSessionBackupPrivateKey(); - expect(new Uint8Array(result!)).toEqual(key); - }); - - it("caches session backup keys as it encounters them", async function () { - const cachedNull = await client.crypto!.getSessionBackupPrivateKey(); - expect(cachedNull).toBeNull(); - client.http.authedRequest = function () { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); - }; - await new Promise((resolve) => { - client.restoreKeyBackupWithRecoveryKey( - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - ROOM_ID, - SESSION_ID, - CURVE25519_BACKUP_INFO, - { cacheCompleteCallback: resolve }, - ); - }); - const cachedKey = await client.crypto!.getSessionBackupPrivateKey(); - expect(cachedKey).not.toBeNull(); - }); - - it("fails if an known algorithm is used", async function () { - const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, { - algorithm: "this.algorithm.does.not.exist", - }); - client.http.authedRequest = function () { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); - }; - - await expect( - client.restoreKeyBackupWithRecoveryKey( - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - ROOM_ID, - SESSION_ID, - BAD_BACKUP_INFO, - ), - ).rejects.toThrow(); - }); - }); - - describe("flagAllGroupSessionsForBackup", () => { - it("should return number of sesions needing backup", async () => { - const scheduler = makeTestScheduler(); - const store = new StubStore(); - const client = new MatrixClient({ - baseUrl: "https://my.home.server", - idBaseUrl: "https://identity.server", - accessToken: "my.access.token", - fetchFn: jest.fn(), // NOP - store, - scheduler, - userId: "@alice:bar", - deviceId: "device", - cryptoStore, - }); - // initialising the crypto library will trigger a key upload request, which we can stub out - client.uploadKeysRequest = jest.fn(); - - await client.initLegacyCrypto(); - - cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6); - await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6); - client.stopClient(); - }); - }); - - describe("getKeyBackupInfo", () => { - it("should return throw an `Not implemented`", async () => { - const client = makeTestClient(cryptoStore); - await client.initLegacyCrypto(); - await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented"); - }); }); }); diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts deleted file mode 100644 index a8b7fa2624b..00000000000 --- a/spec/unit/crypto/cross-signing.spec.ts +++ /dev/null @@ -1,1152 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 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 "../../olm-loader"; -import anotherjson from "another-json"; -import { PkSigning } from "@matrix-org/olm"; -import HttpBackend from "matrix-mock-request"; - -import * as olmlib from "../../../src/crypto/olmlib"; -import { MatrixError } from "../../../src/http-api"; -import { logger } from "../../../src/logger"; -import { ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client"; -import { CryptoEvent } from "../../../src/crypto"; -import { IDevice } from "../../../src/crypto/deviceinfo"; -import { TestClient } from "../../TestClient"; -import { resetCrossSigningKeys } from "./crypto-utils"; -import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../../../src/crypto-api"; - -const PUSH_RULES_RESPONSE: Response = { - method: "GET", - path: "/pushrules/", - data: {}, -}; - -const filterResponse = function (userId: string): Response { - const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; - return { - method: "POST", - path: filterPath, - data: { filter_id: "f1lt3r" }, - }; -}; - -interface Response { - method: "GET" | "PUT" | "POST" | "DELETE"; - path: string; - data: object; -} - -function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) { - responses.forEach((response) => { - httpBackend.when(response.method, response.path).respond(200, response.data); - }); -} - -async function makeTestClient( - userInfo: { userId: string; deviceId: string }, - options: Partial = {}, - keys: Record = {}, -) { - function getCrossSigningKey(type: string) { - return keys[type] ?? null; - } - - function saveCrossSigningKeys(k: Record) { - Object.assign(keys, k); - } - - options.cryptoCallbacks = Object.assign( - {}, - { getCrossSigningKey, saveCrossSigningKeys }, - options.cryptoCallbacks || {}, - ); - const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options); - const client = testClient.client; - - await client.initLegacyCrypto(); - - return { client, httpBackend: testClient.httpBackend }; -} - -describe("Cross Signing", function () { - if (!globalThis.Olm) { - logger.warn("Not running megolm backup unit tests: libolm not present"); - return; - } - - beforeAll(function () { - return globalThis.Olm.init(); - }); - - it("should sign the master key with the device key", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => { - await olmlib.verifySignature( - alice.crypto!.olmDevice, - keys.master_key, - "@alice:example.com", - "Osborne2", - alice.crypto!.olmDevice.deviceEd25519Key!, - ); - }); - alice.uploadKeySignatures = async () => ({ failures: {} }); - alice.setAccountData = async () => ({}); - alice.getAccountDataFromServer = async () => ({}) as T; - // set Alice's cross-signing key - await alice.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({}); - }, - }); - expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); - alice.stopClient(); - }); - - it("should abort bootstrap if device signing auth fails", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async (auth, keys) => { - const errorResponse = { - session: "sessionId", - flows: [ - { - stages: ["m.login.password"], - }, - ], - params: {}, - }; - - // If we're not just polling for flows, add on error rejecting the - // auth attempt. - if (auth) { - Object.assign(errorResponse, { - completed: [], - error: "Invalid password", - errcode: "M_FORBIDDEN", - }); - } - - throw new MatrixError(errorResponse, 401); - }; - alice.uploadKeySignatures = async () => ({ failures: {} }); - alice.setAccountData = async () => ({}); - alice.getAccountDataFromServer = async (): Promise => ({}) as T; - const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => { - await func({}); - }; - - // Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass - // through failure, stopping before actually applying changes. - let bootstrapDidThrow = false; - try { - await alice.bootstrapCrossSigning({ - authUploadDeviceSigningKeys, - }); - } catch (e) { - if ((e).errcode === "M_FORBIDDEN") { - bootstrapDidThrow = true; - } - } - expect(bootstrapDidThrow).toBeTruthy(); - alice.stopClient(); - }); - - it("should upload a signature when a user is verified", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - // set Alice's cross-signing key - await resetCrossSigningKeys(alice); - // Alice downloads Bob's device key - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { - keys: { - master: { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - "ed25519:bobs+master+pubkey": "bobs+master+pubkey", - }, - }, - }, - firstUse: false, - crossSigningVerifiedBefore: false, - }); - // Alice verifies Bob's key - const promise = new Promise((resolve, reject) => { - alice.uploadKeySignatures = async (...args) => { - resolve(...args); - return { failures: {} }; - }; - }); - await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true); - // Alice should send a signature of Bob's key to the server - await promise; - alice.stopClient(); - }); - - it.skip("should get cross-signing keys from sync", async function () { - const masterKey = new Uint8Array([ - 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, - 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d, - ]); - const selfSigningKey = new Uint8Array([ - 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5, - 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f, - ]); - - const { client: alice, httpBackend } = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - // will be called to sign our own device - getCrossSigningKey: async (type) => { - if (type === "master") { - return masterKey; - } else { - return selfSigningKey; - } - }, - }, - }, - ); - - const keyChangePromise = new Promise((resolve, reject) => { - alice.once(CryptoEvent.KeysChanged, async (e) => { - resolve(e); - await alice.checkOwnCrossSigningTrust({ - allowPrivateKeyRequests: true, - }); - }); - }); - - const uploadSigsPromise = new Promise((resolve, reject) => { - alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => { - try { - await olmlib.verifySignature( - alice.crypto!.olmDevice, - content["@alice:example.com"]["nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"], - "@alice:example.com", - "Osborne2", - alice.crypto!.olmDevice.deviceEd25519Key!, - ); - olmlib.pkVerify( - content["@alice:example.com"]["Osborne2"], - "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", - "@alice:example.com", - ); - resolve(); - } catch (e) { - reject(e); - } - }); - }); - - // @ts-ignore private property - const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2; - const aliceDevice = { - user_id: "@alice:example.com", - device_id: "Osborne2", - keys: deviceInfo.keys, - algorithms: deviceInfo.algorithms, - }; - await alice.crypto!.signObject(aliceDevice); - olmlib.pkSign(aliceDevice as ISignedKey, selfSigningKey as unknown as PkSigning, "@alice:example.com", ""); - - // feed sync result that includes master key, ssk, device key - const responses: Response[] = [ - PUSH_RULES_RESPONSE, - { - method: "POST", - path: "/keys/upload", - data: { - one_time_key_counts: { - curve25519: 100, - signed_curve25519: 100, - }, - }, - }, - filterResponse("@alice:example.com"), - { - method: "GET", - path: "/sync", - data: { - next_batch: "abcdefg", - device_lists: { - changed: ["@alice:example.com", "@bob:example.com"], - }, - }, - }, - { - method: "POST", - path: "/keys/query", - data: { - failures: {}, - device_keys: { - "@alice:example.com": { - Osborne2: aliceDevice, - }, - }, - master_keys: { - "@alice:example.com": { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": - "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", - }, - }, - }, - self_signing_keys: { - "@alice:example.com": { - user_id: "@alice:example.com", - usage: ["self-signing"], - keys: { - "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": - "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", - }, - signatures: { - "@alice:example.com": { - "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": - "Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" + - "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA", - }, - }, - }, - }, - }, - }, - { - method: "POST", - path: "/keys/upload", - data: { - one_time_key_counts: { - curve25519: 100, - signed_curve25519: 100, - }, - }, - }, - ]; - setHttpResponses(httpBackend, responses); - - alice.startClient(); - httpBackend.flushAllExpected(); - - // once ssk is confirmed, device key should be trusted - await keyChangePromise; - await uploadSigsPromise; - - const aliceTrust = alice.checkUserTrust("@alice:example.com"); - expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); - expect(aliceTrust.isTofu()).toBeTruthy(); - expect(aliceTrust.isVerified()).toBeTruthy(); - - const aliceDeviceTrust = alice.checkDeviceTrust("@alice:example.com", "Osborne2"); - expect(aliceDeviceTrust.isCrossSigningVerified()).toBeTruthy(); - expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); - expect(aliceDeviceTrust.isTofu()).toBeTruthy(); - expect(aliceDeviceTrust.isVerified()).toBeTruthy(); - alice.stopClient(); - }); - - it("should use trust chain to determine device verification", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - // set Alice's cross-signing key - await resetCrossSigningKeys(alice); - // Alice downloads Bob's ssk and device key - const bobMasterSigning = new globalThis.Olm.PkSigning(); - const bobMasterPrivkey = bobMasterSigning.generate_seed(); - const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); - const bobSigning = new globalThis.Olm.PkSigning(); - const bobPrivkey = bobSigning.generate_seed(); - const bobPubkey = bobSigning.init_with_seed(bobPrivkey); - const bobSSK: CrossSigningKeyInfo = { - user_id: "@bob:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + bobPubkey]: bobPubkey, - }, - }; - const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); - bobSSK.signatures = { - "@bob:example.com": { - ["ed25519:" + bobMasterPubkey]: sskSig, - }, - }; - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { - keys: { - master: { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, - }, - }, - self_signing: bobSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - const bobDeviceUnsigned = { - user_id: "@bob:example.com", - device_id: "Dynabook", - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": "somePubkey", - "ed25519:Dynabook": "someOtherPubkey", - }, - }; - const sig = bobSigning.sign(anotherjson.stringify(bobDeviceUnsigned)); - const bobDevice: IDevice = { - ...bobDeviceUnsigned, - signatures: { - "@bob:example.com": { - ["ed25519:" + bobPubkey]: sig, - }, - }, - verified: 0, - known: false, - }; - alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: bobDevice, - }); - // Bob's device key should be TOFU - const bobTrust = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust.isVerified()).toBeFalsy(); - expect(bobTrust.isTofu()).toBeTruthy(); - - const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust.isVerified()).toBeFalsy(); - expect(bobDeviceTrust.isTofu()).toBeTruthy(); - - // Alice verifies Bob's SSK - alice.uploadKeySignatures = async () => ({ failures: {} }); - await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); - - // Bob's device key should be trusted - const bobTrust2 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust2.isCrossSigningVerified()).toBeTruthy(); - expect(bobTrust2.isTofu()).toBeTruthy(); - - const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy(); - expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy(); - expect(bobDeviceTrust2.isTofu()).toBeTruthy(); - alice.stopClient(); - }); - - it.skip("should trust signatures received from other devices", async function () { - const aliceKeys: Record = {}; - const { client: alice, httpBackend } = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - undefined, - aliceKeys, - ); - alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com"); - alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {}; - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - - // set Alice's cross-signing key - await resetCrossSigningKeys(alice); - - const selfSigningKey = new Uint8Array([ - 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5, - 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f, - ]); - - const keyChangePromise = new Promise((resolve, reject) => { - alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => { - if (userId === "@bob:example.com") { - resolve(); - } - }); - }); - - // @ts-ignore private property - const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2; - const aliceDevice = { - user_id: "@alice:example.com", - device_id: "Osborne2", - keys: deviceInfo.keys, - algorithms: deviceInfo.algorithms, - }; - await alice.crypto!.signObject(aliceDevice); - - const bobOlmAccount = new globalThis.Olm.Account(); - bobOlmAccount.create(); - const bobKeys = JSON.parse(bobOlmAccount.identity_keys()); - const bobDeviceUnsigned = { - user_id: "@bob:example.com", - device_id: "Dynabook", - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Dynabook": bobKeys.ed25519, - "curve25519:Dynabook": bobKeys.curve25519, - }, - }; - const deviceStr = anotherjson.stringify(bobDeviceUnsigned); - const bobDevice: IDevice = { - ...bobDeviceUnsigned, - signatures: { - "@bob:example.com": { - "ed25519:Dynabook": bobOlmAccount.sign(deviceStr), - }, - }, - verified: 0, - known: false, - }; - olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", ""); - - const bobMaster: CrossSigningKeyInfo = { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", - }, - }; - olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", ""); - - // Alice downloads Bob's keys - // - device key - // - ssk - // - master key signed by her usk (pretend that it was signed by another - // of Alice's devices) - const responses: Response[] = [ - PUSH_RULES_RESPONSE, - { - method: "POST", - path: "/keys/upload", - data: { - one_time_key_counts: { - curve25519: 100, - signed_curve25519: 100, - }, - }, - }, - filterResponse("@alice:example.com"), - { - method: "GET", - path: "/sync", - data: { - next_batch: "abcdefg", - device_lists: { - changed: ["@bob:example.com"], - }, - }, - }, - { - method: "POST", - path: "/keys/query", - data: { - failures: {}, - device_keys: { - "@alice:example.com": { - Osborne2: aliceDevice, - }, - "@bob:example.com": { - Dynabook: bobDevice, - }, - }, - master_keys: { - "@bob:example.com": bobMaster, - }, - self_signing_keys: { - "@bob:example.com": { - user_id: "@bob:example.com", - usage: ["self-signing"], - keys: { - "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": - "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", - }, - signatures: { - "@bob:example.com": { - "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": - "2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" + - "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ", - }, - }, - }, - }, - }, - }, - { - method: "POST", - path: "/keys/upload", - data: { - one_time_key_counts: { - curve25519: 100, - signed_curve25519: 100, - }, - }, - }, - ]; - setHttpResponses(httpBackend, responses); - - alice.startClient(); - httpBackend.flushAllExpected(); - await keyChangePromise; - - // Bob's device key should be trusted - const bobTrust = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); - expect(bobTrust.isTofu()).toBeTruthy(); - - const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy(); - expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy(); - expect(bobDeviceTrust.isTofu()).toBeTruthy(); - alice.stopClient(); - }); - - it("should dis-trust an unsigned device", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - // set Alice's cross-signing key - await resetCrossSigningKeys(alice); - // Alice downloads Bob's ssk and device key - // (NOTE: device key is not signed by ssk) - const bobMasterSigning = new globalThis.Olm.PkSigning(); - const bobMasterPrivkey = bobMasterSigning.generate_seed(); - const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); - const bobSigning = new globalThis.Olm.PkSigning(); - const bobPrivkey = bobSigning.generate_seed(); - const bobPubkey = bobSigning.init_with_seed(bobPrivkey); - const bobSSK: CrossSigningKeyInfo = { - user_id: "@bob:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + bobPubkey]: bobPubkey, - }, - }; - const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); - bobSSK.signatures = { - "@bob:example.com": { - ["ed25519:" + bobMasterPubkey]: sskSig, - }, - }; - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { - keys: { - master: { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, - }, - }, - self_signing: bobSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - const bobDevice = { - user_id: "@bob:example.com", - device_id: "Dynabook", - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": "somePubkey", - "ed25519:Dynabook": "someOtherPubkey", - }, - }; - alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: bobDevice as unknown as IDevice, - }); - // Bob's device key should be untrusted - const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust.isVerified()).toBeFalsy(); - expect(bobDeviceTrust.isTofu()).toBeFalsy(); - - // Alice verifies Bob's SSK - await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); - - // Bob's device key should be untrusted - const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust2.isVerified()).toBeFalsy(); - expect(bobDeviceTrust2.isTofu()).toBeFalsy(); - alice.stopClient(); - }); - - it("should dis-trust a user when their ssk changes", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - await resetCrossSigningKeys(alice); - // Alice downloads Bob's keys - const bobMasterSigning = new globalThis.Olm.PkSigning(); - const bobMasterPrivkey = bobMasterSigning.generate_seed(); - const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); - const bobSigning = new globalThis.Olm.PkSigning(); - const bobPrivkey = bobSigning.generate_seed(); - const bobPubkey = bobSigning.init_with_seed(bobPrivkey); - const bobSSK: CrossSigningKeyInfo = { - user_id: "@bob:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + bobPubkey]: bobPubkey, - }, - }; - const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); - bobSSK.signatures = { - "@bob:example.com": { - ["ed25519:" + bobMasterPubkey]: sskSig, - }, - }; - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { - keys: { - master: { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, - }, - }, - self_signing: bobSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - const bobDeviceUnsigned = { - user_id: "@bob:example.com", - device_id: "Dynabook", - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": "somePubkey", - "ed25519:Dynabook": "someOtherPubkey", - }, - }; - const bobDeviceString = anotherjson.stringify(bobDeviceUnsigned); - const sig = bobSigning.sign(bobDeviceString); - const bobDevice: IDevice = { - ...bobDeviceUnsigned, - verified: 0, - known: false, - signatures: { - "@bob:example.com": { - ["ed25519:" + bobPubkey]: sig, - }, - }, - }; - alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: bobDevice, - }); - // Alice verifies Bob's SSK - alice.uploadKeySignatures = async () => ({ failures: {} }); - await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); - - // Bob's device key should be trusted - const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust.isVerified()).toBeTruthy(); - expect(bobDeviceTrust.isTofu()).toBeTruthy(); - - // Alice downloads new SSK for Bob - const bobMasterSigning2 = new globalThis.Olm.PkSigning(); - const bobMasterPrivkey2 = bobMasterSigning2.generate_seed(); - const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2); - const bobSigning2 = new globalThis.Olm.PkSigning(); - const bobPrivkey2 = bobSigning2.generate_seed(); - const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2); - const bobSSK2: CrossSigningKeyInfo = { - user_id: "@bob:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + bobPubkey2]: bobPubkey2, - }, - }; - const sskSig2 = bobMasterSigning2.sign(anotherjson.stringify(bobSSK2)); - bobSSK2.signatures = { - "@bob:example.com": { - ["ed25519:" + bobMasterPubkey2]: sskSig2, - }, - }; - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { - keys: { - master: { - user_id: "@bob:example.com", - usage: ["master"], - keys: { - ["ed25519:" + bobMasterPubkey2]: bobMasterPubkey2, - }, - }, - self_signing: bobSSK2, - }, - firstUse: false, - crossSigningVerifiedBefore: false, - }); - // Bob's and his device should be untrusted - const bobTrust = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust.isVerified()).toBeFalsy(); - expect(bobTrust.isTofu()).toBeFalsy(); - - const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust2.isVerified()).toBeFalsy(); - expect(bobDeviceTrust2.isTofu()).toBeFalsy(); - - // Alice verifies Bob's SSK - alice.uploadKeySignatures = async () => ({ failures: {} }); - await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); - - // Bob should be trusted but not his device - const bobTrust2 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust2.isVerified()).toBeTruthy(); - - const bobDeviceTrust3 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust3.isVerified()).toBeFalsy(); - - // Alice gets new signature for device - const sig2 = bobSigning2.sign(bobDeviceString); - bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; - alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: bobDevice, - }); - - // Bob's device should be trusted again (but not TOFU) - const bobTrust3 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust3.isVerified()).toBeTruthy(); - - const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); - expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy(); - alice.stopClient(); - }); - - it("should offer to upgrade device verifications to cross-signing", async function () { - let upgradeResolveFunc: () => void; - - const { client: alice } = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - shouldUpgradeDeviceVerifications: async (verifs) => { - expect(verifs.users["@bob:example.com"]).toBeDefined(); - upgradeResolveFunc(); - return ["@bob:example.com"]; - }, - }, - }, - ); - const { client: bob } = await makeTestClient({ userId: "@bob:example.com", deviceId: "Dynabook" }); - - bob.uploadDeviceSigningKeys = async () => ({}); - bob.uploadKeySignatures = async () => ({ failures: {} }); - // set Bob's cross-signing key - await resetCrossSigningKeys(bob); - alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: { - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!, - "ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!, - }, - verified: 1, - known: true, - }, - }); - alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", bob.crypto!.crossSigningInfo.toStorage()); - - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - // when alice sets up cross-signing, she should notice that bob's - // cross-signing key is signed by his Dynabook, which alice has - // verified, and ask if the device verification should be upgraded to a - // cross-signing verification - let upgradePromise = new Promise((resolve) => { - upgradeResolveFunc = resolve; - }); - await resetCrossSigningKeys(alice); - await upgradePromise; - - const bobTrust = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); - expect(bobTrust.isTofu()).toBeTruthy(); - - // "forget" that Bob is trusted - delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"].keys.master.signatures![ - "@alice:example.com" - ]; - - const bobTrust2 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); - expect(bobTrust2.isTofu()).toBeTruthy(); - - upgradePromise = new Promise((resolve) => { - upgradeResolveFunc = resolve; - }); - alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com"); - await new Promise((resolve) => { - alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve); - }); - await upgradePromise; - - const bobTrust3 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust3.isCrossSigningVerified()).toBeTruthy(); - expect(bobTrust3.isTofu()).toBeTruthy(); - alice.stopClient(); - bob.stopClient(); - }); - - it("should observe that our own device is cross-signed, even if this device doesn't trust the key", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - - // Generate Alice's SSK etc - const aliceMasterSigning = new globalThis.Olm.PkSigning(); - const aliceMasterPrivkey = aliceMasterSigning.generate_seed(); - const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey); - const aliceSigning = new globalThis.Olm.PkSigning(); - const alicePrivkey = aliceSigning.generate_seed(); - const alicePubkey = aliceSigning.init_with_seed(alicePrivkey); - const aliceSSK: CrossSigningKeyInfo = { - user_id: "@alice:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + alicePubkey]: alicePubkey, - }, - }; - const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK)); - aliceSSK.signatures = { - "@alice:example.com": { - ["ed25519:" + aliceMasterPubkey]: sskSig, - }, - }; - - // Alice's device downloads the keys, but doesn't trust them yet - alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: { - master: { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - ["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey, - }, - }, - self_signing: aliceSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - - // Alice has a second device that's cross-signed - const aliceDeviceId = "Dynabook"; - const aliceUnsignedDevice = { - user_id: "@alice:example.com", - device_id: aliceDeviceId, - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": "somePubkey", - "ed25519:Dynabook": "someOtherPubkey", - }, - }; - const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice)); - const aliceCrossSignedDevice: IDevice = { - ...aliceUnsignedDevice, - verified: 0, - known: false, - signatures: { - "@alice:example.com": { - ["ed25519:" + alicePubkey]: sig, - }, - }, - }; - alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { - [aliceDeviceId]: aliceCrossSignedDevice, - }); - - // We don't trust the cross-signing keys yet... - expect(alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified()).toBeFalsy(); - // ... but we do acknowledge that the device is signed by them - expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy(); - alice.stopClient(); - }); - - it("should observe that our own device isn't cross-signed", async function () { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - - // Generate Alice's SSK etc - const aliceMasterSigning = new globalThis.Olm.PkSigning(); - const aliceMasterPrivkey = aliceMasterSigning.generate_seed(); - const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey); - const aliceSigning = new globalThis.Olm.PkSigning(); - const alicePrivkey = aliceSigning.generate_seed(); - const alicePubkey = aliceSigning.init_with_seed(alicePrivkey); - const aliceSSK: CrossSigningKeyInfo = { - user_id: "@alice:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + alicePubkey]: alicePubkey, - }, - }; - const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK)); - aliceSSK.signatures = { - "@alice:example.com": { - ["ed25519:" + aliceMasterPubkey]: sskSig, - }, - }; - - // Alice's device downloads the keys - alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: { - master: { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - ["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey, - }, - }, - self_signing: aliceSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - - const deviceId = "Dynabook"; - const aliceNotCrossSignedDevice: IDevice = { - verified: 0, - known: false, - algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], - keys: { - "curve25519:Dynabook": "somePubkey", - "ed25519:Dynabook": "someOtherPubkey", - }, - }; - alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { - [deviceId]: aliceNotCrossSignedDevice, - }); - - expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy(); - alice.stopClient(); - }); - - it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - alice.uploadDeviceSigningKeys = async () => ({}); - alice.uploadKeySignatures = async () => ({ failures: {} }); - - // Generate Alice's SSK etc - const aliceMasterSigning = new globalThis.Olm.PkSigning(); - const aliceMasterPrivkey = aliceMasterSigning.generate_seed(); - const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey); - const aliceSigning = new globalThis.Olm.PkSigning(); - const alicePrivkey = aliceSigning.generate_seed(); - const alicePubkey = aliceSigning.init_with_seed(alicePrivkey); - const aliceSSK: CrossSigningKeyInfo = { - user_id: "@alice:example.com", - usage: ["self_signing"], - keys: { - ["ed25519:" + alicePubkey]: alicePubkey, - }, - }; - const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK)); - aliceSSK.signatures = { - "@alice:example.com": { - ["ed25519:" + aliceMasterPubkey]: sskSig, - }, - }; - - // Alice's device downloads the keys - alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: { - master: { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - ["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey, - }, - }, - self_signing: aliceSSK, - }, - firstUse: true, - crossSigningVerifiedBefore: false, - }); - - expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy(); - alice.stopClient(); - }); - - it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => { - const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy(); - alice.stopClient(); - }); -}); - -describe("userHasCrossSigningKeys", function () { - if (!globalThis.Olm) { - return; - } - - beforeAll(() => { - return globalThis.Olm.init(); - }); - - let aliceClient: MatrixClient; - let httpBackend: HttpBackend; - beforeEach(async () => { - const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - aliceClient = testClient.client; - httpBackend = testClient.httpBackend; - }); - - afterEach(() => { - aliceClient.stopClient(); - }); - - it("should download devices and return true if one is a cross-signing key", async () => { - httpBackend.when("POST", "/keys/query").respond(200, { - master_keys: { - "@alice:example.com": { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": - "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", - }, - }, - }, - }); - - let result: boolean; - await Promise.all([ - httpBackend.flush("/keys/query"), - aliceClient.userHasCrossSigningKeys().then((res) => { - result = res; - }), - ]); - expect(result!).toBeTruthy(); - }); - - it("should download devices and return false if there is no cross-signing key", async () => { - httpBackend.when("POST", "/keys/query").respond(200, {}); - - let result: boolean; - await Promise.all([ - httpBackend.flush("/keys/query"), - aliceClient.userHasCrossSigningKeys().then((res) => { - result = res; - }), - ]); - expect(result!).toBeFalsy(); - }); - - it("throws an error if crypto is disabled", () => { - aliceClient["cryptoBackend"] = undefined; - expect(() => aliceClient.userHasCrossSigningKeys()).toThrow("encryption disabled"); - }); -}); diff --git a/spec/unit/crypto/dehydration.spec.ts b/spec/unit/crypto/dehydration.spec.ts index d9a0dac895e..8df92e6314c 100644 --- a/spec/unit/crypto/dehydration.spec.ts +++ b/spec/unit/crypto/dehydration.spec.ts @@ -59,80 +59,4 @@ describe("Dehydration", () => { expect(alice.client.getDeviceId()).toEqual("ABCDEFG"); }); - - it("should dehydrate a device", async () => { - const key = new Uint8Array([1, 2, 3]); - const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, { - cryptoCallbacks: { - getDehydrationKey: async (t) => key, - }, - }); - - await alice.client.initLegacyCrypto(); - - alice.httpBackend.when("GET", "/room_keys/version").respond(404, { - errcode: "M_NOT_FOUND", - }); - - let pickledAccount = ""; - - alice.httpBackend - .when("PUT", "/dehydrated_device") - .check((req) => { - expect(req.data.device_data).toMatchObject({ - algorithm: DEHYDRATION_ALGORITHM, - account: expect.any(String), - }); - pickledAccount = req.data.device_data.account; - }) - .respond(200, { - device_id: "ABCDEFG", - }); - alice.httpBackend - .when("POST", "/keys/upload/ABCDEFG") - .check((req) => { - expect(req.data).toMatchObject({ - "device_keys": expect.objectContaining({ - algorithms: expect.any(Array), - device_id: "ABCDEFG", - user_id: "@alice:example.com", - keys: expect.objectContaining({ - "ed25519:ABCDEFG": expect.any(String), - "curve25519:ABCDEFG": expect.any(String), - }), - signatures: expect.objectContaining({ - "@alice:example.com": expect.objectContaining({ - "ed25519:ABCDEFG": expect.any(String), - }), - }), - }), - "one_time_keys": expect.any(Object), - "org.matrix.msc2732.fallback_keys": expect.any(Object), - }); - }) - .respond(200, {}); - - try { - const deviceId = ( - await Promise.all([ - alice.client.createDehydratedDevice(new Uint8Array(key), {}), - alice.httpBackend.flushAllExpected(), - ]) - )[0]; - - expect(deviceId).toEqual("ABCDEFG"); - expect(deviceId).not.toEqual(""); - - // try to rehydrate the dehydrated device - const rehydrated = new Olm.Account(); - try { - rehydrated.unpickle(new Uint8Array(key), pickledAccount); - } finally { - rehydrated.free(); - } - } finally { - alice.client?.crypto?.dehydrationManager?.stop(); - alice.client?.crypto?.deviceList.stop(); - } - }); }); diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts deleted file mode 100644 index 097ee2b1b19..00000000000 --- a/spec/unit/crypto/secrets.spec.ts +++ /dev/null @@ -1,697 +0,0 @@ -/* -Copyright 2019, 2022 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 "../../olm-loader"; -import * as olmlib from "../../../src/crypto/olmlib"; -import { IObject } from "../../../src/crypto/olmlib"; -import { MatrixEvent } from "../../../src/models/event"; -import { TestClient } from "../../TestClient"; -import { makeTestClients } from "./verification/util"; -import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; -import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; -import { logger } from "../../../src/logger"; -import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client"; -import { DeviceInfo } from "../../../src/crypto/deviceinfo"; -import { ISignatures } from "../../../src/@types/signed"; -import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; -import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; -import { decodeBase64 } from "../../../src/base64"; -import { CrossSigningKeyInfo } from "../../../src/crypto-api"; -import { SecretInfo } from "../../../src/secret-storage.ts"; - -async function makeTestClient( - userInfo: { userId: string; deviceId: string }, - options: Partial = {}, -) { - const client = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options).client; - - // Make it seem as if we've synced and thus the store can be trusted to - // contain valid account data. - client.isInitialSyncComplete = function () { - return true; - }; - - await client.initLegacyCrypto(); - - // No need to download keys for these tests - jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map()); - - return client; -} - -// Wrapper around pkSign to return a signed object. pkSign returns the -// signature, rather than the signed object. -function sign( - obj: T, - key: Uint8Array, - userId: string, -): T & { - signatures: ISignatures; - unsigned?: object; -} { - olmlib.pkSign(obj, key, userId, ""); - return obj as T & { - signatures: ISignatures; - unsigned?: object; - }; -} - -declare module "../../../src/@types/event" { - interface SecretStorageAccountDataEvents { - foo: SecretInfo; - } -} - -describe("Secrets", function () { - if (!globalThis.Olm) { - logger.warn("Not running megolm backup unit tests: libolm not present"); - return; - } - - beforeAll(function () { - return globalThis.Olm.init(); - }); - - it("should store and retrieve a secret", async function () { - const key = new Uint8Array(16); - for (let i = 0; i < 16; i++) key[i] = i; - - const signing = new globalThis.Olm.PkSigning(); - const signingKey = signing.generate_seed(); - const signingPubKey = signing.init_with_seed(signingKey); - - const signingkeyInfo = { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - ["ed25519:" + signingPubKey]: signingPubKey, - }, - }; - - const getKey = jest.fn().mockImplementation(async (e) => { - expect(Object.keys(e.keys)).toEqual(["abc"]); - return ["abc", key]; - }); - - const alice = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - getCrossSigningKey: async (t) => signingKey, - getSecretStorageKey: getKey, - }, - }, - ); - alice.crypto!.crossSigningInfo.setKeys({ - master: signingkeyInfo, - }); - - const secretStorage = alice.crypto!.secretStorage; - - jest.spyOn(alice, "setAccountData").mockImplementation(async function (eventType, contents) { - alice.store.storeAccountDataEvents([ - new MatrixEvent({ - type: eventType, - content: contents, - }), - ]); - return {}; - }); - - const keyAccountData = { - algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, - }; - await alice.crypto!.crossSigningInfo.signObject(keyAccountData, "master"); - - alice.store.storeAccountDataEvents([ - new MatrixEvent({ - type: "m.secret_storage.key.abc", - content: keyAccountData, - }), - ]); - - expect(await secretStorage.isStored("foo")).toBeFalsy(); - - await secretStorage.store("foo", "bar", ["abc"]); - - expect(await secretStorage.isStored("foo")).toBeTruthy(); - expect(await secretStorage.get("foo")).toBe("bar"); - - expect(getKey).toHaveBeenCalled(); - alice.stopClient(); - }); - - it("should throw if given a key that doesn't exist", async function () { - const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - - await expect(alice.storeSecret("foo", "bar", ["this secret does not exist"])).rejects.toBeTruthy(); - alice.stopClient(); - }); - - it("should refuse to encrypt with zero keys", async function () { - const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - - await expect(alice.storeSecret("foo", "bar", [])).rejects.toBeTruthy(); - alice.stopClient(); - }); - - it("should encrypt with default key if keys is null", async function () { - const key = new Uint8Array(16); - for (let i = 0; i < 16; i++) key[i] = i; - const getKey = jest.fn().mockImplementation(async (e) => { - expect(Object.keys(e.keys)).toEqual([newKeyId]); - return [newKeyId, key]; - }); - - let keys: Record = {}; - const alice = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - getCrossSigningKey: (t) => Promise.resolve(keys[t]), - saveCrossSigningKeys: (k) => (keys = k), - getSecretStorageKey: getKey, - }, - }, - ); - alice.setAccountData = async function (eventType, contents) { - alice.store.storeAccountDataEvents([ - new MatrixEvent({ - type: eventType, - content: contents, - }), - ]); - return {}; - }; - resetCrossSigningKeys(alice); - - const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, { key }); - // we don't await on this because it waits for the event to come down the sync - // which won't happen in the test setup - alice.setDefaultSecretStorageKeyId(newKeyId); - await alice.storeSecret("foo", "bar"); - - const accountData = alice.getAccountData("foo"); - expect(accountData!.getContent().encrypted).toBeTruthy(); - alice.stopClient(); - }); - - it("should refuse to encrypt if no keys given and no default key", async function () { - const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - - await expect(alice.storeSecret("foo", "bar")).rejects.toBeTruthy(); - alice.stopClient(); - }); - - it("should request secrets from other clients", async function () { - const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients( - [ - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { userId: "@alice:example.com", deviceId: "VAX" }, - ], - { - cryptoCallbacks: { - onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => { - expect(secretName).toBe("foo"); - return Promise.resolve("bar"); - }, - }, - }, - ); - - const vaxDevice = vax.client.crypto!.olmDevice; - const osborne2Device = osborne2.client.crypto!.olmDevice; - const secretStorage = osborne2.client.crypto!.secretStorage; - - osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { - VAX: { - known: false, - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:VAX": vaxDevice.deviceEd25519Key!, - "curve25519:VAX": vaxDevice.deviceCurve25519Key!, - }, - verified: DeviceInfo.DeviceVerification.VERIFIED, - }, - }); - vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { - Osborne2: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - verified: 0, - known: false, - keys: { - "ed25519:Osborne2": osborne2Device.deviceEd25519Key!, - "curve25519:Osborne2": osborne2Device.deviceCurve25519Key!, - }, - }, - }); - - await osborne2Device.generateOneTimeKeys(1); - const otks = (await osborne2Device.getOneTimeKeys()).curve25519; - await osborne2Device.markKeysAsPublished(); - - await vax.client.crypto!.olmDevice.createOutboundSession( - osborne2Device.deviceCurve25519Key!, - Object.values(otks)[0], - ); - - osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map()); - osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - - const request = await secretStorage.request("foo", ["VAX"]); - await request.promise; // return value not used - - osborne2.stop(); - vax.stop(); - clearTestClientTimeouts(); - }); - - describe("bootstrap", function () { - // keys used in some of the tests - const XSK = new Uint8Array(decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q=")); - const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0"; - const USK = new Uint8Array(decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU=")); - const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU"; - const SSK = new Uint8Array(decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M=")); - const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q"; - const SSSSKey = new Uint8Array(decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=")); - - it("bootstraps when no storage or cross-signing keys locally", async function () { - const key = new Uint8Array(16); - for (let i = 0; i < 16; i++) key[i] = i; - const getKey = jest.fn().mockImplementation(async (e) => { - return [Object.keys(e.keys)[0], key]; - }); - - const bob = await makeTestClient( - { - userId: "@bob:example.com", - deviceId: "bob1", - }, - { - cryptoCallbacks: { - getSecretStorageKey: getKey, - }, - }, - ); - bob.uploadDeviceSigningKeys = async () => ({}); - bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined); - bob.setAccountData = async function (eventType, contents) { - const event = new MatrixEvent({ - type: eventType, - content: contents, - }); - this.store.storeAccountDataEvents([event]); - this.emit(ClientEvent.AccountData, event); - return {}; - }; - bob.getKeyBackupVersion = jest.fn().mockResolvedValue(null); - - await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({}); - }, - }); - await bob.bootstrapSecretStorage({ - createSecretStorageKey, - }); - - const crossSigning = bob.crypto!.crossSigningInfo; - const secretStorage = bob.crypto!.secretStorage; - - expect(crossSigning.getId()).toBeTruthy(); - expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy(); - expect(await secretStorage.hasKey()).toBeTruthy(); - bob.stopClient(); - }); - - it("bootstraps when cross-signing keys in secret storage", async function () { - const decryption = new globalThis.Olm.PkDecryption(); - const storagePrivateKey = decryption.get_private_key(); - - const bob: MatrixClient = await makeTestClient( - { - userId: "@bob:example.com", - deviceId: "bob1", - }, - { - cryptoCallbacks: { - getSecretStorageKey: async (request) => { - const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); - expect(Object.keys(request.keys)).toEqual([defaultKeyId]); - return [defaultKeyId!, storagePrivateKey]; - }, - }, - }, - ); - - bob.uploadDeviceSigningKeys = async () => ({}); - bob.uploadKeySignatures = async () => ({ failures: {} }); - bob.setAccountData = async function (eventType, contents) { - const event = new MatrixEvent({ - type: eventType, - content: contents, - }); - this.store.storeAccountDataEvents([event]); - this.emit(ClientEvent.AccountData, event); - return {}; - }; - bob.crypto!.backupManager.checkKeyBackup = async () => null; - - const crossSigning = bob.crypto!.crossSigningInfo; - const secretStorage = bob.crypto!.secretStorage; - - // Set up cross-signing keys from scratch with specific storage key - await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({}); - }, - }); - await bob.bootstrapSecretStorage({ - createSecretStorageKey: async () => ({ - privateKey: storagePrivateKey, - }), - }); - - // Clear local cross-signing keys and read from secret storage - bob.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", crossSigning.toStorage()); - crossSigning.keys = {}; - await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({}); - }, - }); - - expect(crossSigning.getId()).toBeTruthy(); - expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy(); - expect(await secretStorage.hasKey()).toBeTruthy(); - bob.stopClient(); - }); - - it("adds passphrase checking if it's lacking", async function () { - let crossSigningKeys: Record = { - master: XSK, - user_signing: USK, - self_signing: SSK, - }; - const secretStorageKeys: Record = { - key_id: SSSSKey, - }; - const alice = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - getCrossSigningKey: async (t) => crossSigningKeys[t], - saveCrossSigningKeys: (k) => (crossSigningKeys = k), - getSecretStorageKey: async ({ keys }, name) => { - for (const keyId of Object.keys(keys)) { - if (secretStorageKeys[keyId]) { - return [keyId, secretStorageKeys[keyId]]; - } - } - return null; - }, - }, - }, - ); - alice.store.storeAccountDataEvents([ - new MatrixEvent({ - type: "m.secret_storage.default_key", - content: { - key: "key_id", - }, - }), - new MatrixEvent({ - type: "m.secret_storage.key.key_id", - content: { - algorithm: "m.secret_storage.v1.aes-hmac-sha2", - passphrase: { - algorithm: "m.pbkdf2", - iterations: 500000, - salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", - }, - }, - }), - // we never use these values, other than checking that they - // exist, so just use dummy values - new MatrixEvent({ - type: "m.cross_signing.master", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - new MatrixEvent({ - type: "m.cross_signing.self_signing", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - new MatrixEvent({ - type: "m.cross_signing.user_signing", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - ]); - alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - firstUse: false, - crossSigningVerifiedBefore: false, - keys: { - master: { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - [`ed25519:${XSPubKey}`]: XSPubKey, - }, - }, - self_signing: sign( - { - user_id: "@alice:example.com", - usage: ["self_signing"], - keys: { - [`ed25519:${SSPubKey}`]: SSPubKey, - }, - }, - XSK, - "@alice:example.com", - ), - user_signing: sign( - { - user_id: "@alice:example.com", - usage: ["user_signing"], - keys: { - [`ed25519:${USPubKey}`]: USPubKey, - }, - }, - XSK, - "@alice:example.com", - ), - }, - }); - alice.getKeyBackupVersion = async () => { - return { - version: "1", - algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - auth_data: sign( - { - public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", - }, - XSK, - "@alice:example.com", - ), - }; - }; - alice.setAccountData = async function (name, data) { - const event = new MatrixEvent({ - type: name, - content: data, - }); - alice.store.storeAccountDataEvents([event]); - this.emit(ClientEvent.AccountData, event); - return {}; - }; - - await alice.bootstrapSecretStorage({}); - - expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" }); - const keyInfo = alice - .getAccountData("m.secret_storage.key.key_id")! - .getContent(); - expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2"); - expect(keyInfo.passphrase).toEqual({ - algorithm: "m.pbkdf2", - iterations: 500000, - salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", - }); - expect(keyInfo).toHaveProperty("iv"); - expect(keyInfo).toHaveProperty("mac"); - expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)).toBeTruthy(); - alice.stopClient(); - }); - it("fixes backup keys in the wrong format", async function () { - let crossSigningKeys: Record = { - master: XSK, - user_signing: USK, - self_signing: SSK, - }; - const secretStorageKeys: Record = { - key_id: SSSSKey, - }; - const alice = await makeTestClient( - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { - cryptoCallbacks: { - getCrossSigningKey: async (t) => crossSigningKeys[t], - saveCrossSigningKeys: (k) => (crossSigningKeys = k), - getSecretStorageKey: async ({ keys }, name) => { - for (const keyId of Object.keys(keys)) { - if (secretStorageKeys[keyId]) { - return [keyId, secretStorageKeys[keyId]]; - } - } - return null; - }, - }, - }, - ); - alice.store.storeAccountDataEvents([ - new MatrixEvent({ - type: "m.secret_storage.default_key", - content: { - key: "key_id", - }, - }), - new MatrixEvent({ - type: "m.secret_storage.key.key_id", - content: { - algorithm: "m.secret_storage.v1.aes-hmac-sha2", - passphrase: { - algorithm: "m.pbkdf2", - iterations: 500000, - salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK", - }, - }, - }), - new MatrixEvent({ - type: "m.cross_signing.master", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - new MatrixEvent({ - type: "m.cross_signing.self_signing", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - new MatrixEvent({ - type: "m.cross_signing.user_signing", - content: { - encrypted: { - key_id: { ciphertext: "bla", mac: "bla", iv: "bla" }, - }, - }, - }), - new MatrixEvent({ - type: "m.megolm_backup.v1", - content: { - encrypted: { - key_id: await encryptAESSecretStorageItem( - "123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90", - secretStorageKeys.key_id, - "m.megolm_backup.v1", - ), - }, - }, - }), - ]); - alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - firstUse: false, - crossSigningVerifiedBefore: false, - keys: { - master: { - user_id: "@alice:example.com", - usage: ["master"], - keys: { - [`ed25519:${XSPubKey}`]: XSPubKey, - }, - }, - self_signing: sign( - { - user_id: "@alice:example.com", - usage: ["self_signing"], - keys: { - [`ed25519:${SSPubKey}`]: SSPubKey, - }, - }, - XSK, - "@alice:example.com", - ), - user_signing: sign( - { - user_id: "@alice:example.com", - usage: ["user_signing"], - keys: { - [`ed25519:${USPubKey}`]: USPubKey, - }, - }, - XSK, - "@alice:example.com", - ), - }, - }); - alice.getKeyBackupVersion = async () => { - return { - version: "1", - algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", - auth_data: sign( - { - public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A", - }, - XSK, - "@alice:example.com", - ), - }; - }; - alice.setAccountData = async function (name, data) { - const event = new MatrixEvent({ - type: name, - content: data, - }); - alice.store.storeAccountDataEvents([event]); - this.emit(ClientEvent.AccountData, event); - return {}; - }; - - await alice.bootstrapSecretStorage({}); - - const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent(); - expect(backupKey.encrypted).toHaveProperty("key_id"); - expect(await alice.getSecret("m.megolm_backup.v1")).toEqual("ey0GB1kB6jhOWgwiBUMIWg=="); - alice.stopClient(); - }); - }); -}); diff --git a/spec/unit/crypto/verification/request.spec.ts b/spec/unit/crypto/verification/request.spec.ts deleted file mode 100644 index c3b45b7b813..00000000000 --- a/spec/unit/crypto/verification/request.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 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 "../../../olm-loader"; -import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; -import { logger } from "../../../../src/logger"; -import { SAS } from "../../../../src/crypto/verification/SAS"; -import { makeTestClients } from "./util"; - -const Olm = globalThis.Olm; - -jest.useFakeTimers(); - -describe("verification request integration tests with crypto layer", function () { - if (!globalThis.Olm) { - logger.warn("Not running device verification unit tests: libolm not present"); - return; - } - - beforeAll(function () { - return Olm.init(); - }); - - it("should request and accept a verification", async function () { - const [[alice, bob], clearTestClientTimeouts] = await makeTestClients( - [ - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { userId: "@bob:example.com", deviceId: "Dynabook" }, - ], - { - verificationMethods: [verificationMethods.SAS], - }, - ); - alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function () { - return { - Dynabook: { - algorithms: [], - verified: 0, - known: false, - keys: { - "ed25519:Dynabook": "bob+base64+ed25519+key", - }, - }, - }; - }; - alice.client.downloadKeys = jest.fn().mockResolvedValue({}); - bob.client.downloadKeys = jest.fn().mockResolvedValue({}); - bob.client.on(CryptoEvent.VerificationRequest, (request) => { - const bobVerifier = request.beginKeyVerification(verificationMethods.SAS); - bobVerifier.verify(); - - // @ts-ignore Private function access (but it's a test, so we're okay) - bobVerifier.endTimer(); - }); - const aliceRequest = await alice.client.requestVerification("@bob:example.com"); - await aliceRequest.waitFor((r) => r.started); - const aliceVerifier = aliceRequest.verifier; - expect(aliceVerifier).toBeInstanceOf(SAS); - - // @ts-ignore Private function access (but it's a test, so we're okay) - aliceVerifier.endTimer(); - - alice.stop(); - bob.stop(); - clearTestClientTimeouts(); - }); -}); diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index 939dc3b7789..ec7b67f85f4 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -15,25 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ import "../../../olm-loader"; -import { makeTestClients } from "./util"; import { MatrixEvent } from "../../../../src/models/event"; -import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; -import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo"; -import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; -import * as olmlib from "../../../../src/crypto/olmlib"; +import { SAS } from "../../../../src/crypto/verification/SAS"; import { logger } from "../../../../src/logger"; -import { resetCrossSigningKeys } from "../crypto-utils"; -import { VerificationBase } from "../../../../src/crypto/verification/Base"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { MatrixClient } from "../../../../src"; import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest"; -import { TestClient } from "../../../TestClient"; - const Olm = globalThis.Olm; -let ALICE_DEVICES: Record; -let BOB_DEVICES: Record; - describe("SAS verification", function () { if (!globalThis.Olm) { logger.warn("Not running device verification unit tests: libolm not present"); @@ -71,511 +60,4 @@ describe("SAS verification", function () { // Cancel the SAS for cleanup (we started a verification, so abort) sas.cancel(new Error("error")); }); - - describe("verification", () => { - let alice: TestClient; - let bob: TestClient; - let aliceSasEvent: ISasEvent | null; - let bobSasEvent: ISasEvent | null; - let aliceVerifier: SAS; - let bobPromise: Promise>; - let clearTestClientTimeouts: () => void; - - beforeEach(async () => { - [[alice, bob], clearTestClientTimeouts] = await makeTestClients( - [ - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { userId: "@bob:example.com", deviceId: "Dynabook" }, - ], - { - verificationMethods: [verificationMethods.SAS], - }, - ); - - const aliceDevice = alice.client.crypto!.olmDevice; - const bobDevice = bob.client.crypto!.olmDevice; - - ALICE_DEVICES = { - Osborne2: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Osborne2": aliceDevice.deviceEd25519Key!, - "curve25519:Osborne2": aliceDevice.deviceCurve25519Key!, - }, - verified: DeviceInfo.DeviceVerification.UNVERIFIED, - known: false, - }, - }; - - BOB_DEVICES = { - Dynabook: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Dynabook": bobDevice.deviceEd25519Key!, - "curve25519:Dynabook": bobDevice.deviceCurve25519Key!, - }, - verified: DeviceInfo.DeviceVerification.UNVERIFIED, - known: false, - }, - }; - - alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); - alice.client.downloadKeys = () => { - return Promise.resolve(new Map()); - }; - - bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES); - bob.client.downloadKeys = () => { - return Promise.resolve(new Map()); - }; - - aliceSasEvent = null; - bobSasEvent = null; - - bobPromise = new Promise>((resolve, reject) => { - bob.client.on(CryptoEvent.VerificationRequest, (request) => { - (request.verifier!).on(SasEvent.ShowSas, (e) => { - if (!e.sas.emoji || !e.sas.decimal) { - e.cancel(); - } else if (!aliceSasEvent) { - bobSasEvent = e; - } else { - try { - expect(e.sas).toEqual(aliceSasEvent.sas); - e.confirm(); - aliceSasEvent.confirm(); - } catch { - e.mismatch(); - aliceSasEvent.mismatch(); - } - } - }); - resolve(request.verifier!); - }); - }); - - aliceVerifier = alice.client.beginKeyVerification( - verificationMethods.SAS, - bob.client.getUserId()!, - bob.deviceId!, - ) as SAS; - aliceVerifier.on(SasEvent.ShowSas, (e) => { - if (!e.sas.emoji || !e.sas.decimal) { - e.cancel(); - } else if (!bobSasEvent) { - aliceSasEvent = e; - } else { - try { - expect(e.sas).toEqual(bobSasEvent.sas); - e.confirm(); - bobSasEvent.confirm(); - } catch { - e.mismatch(); - bobSasEvent.mismatch(); - } - } - }); - }); - - afterEach(async () => { - await Promise.all([alice.stop(), bob.stop()]); - - clearTestClientTimeouts(); - }); - - it("should verify a key", async () => { - let macMethod; - let keyAgreement; - const origSendToDevice = bob.client.sendToDevice.bind(bob.client); - bob.client.sendToDevice = async (type, map) => { - if (type === "m.key.verification.accept") { - macMethod = map - .get(alice.client.getUserId()!) - ?.get(alice.client.deviceId!)?.message_authentication_code; - keyAgreement = map - .get(alice.client.getUserId()!) - ?.get(alice.client.deviceId!)?.key_agreement_protocol; - } - return origSendToDevice(type, map); - }; - - alice.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@bob:example.com": BOB_DEVICES, - }, - }); - bob.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@alice:example.com": ALICE_DEVICES, - }, - }); - - await Promise.all([ - aliceVerifier.verify(), - bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(undefined), - bob.httpBackend.flush(undefined), - ]); - - // make sure that it uses the preferred method - expect(macMethod).toBe("hkdf-hmac-sha256.v2"); - expect(keyAgreement).toBe("curve25519-hkdf-sha256"); - - // make sure Alice and Bob verified each other - const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice?.isVerified()).toBeTruthy(); - const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice?.isVerified()).toBeTruthy(); - }); - - it("should be able to verify using the old base64", async () => { - // pretend that Alice can only understand the old (incorrect) base64 - // encoding, and make sure that she can still verify with Bob - let macMethod; - const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client); - alice.client.sendToDevice = (type, map) => { - if (type === "m.key.verification.start") { - // Note: this modifies not only the message that Bob - // receives, but also the copy of the message that Alice - // has, since it is the same object. If this does not - // happen, the verification will fail due to a hash - // commitment mismatch. - map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [ - "hkdf-hmac-sha256", - ]; - } - return aliceOrigSendToDevice(type, map); - }; - const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); - bob.client.sendToDevice = (type, map) => { - if (type === "m.key.verification.accept") { - macMethod = map - .get(alice.client.getUserId()!)! - .get(alice.client.deviceId!)!.message_authentication_code; - } - return bobOrigSendToDevice(type, map); - }; - - alice.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@bob:example.com": BOB_DEVICES, - }, - }); - bob.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@alice:example.com": ALICE_DEVICES, - }, - }); - - await Promise.all([ - aliceVerifier.verify(), - bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(undefined), - bob.httpBackend.flush(undefined), - ]); - - expect(macMethod).toBe("hkdf-hmac-sha256"); - - const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice!.isVerified()).toBeTruthy(); - const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice!.isVerified()).toBeTruthy(); - }); - - it("should be able to verify using the old MAC", async () => { - // pretend that Alice can only understand the old (incorrect) MAC, - // and make sure that she can still verify with Bob - let macMethod; - const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client); - alice.client.sendToDevice = (type, map) => { - if (type === "m.key.verification.start") { - // Note: this modifies not only the message that Bob - // receives, but also the copy of the message that Alice - // has, since it is the same object. If this does not - // happen, the verification will fail due to a hash - // commitment mismatch. - map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [ - "hmac-sha256", - ]; - } - return aliceOrigSendToDevice(type, map); - }; - const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); - bob.client.sendToDevice = (type, map) => { - if (type === "m.key.verification.accept") { - macMethod = map - .get(alice.client.getUserId()!)! - .get(alice.client.deviceId!)!.message_authentication_code; - } - return bobOrigSendToDevice(type, map); - }; - - alice.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@bob:example.com": BOB_DEVICES, - }, - }); - bob.httpBackend.when("POST", "/keys/query").respond(200, { - failures: {}, - device_keys: { - "@alice:example.com": ALICE_DEVICES, - }, - }); - - await Promise.all([ - aliceVerifier.verify(), - bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(undefined), - bob.httpBackend.flush(undefined), - ]); - - expect(macMethod).toBe("hmac-sha256"); - - const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice?.isVerified()).toBeTruthy(); - const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice?.isVerified()).toBeTruthy(); - }); - - it("should verify a cross-signing key", async () => { - alice.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {}); - alice.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {}); - alice.httpBackend.flush(undefined, 2); - await resetCrossSigningKeys(alice.client); - bob.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {}); - bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {}); - bob.httpBackend.flush(undefined, 2); - - await resetCrossSigningKeys(bob.client); - - bob.client.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: alice.client.crypto!.crossSigningInfo.keys, - crossSigningVerifiedBefore: false, - firstUse: true, - }); - - const verifyProm = Promise.all([ - aliceVerifier.verify(), - bobPromise.then((verifier) => { - bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {}); - bob.httpBackend.flush(undefined, 1, 2000); - return verifier.verify(); - }), - ]); - - await verifyProm; - - const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook"); - 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); - }); - }); - - it("should send a cancellation message on error", async function () { - const [[alice, bob], clearTestClientTimeouts] = await makeTestClients( - [ - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { userId: "@bob:example.com", deviceId: "Dynabook" }, - ], - { - verificationMethods: [verificationMethods.SAS], - }, - ); - alice.client.setDeviceVerified = jest.fn(); - alice.client.downloadKeys = jest.fn().mockResolvedValue({}); - bob.client.setDeviceVerified = jest.fn(); - bob.client.downloadKeys = jest.fn().mockResolvedValue({}); - - const bobPromise = new Promise>((resolve, reject) => { - bob.client.on(CryptoEvent.VerificationRequest, (request) => { - (request.verifier!).on(SasEvent.ShowSas, (e) => { - e.mismatch(); - }); - resolve(request.verifier!); - }); - }); - - const aliceVerifier = alice.client.beginKeyVerification( - verificationMethods.SAS, - bob.client.getUserId()!, - bob.client.deviceId!, - ); - - const aliceSpy = jest.fn(); - const bobSpy = jest.fn(); - await Promise.all([ - aliceVerifier.verify().catch(aliceSpy), - bobPromise.then((verifier) => verifier.verify()).catch(bobSpy), - ]); - expect(aliceSpy).toHaveBeenCalled(); - expect(bobSpy).toHaveBeenCalled(); - expect(alice.client.setDeviceVerified).not.toHaveBeenCalled(); - expect(bob.client.setDeviceVerified).not.toHaveBeenCalled(); - - alice.stop(); - bob.stop(); - clearTestClientTimeouts(); - }); - - describe("verification in DM", function () { - let alice: TestClient; - let bob: TestClient; - let aliceSasEvent: ISasEvent | null; - let bobSasEvent: ISasEvent | null; - let aliceVerifier: SAS; - let bobPromise: Promise; - let clearTestClientTimeouts: () => void; - - beforeEach(async function () { - [[alice, bob], clearTestClientTimeouts] = await makeTestClients( - [ - { userId: "@alice:example.com", deviceId: "Osborne2" }, - { userId: "@bob:example.com", deviceId: "Dynabook" }, - ], - { - verificationMethods: [verificationMethods.SAS], - }, - ); - - alice.client.crypto!.setDeviceVerification = jest.fn(); - alice.client.getDeviceEd25519Key = () => { - return "alice+base64+ed25519+key"; - }; - alice.client.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Dynabook": "bob+base64+ed25519+key", - }, - }, - "Dynabook", - ); - }; - alice.client.downloadKeys = () => { - return Promise.resolve(new Map()); - }; - - bob.client.crypto!.setDeviceVerification = jest.fn(); - bob.client.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Osborne2": "alice+base64+ed25519+key", - }, - }, - "Osborne2", - ); - }; - bob.client.getDeviceEd25519Key = () => { - return "bob+base64+ed25519+key"; - }; - bob.client.downloadKeys = () => { - return Promise.resolve(new Map()); - }; - - aliceSasEvent = null; - bobSasEvent = null; - - bobPromise = new Promise((resolve, reject) => { - bob.client.on(CryptoEvent.VerificationRequest, async (request) => { - const verifier = request.beginKeyVerification(SAS.NAME) as SAS; - verifier.on(SasEvent.ShowSas, (e) => { - if (!e.sas.emoji || !e.sas.decimal) { - e.cancel(); - } else if (!aliceSasEvent) { - bobSasEvent = e; - } else { - try { - expect(e.sas).toEqual(aliceSasEvent.sas); - e.confirm(); - aliceSasEvent.confirm(); - } catch { - e.mismatch(); - aliceSasEvent.mismatch(); - } - } - }); - await verifier.verify(); - resolve(); - }); - }); - - const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id"); - await aliceRequest.waitFor((r) => r.started); - aliceVerifier = aliceRequest.verifier! as SAS; - aliceVerifier.on(SasEvent.ShowSas, (e) => { - if (!e.sas.emoji || !e.sas.decimal) { - e.cancel(); - } else if (!bobSasEvent) { - aliceSasEvent = e; - } else { - try { - expect(e.sas).toEqual(bobSasEvent.sas); - e.confirm(); - bobSasEvent.confirm(); - } catch { - e.mismatch(); - bobSasEvent.mismatch(); - } - } - }); - }); - afterEach(async function () { - await Promise.all([alice.stop(), bob.stop()]); - - clearTestClientTimeouts(); - }); - - it("should verify a key", async function () { - await Promise.all([aliceVerifier.verify(), bobPromise]); - - // make sure Alice and Bob verified each other - expect(alice.client.crypto!.setDeviceVerification).toHaveBeenCalledWith( - bob.client.getUserId(), - bob.client.deviceId, - true, - null, - null, - { "ed25519:Dynabook": "bob+base64+ed25519+key" }, - ); - expect(bob.client.crypto!.setDeviceVerification).toHaveBeenCalledWith( - alice.client.getUserId(), - alice.client.deviceId, - true, - null, - null, - { "ed25519:Osborne2": "alice+base64+ed25519+key" }, - ); - }); - }); }); diff --git a/spec/unit/crypto/verification/util.ts b/spec/unit/crypto/verification/util.ts deleted file mode 100644 index 16a18559870..00000000000 --- a/spec/unit/crypto/verification/util.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 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 { TestClient } from "../../../TestClient"; -import { IContent, MatrixEvent } from "../../../../src/models/event"; -import { IRoomTimelineData } from "../../../../src/models/event-timeline-set"; -import { Room, RoomEvent } from "../../../../src/models/room"; -import { logger } from "../../../../src/logger"; -import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client"; - -interface UserInfo { - userId: string; - deviceId: string; -} - -export async function makeTestClients( - userInfos: UserInfo[], - options: Partial, -): Promise<[TestClient[], () => void]> { - const clients: TestClient[] = []; - const timeouts: ReturnType[] = []; - const clientMap: Record> = {}; - const makeSendToDevice = - (matrixClient: MatrixClient): MatrixClient["sendToDevice"] => - async (type: string, contentMap: SendToDeviceContentMap) => { - // logger.log(this.getUserId(), "sends", type, map); - for (const [userId, deviceMessages] of contentMap) { - if (userId in clientMap) { - for (const [deviceId, message] of deviceMessages) { - if (deviceId in clientMap[userId]) { - const event = new MatrixEvent({ - sender: matrixClient.getUserId()!, - type: type, - content: message, - }); - const client = clientMap[userId][deviceId]; - const decryptionPromise = event.isEncrypted() - ? event.attemptDecryption(client.crypto!) - : Promise.resolve(); - - decryptionPromise.then(() => client.emit(ClientEvent.ToDeviceEvent, event)); - } - } - } - } - return {}; - }; - const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => { - // make up a unique ID as the event ID - const eventId = "$" + matrixClient.makeTxnId(); - const rawEvent = { - sender: matrixClient.getUserId()!, - type: type, - content: content, - room_id: room, - event_id: eventId, - origin_server_ts: Date.now(), - }; - const event = new MatrixEvent(rawEvent); - const remoteEcho = new MatrixEvent( - Object.assign({}, rawEvent, { - unsigned: { - transaction_id: matrixClient.makeTxnId(), - }, - }), - ); - - const timeout = setTimeout(() => { - for (const tc of clients) { - const room = new Room("test", tc.client, tc.client.getUserId()!); - const roomTimelineData = {} as unknown as IRoomTimelineData; - if (tc.client === matrixClient) { - logger.log("sending remote echo!!"); - tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData); - } else { - tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData); - } - } - }); - - timeouts.push(timeout as unknown as ReturnType); - - return Promise.resolve({ event_id: eventId }); - }; - - for (const userInfo of userInfos) { - let keys: Record = {}; - if (!options) options = {}; - if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; - if (!options.cryptoCallbacks.saveCrossSigningKeys) { - options.cryptoCallbacks.saveCrossSigningKeys = (k) => { - keys = k; - }; - // @ts-ignore tsc getting confused by overloads - options.cryptoCallbacks.getCrossSigningKey = (typ) => keys[typ]; - } - const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options); - if (!(userInfo.userId in clientMap)) { - clientMap[userInfo.userId] = {}; - } - clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; - testClient.client.sendToDevice = makeSendToDevice(testClient.client); - // @ts-ignore tsc getting confused by overloads - testClient.client.sendEvent = makeSendEvent(testClient.client); - clients.push(testClient); - } - - await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto())); - - const destroy = () => { - timeouts.forEach((t) => clearTimeout(t)); - }; - - return [clients, destroy]; -}