From a45de2f13244b95da89f2c7aea9741db1c53efc6 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 28 Nov 2024 14:10:41 +0100 Subject: [PATCH 01/14] Remove deprecated calls in `webrtc/call.ts` --- src/webrtc/call.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index c2b24cadb82..29919137657 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -631,19 +631,17 @@ export class MatrixCall extends TypedEventEmitter Date: Fri, 6 Dec 2024 09:54:10 +0100 Subject: [PATCH 02/14] Throw error when legacy call was used --- src/webrtc/call.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 29919137657..99bce1e0074 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2514,7 +2514,9 @@ export class MatrixCall extends TypedEventEmitter Date: Mon, 27 Jan 2025 10:34:12 +0100 Subject: [PATCH 03/14] Remove `MatrixClient.initLegacyCrypto` (#4620) * Remove `MatrixClient.initLegacyCrypto` * Remove `MatrixClient.initLegacyCrypto` in README.md * Remove tests using `MatrixClient.initLegacyCrypto` --- README.md | 2 - 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/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 -- src/client.ts | 89 +- 16 files changed, 9 insertions(+), 6895 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/README.md b/README.md index e3bf79204a0..274a7f9deac 100644 --- a/README.md +++ b/README.md @@ -307,8 +307,6 @@ Then visit `http://localhost:8005` to see the API docs. ## Initialization -**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.** - To initialize the end-to-end encryption support in the matrix client: ```javascript 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/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]; -} diff --git a/src/client.ts b/src/client.ts index 69c9b9d0bd0..30c8a628125 100644 --- a/src/client.ts +++ b/src/client.ts @@ -297,7 +297,7 @@ export interface ICreateClientOpts { * specified, uses a default implementation (indexeddb in the browser, * in-memory otherwise). * - * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}), + * This is only used for the legacy crypto implementation, * but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device * previously used legacy crypto (so must be migrated), then this must still be provided, so that the * data can be migrated from the legacy store. @@ -392,7 +392,7 @@ export interface ICreateClientOpts { * * This must be set to the same value every time the client is initialised for the same device. * - * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}), + * This is only used for the legacy crypto implementation, * but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device * previously used legacy crypto (so must be migrated), then this must still be provided, so that the * data can be migrated from the legacy store. @@ -2139,92 +2139,9 @@ export class MatrixClient extends TypedEventEmitter { - if (!isCryptoAvailable()) { - throw new Error( - `End-to-end encryption not supported in this js-sdk build: did ` + - `you remember to load the olm library?`, - ); - } - - if (this.cryptoBackend) { - this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); - return; - } - - if (!this.cryptoStore) { - // the cryptostore is provided by sdk.createClient, so this shouldn't happen - throw new Error(`Cannot enable encryption: no cryptoStore provided`); - } - - this.logger.debug("Crypto: Starting up crypto store..."); - await this.cryptoStore.startup(); - - const userId = this.getUserId(); - if (userId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown userId: ` + - `ensure userId is passed in createClient().`, - ); - } - if (this.deviceId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown deviceId: ` + - `ensure deviceId is passed in createClient().`, - ); - } - - const crypto = new Crypto(this, userId, this.deviceId, this.store, this.cryptoStore, this.verificationMethods!); - - this.reEmitter.reEmit(crypto, [ - LegacyCryptoEvent.KeyBackupFailed, - LegacyCryptoEvent.KeyBackupSessionsRemaining, - LegacyCryptoEvent.RoomKeyRequest, - LegacyCryptoEvent.RoomKeyRequestCancellation, - LegacyCryptoEvent.Warning, - LegacyCryptoEvent.DevicesUpdated, - LegacyCryptoEvent.WillUpdateDevices, - LegacyCryptoEvent.DeviceVerificationChanged, - LegacyCryptoEvent.UserTrustStatusChanged, - LegacyCryptoEvent.KeysChanged, - ]); - - this.logger.debug("Crypto: initialising crypto object..."); - await crypto.init({ - exportedOlmDevice: this.exportedOlmDeviceToImport, - pickleKey: this.pickleKey, - }); - delete this.exportedOlmDeviceToImport; - - this.olmVersion = Crypto.getOlmVersion(); - - // if crypto initialisation was successful, tell it to attach its event handlers. - crypto.registerEventHandlers(this as Parameters[0]); - this.cryptoBackend = this.crypto = crypto; - - // upload our keys in the background - this.crypto.uploadDeviceKeys().catch((e) => { - // TODO: throwing away this error is a really bad idea. - this.logger.error("Error uploading device keys", e); - }); - } - /** * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto. * - * An alternative to {@link initLegacyCrypto}. - * * **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to * the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for * ensuring that only one `MatrixClient` issue is instantiated at a time. @@ -2324,7 +2241,7 @@ export class MatrixClient extends TypedEventEmitter Date: Mon, 27 Jan 2025 10:50:49 +0100 Subject: [PATCH 04/14] Remove legacy crypto support in `sync` api (#4622) --- src/client.ts | 1 - src/sync.ts | 47 ----------------------------------------------- 2 files changed, 48 deletions(-) diff --git a/src/client.ts b/src/client.ts index 30c8a628125..7447b7f415e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1555,7 +1555,6 @@ export class MatrixClient extends TypedEventEmitter { if (!this.canResetTimelineCallback) { diff --git a/src/sync.ts b/src/sync.ts index 37ebc9139fb..00c91687078 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -59,7 +59,6 @@ import { BeaconEvent } from "./models/beacon.ts"; import { IEventsResponse } from "./@types/requests.ts"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts"; import { Feature, ServerSupport } from "./feature.ts"; -import { Crypto } from "./crypto/index.ts"; import { KnownMembership } from "./@types/membership.ts"; const DEBUG = true; @@ -116,13 +115,6 @@ function debuglog(...params: any[]): void { * Options passed into the constructor of SyncApi by MatrixClient */ export interface SyncApiOptions { - /** - * Crypto manager - * - * @deprecated in favour of cryptoCallbacks - */ - crypto?: Crypto; - /** * If crypto is enabled on our client, callbacks into the crypto module */ @@ -642,9 +634,6 @@ export class SyncApi { } this.opts.filter.setLazyLoadMembers(true); } - if (this.opts.lazyLoadMembers) { - this.syncOpts.crypto?.enableLazyLoading(); - } }; private storeClientOptions = async (): Promise => { @@ -880,12 +869,6 @@ export class SyncApi { catchingUp: this.catchingUp, }; - if (this.syncOpts.crypto) { - // tell the crypto module we're about to process a sync - // response - await this.syncOpts.crypto.onSyncWillProcess(syncEventData); - } - try { await this.processSyncResponse(syncEventData, data); } catch (e) { @@ -920,15 +903,6 @@ export class SyncApi { this.updateSyncState(SyncState.Syncing, syncEventData); if (this.client.store.wantsSave()) { - // We always save the device list (if it's dirty) before saving the sync data: - // this means we know the saved device list data is at least as fresh as the - // stored sync data which means we don't have to worry that we may have missed - // device changes. We can also skip the delay since we're not calling this very - // frequently (and we don't really want to delay the sync for it). - if (this.syncOpts.crypto) { - await this.syncOpts.crypto.saveDeviceList(0); - } - // tell databases that everything is now in a consistent state and can be saved. await this.client.store.save(); } @@ -1248,27 +1222,6 @@ export class SyncApi { await this.injectRoomEvents(room, stateEvents, undefined); - const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender(); - - const crypto = client.crypto; - if (crypto) { - const parkedHistory = await crypto.cryptoStore.takeParkedSharedHistory(room.roomId); - for (const parked of parkedHistory) { - if (parked.senderId === inviter) { - await crypto.olmDevice.addInboundGroupSession( - room.roomId, - parked.senderKey, - parked.forwardingCurve25519KeyChain, - parked.sessionId, - parked.sessionKey, - parked.keysClaimed, - true, - { sharedHistory: true, untrusted: true }, - ); - } - } - } - if (inviteObj.isBrandNewRoom) { room.recalculate(); client.store.storeRoom(room); From f76319221fc68f283854aa42d2d01df5dc372eda Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 27 Jan 2025 15:16:14 +0100 Subject: [PATCH 05/14] Remove deprecated `DeviceInfo` in `webrtc/call.ts` (#4654) * chore(legacy call): Remove `DeviceInfo` usage * refactor(legacy call): throw `GroupCallUnknownDeviceError` at the end of `initOpponentCrypto` --- src/webrtc/call.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 6ab15d48b86..ac1fde665dc 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -48,7 +48,6 @@ import { import { CallFeed } from "./callFeed.ts"; import { MatrixClient } from "../client.ts"; import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter.ts"; -import { DeviceInfo } from "../crypto/deviceinfo.ts"; import { GroupCallUnknownDeviceError } from "./groupCall.ts"; import { IScreensharingOpts } from "./mediaHandler.ts"; import { MatrixError } from "../http-api/index.ts"; @@ -426,7 +425,7 @@ export class MatrixCall extends TypedEventEmitter Date: Tue, 28 Jan 2025 15:27:09 +0100 Subject: [PATCH 06/14] Remove deprecated methods and attributes of `MatrixClient` (#4659) * feat(legacy crypto)!: remove deprecated methods of `MatrixClient` * test(legacy crypto): update existing tests to not use legacy crypto - `Embedded.spec.ts`: casting since `encryptAndSendToDevices` is removed from `MatrixClient`. - `room.spec.ts`: remove deprecated usage of `MatrixClient.crypto` - `matrix-client.spec.ts` & `matrix-client-methods.spec.ts`: remove calls of deprecated methods of `MatrixClient` * test(legacy crypto): remove test files using `MatrixClient` deprecated methods * test(legacy crypto): update existing integ tests to run successfully * feat(legacy crypto!): remove `ICreateClientOpts.deviceToImport`. `ICreateClientOpts.deviceToImport` was used in the legacy cryto. The rust crypto doesn't support to import devices in this way. * feat(legacy crypto!): remove `{get,set}GlobalErrorOnUnknownDevices` `globalErrorOnUnknownDevices` is not used in the rust-crypto. The API is marked as unstable, we can remove it. --- spec/integ/crypto/cross-signing.spec.ts | 3 +- spec/integ/crypto/crypto.spec.ts | 530 +--- spec/integ/crypto/device-dehydration.spec.ts | 4 +- spec/integ/crypto/megolm-backup.spec.ts | 114 +- spec/integ/crypto/rust-crypto.spec.ts | 3 +- spec/integ/crypto/verification.spec.ts | 5 +- spec/integ/matrix-client-methods.spec.ts | 45 - spec/unit/crypto/backup.spec.ts | 214 -- spec/unit/crypto/dehydration.spec.ts | 62 - spec/unit/embedded.spec.ts | 3 +- spec/unit/matrix-client.spec.ts | 93 +- spec/unit/room.spec.ts | 2 +- src/client.ts | 2438 ++---------------- 13 files changed, 218 insertions(+), 3298 deletions(-) delete mode 100644 spec/unit/crypto/backup.spec.ts delete mode 100644 spec/unit/crypto/dehydration.spec.ts diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index 95b0f756e0a..840970aebc2 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -19,7 +19,7 @@ import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; -import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src"; +import { AuthDict, createClient, MatrixClient } from "../../../src"; import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints"; import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api"; @@ -37,6 +37,7 @@ import { import * as testData from "../../test-utils/test-data"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator"; +import { CryptoEvent } from "../../../src/crypto-api"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 466a46e39a7..b241975af70 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -24,7 +24,6 @@ import Olm from "@matrix-org/olm"; import * as testUtils from "../../test-utils/test-utils"; import { - advanceTimersUntil, CRYPTO_BACKENDS, emitPromise, getSyncResponse, @@ -49,7 +48,6 @@ import { Category, ClientEvent, createClient, - CryptoEvent, HistoryVisibility, IClaimOTKsResult, IContent, @@ -65,7 +63,6 @@ import { RoomMember, RoomStateEvent, } from "../../../src/matrix"; -import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; import { defer, escapeRegExp } from "../../../src/utils"; @@ -98,11 +95,11 @@ import { establishOlmSession, getTestOlmAccountKeys, } from "./olm-utils"; -import { ToDevicePayload } from "../../../src/models/ToDeviceMessage"; import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator"; import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event"; import { KnownMembership } from "../../../src/@types/membership"; import { KeyBackup } from "../../../src/rust-crypto/backup.ts"; +import { CryptoEvent } from "../../../src/crypto-api"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections @@ -415,13 +412,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - const p2pSession = await createOlmSession(testOlmAccount, keyReceiver); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -878,13 +868,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - const p2pSession = await createOlmSession(testOlmAccount, keyReceiver); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -939,13 +922,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - const p2pSession = await createOlmSession(testOlmAccount, keyReceiver); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -1018,7 +994,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, keyResponder.addDeviceKeys(testDeviceKeys); await startClientAndAwaitFirstSync(); - aliceClient.setGlobalErrorOnUnknownDevices(false); // tell alice she is sharing a room with bob syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); @@ -1030,17 +1005,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, // fire off the prepare request const room = aliceClient.getRoom(ROOM_ID); expect(room).toBeTruthy(); - const p = aliceClient.prepareToEncrypt(room!); + aliceClient.getCrypto()?.prepareToEncrypt(room!); // we expect to get a room key message await expectSendRoomKey("@bob:xyz", testOlmAccount); - - // the prepare request should complete successfully. - await p; }); - it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => { - aliceClient.setGlobalErrorOnUnknownDevices(false); + it("Alice sends a megolm message", async () => { const homeserverUrl = aliceClient.getHomeserverUrl(); const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); @@ -1068,7 +1039,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }); it("We should start a new megolm session after forceDiscardSession", async () => { - aliceClient.setGlobalErrorOnUnknownDevices(false); const homeserverUrl = aliceClient.getHomeserverUrl(); const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); @@ -1095,7 +1065,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, ]); // Finally the interesting part: discard the session. - aliceClient.forceDiscardSession(ROOM_ID); + aliceClient.getCrypto()!.forceDiscardSession(ROOM_ID); // Now when we send the next message, we should get a *new* megolm session. const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount); @@ -1103,207 +1073,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]); }); - oldBackendOnly("Alice sends a megolm message", async () => { - // TODO: do something about this for the rust backend. - // Currently it fails because we don't respect the default GlobalErrorOnUnknownDevices and - // send messages to unknown devices. - - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - // start out with the device unknown - the send should be rejected. - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await aliceClient.sendTextMessage(ROOM_ID, "test").then( - () => { - throw new Error("sendTextMessage failed on an unknown device"); - }, - (e) => { - expect(e.name).toEqual("UnknownDeviceError"); - }, - ); - - // mark the device as known, and resend. - aliceClient.setDeviceKnown("@bob:xyz", "DEVICE_ID"); - - const room = aliceClient.getRoom(ROOM_ID)!; - const pendingMsg = room.getPendingEvents()[0]; - - const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); - - await Promise.all([ - aliceClient.resendEvent(pendingMsg, room), - expectSendMegolmMessage(inboundGroupSessionPromise), - ]); - }); - - oldBackendOnly("We shouldn't attempt to send to blocked devices", async () => { - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - logger.log("Forcing alice to download our device keys"); - - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await aliceClient.downloadKeys(["@bob:xyz"]); - - logger.log("Telling alice to block our device"); - aliceClient.setDeviceBlocked("@bob:xyz", "DEVICE_ID"); - - logger.log("Telling alice to send a megolm message"); - fetchMock.putOnce({ url: new RegExp("/send/"), name: "send-event" }, { event_id: "$event_id" }); - fetchMock.putOnce({ url: new RegExp("/sendToDevice/m.room_key.withheld/"), name: "send-withheld" }, {}); - - await aliceClient.sendTextMessage(ROOM_ID, "test"); - - // check that the event and withheld notifications were both sent - expect(fetchMock.done("send-event")).toBeTruthy(); - expect(fetchMock.done("send-withheld")).toBeTruthy(); - }); - - describe("get|setGlobalErrorOnUnknownDevices", () => { - it("should raise an error if crypto is disabled", () => { - aliceClient["cryptoBackend"] = undefined; - expect(() => aliceClient.setGlobalErrorOnUnknownDevices(true)).toThrow("encryption disabled"); - expect(() => aliceClient.getGlobalErrorOnUnknownDevices()).toThrow("encryption disabled"); - }); - - oldBackendOnly("should permit sending to unknown devices", async () => { - expect(aliceClient.getGlobalErrorOnUnknownDevices()).toBeTruthy(); - - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - // start out with the device unknown - the send should be rejected. - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await aliceClient.sendTextMessage(ROOM_ID, "test").then( - () => { - throw new Error("sendTextMessage failed on an unknown device"); - }, - (e) => { - expect(e.name).toEqual("UnknownDeviceError"); - }, - ); - - // enable sending to unknown devices, and resend - aliceClient.setGlobalErrorOnUnknownDevices(false); - expect(aliceClient.getGlobalErrorOnUnknownDevices()).toBeFalsy(); - - const room = aliceClient.getRoom(ROOM_ID)!; - const pendingMsg = room.getPendingEvents()[0]; - - const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); - - await Promise.all([ - aliceClient.resendEvent(pendingMsg, room), - expectSendMegolmMessage(inboundGroupSessionPromise), - ]); - }); - }); - - describe("get|setGlobalBlacklistUnverifiedDevices", () => { - it("should raise an error if crypto is disabled", () => { - aliceClient["cryptoBackend"] = undefined; - expect(() => aliceClient.setGlobalBlacklistUnverifiedDevices(true)).toThrow("encryption disabled"); - expect(() => aliceClient.getGlobalBlacklistUnverifiedDevices()).toThrow("encryption disabled"); - }); - - oldBackendOnly("should disable sending to unverified devices", async () => { - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - // tell alice we share a room with bob - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - logger.log("Forcing alice to download our device keys"); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await aliceClient.downloadKeys(["@bob:xyz"]); - - logger.log("Telling alice to block messages to unverified devices"); - expect(aliceClient.getGlobalBlacklistUnverifiedDevices()).toBeFalsy(); - aliceClient.setGlobalBlacklistUnverifiedDevices(true); - expect(aliceClient.getGlobalBlacklistUnverifiedDevices()).toBeTruthy(); - - logger.log("Telling alice to send a megolm message"); - fetchMock.putOnce(new RegExp("/send/"), { event_id: "$event_id" }); - fetchMock.putOnce(new RegExp("/sendToDevice/m.room_key.withheld/"), {}); - - await aliceClient.sendTextMessage(ROOM_ID, "test"); - - // Now, let's mark the device as verified, and check that keys are sent to it. - - logger.log("Marking the device as verified"); - // XXX: this is an integration test; we really ought to do this via the cross-signing dance - const d = aliceClient.crypto!.deviceList.getStoredDevice("@bob:xyz", "DEVICE_ID")!; - d.verified = DeviceInfo.DeviceVerification.VERIFIED; - aliceClient.crypto?.deviceList.storeDevicesForUser("@bob:xyz", { DEVICE_ID: d }); - - const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); - - logger.log("Asking alice to re-send"); - await Promise.all([ - expectSendMegolmMessage(inboundGroupSessionPromise).then((decrypted) => { - expect(decrypted.type).toEqual("m.room.message"); - expect(decrypted.content!.body).toEqual("test"); - }), - aliceClient.sendTextMessage(ROOM_ID, "test"), - ]); - }); - - it("should send a m.unverified code in toDevice messages to an unverified device when globalBlacklistUnverifiedDevices=true", async () => { - aliceClient.getCrypto()!.globalBlacklistUnverifiedDevices = true; - - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - // Tell alice we share a room with bob - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - // Force alice to download bob keys - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - // Wait to receive the toDevice message and return bob device content - const toDevicePromise = new Promise((resolve) => { - fetchMock.putOnce(new RegExp("/sendToDevice/m.room_key.withheld/"), (url, request) => { - const content = JSON.parse(request.body as string); - resolve(content.messages["@bob:xyz"]["DEVICE_ID"]); - return {}; - }); - }); - - // Mock endpoint of message sending - fetchMock.put(new RegExp("/send/"), { event_id: "$event_id" }); - - await aliceClient.sendTextMessage(ROOM_ID, "test"); - - // Finally, check that the toDevice message has the m.unverified code - const toDeviceContent = await toDevicePromise; - expect(toDeviceContent.code).toBe("m.unverified"); - }); - }); - describe("Session should rotate according to encryption settings", () => { /** * Send a message to bob and get the encrypted message @@ -1472,272 +1241,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(sessionId).not.toEqual(newSessionId); }); - oldBackendOnly("We should start a new megolm session when a device is blocked", async () => { - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - logger.log("Fetching bob's devices and marking known"); - - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await aliceClient.downloadKeys(["@bob:xyz"]); - await aliceClient.setDeviceKnown("@bob:xyz", "DEVICE_ID"); - - logger.log("Telling alice to send a megolm message"); - - let megolmSessionId: string; - const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); - inboundGroupSessionPromise.then((igs) => { - megolmSessionId = igs.session_id(); - }); - - await Promise.all([ - aliceClient.sendTextMessage(ROOM_ID, "test"), - expectSendMegolmMessage(inboundGroupSessionPromise), - ]); - - logger.log("Telling alice to block our device"); - aliceClient.setDeviceBlocked("@bob:xyz", "DEVICE_ID"); - - logger.log("Telling alice to send another megolm message"); - - fetchMock.putOnce( - { url: new RegExp("/send/"), name: "send-event" }, - (url: string, opts: RequestInit): FetchMock.MockResponse => { - const content = JSON.parse(opts.body as string); - logger.log("/send:", content); - // make sure that a new session is used - expect(content.session_id).not.toEqual(megolmSessionId); - return { - event_id: "$event_id", - }; - }, - ); - fetchMock.putOnce({ url: new RegExp("/sendToDevice/m.room_key.withheld/"), name: "send-withheld" }, {}); - - await aliceClient.sendTextMessage(ROOM_ID, "test2"); - - // check that the event and withheld notifications were both sent - expect(fetchMock.done("send-event")).toBeTruthy(); - expect(fetchMock.done("send-withheld")).toBeTruthy(); - }); - - // https://github.com/vector-im/element-web/issues/2676 - oldBackendOnly("Alice should send to her other devices", async () => { - // for this test, we make the testOlmAccount be another of Alice's devices. - // it ought to get included in messages Alice sends. - expectAliceKeyQuery(getTestKeysQueryResponse(aliceClient.getUserId()!)); - - await startClientAndAwaitFirstSync(); - // an encrypted room with just alice - const syncResponse = { - next_batch: 1, - rooms: { - join: { - [ROOM_ID]: { - state: { - events: [ - testUtils.mkEvent({ - type: "m.room.encryption", - skey: "", - content: { algorithm: "m.megolm.v1.aes-sha2" }, - }), - testUtils.mkMembership({ - mship: KnownMembership.Join, - sender: aliceClient.getUserId()!, - }), - ], - }, - }, - }, - }, - }; - syncResponder.sendOrQueueSyncResponse(syncResponse); - - await syncPromise(aliceClient); - - // start out with the device unknown - the send should be rejected. - try { - await aliceClient.sendTextMessage(ROOM_ID, "test"); - throw new Error("sendTextMessage succeeded on an unknown device"); - } catch (e) { - expect((e as any).name).toEqual("UnknownDeviceError"); - expect([...(e as any).devices.keys()]).toEqual([aliceClient.getUserId()!]); - expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID")).toBeTruthy(); - } - - // mark the device as known, and resend. - aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID"); - expectAliceKeyClaim((url: string, opts: RequestInit): FetchMock.MockResponse => { - const content = JSON.parse(opts.body as string); - expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519"); - return getTestKeysClaimResponse(aliceClient.getUserId()!); - }); - - const inboundGroupSessionPromise = expectSendRoomKey(aliceClient.getUserId()!, testOlmAccount); - - let decrypted: Partial = {}; - - // Grab the event that we'll need to resend - const room = aliceClient.getRoom(ROOM_ID)!; - const pendingEvents = room.getPendingEvents(); - expect(pendingEvents.length).toEqual(1); - const unsentEvent = pendingEvents[0]; - - await Promise.all([ - expectSendMegolmMessage(inboundGroupSessionPromise).then((d) => { - decrypted = d; - }), - aliceClient.resendEvent(unsentEvent, room), - ]); - - expect(decrypted.type).toEqual("m.room.message"); - expect(decrypted.content?.body).toEqual("test"); - }); - - oldBackendOnly("Alice should wait for device list to complete when sending a megolm message", async () => { - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - - syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); - await syncPromise(aliceClient); - - // this will block - logger.log("Forcing alice to download our device keys"); - const downloadPromise = aliceClient.downloadKeys(["@bob:xyz"]); - - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - // so will this. - const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test").then( - () => { - throw new Error("sendTextMessage failed on an unknown device"); - }, - (e) => { - expect(e.name).toEqual("UnknownDeviceError"); - }, - ); - - expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); - - await Promise.all([downloadPromise, sendPromise]); - }); - - oldBackendOnly("Alice exports megolm keys and imports them to a new device", async () => { - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - - // establish an olm session with alice - const p2pSession = await createOlmSession(testOlmAccount, keyReceiver); - - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - - // make the room_key event - const roomKeyEncrypted = encryptGroupSessionKey({ - recipient: aliceClient.getUserId()!, - recipientCurve25519Key: keyReceiver.getDeviceKey(), - recipientEd25519Key: keyReceiver.getSigningKey(), - olmAccount: testOlmAccount, - p2pSession: p2pSession, - groupSession: groupSession, - room_id: ROOM_ID, - }); - - // encrypt a message with the group session - const messageEncrypted = encryptMegolmEvent({ - senderKey: testSenderKey, - groupSession: groupSession, - room_id: ROOM_ID, - }); - - // Alice gets both the events in a single sync - syncResponder.sendOrQueueSyncResponse({ - next_batch: 1, - to_device: { - events: [roomKeyEncrypted], - }, - rooms: { - join: { [ROOM_ID]: { timeline: { events: [messageEncrypted] } } }, - }, - }); - await syncPromise(aliceClient); - - const room = aliceClient.getRoom(ROOM_ID)!; - await room.decryptCriticalEvents(); - - // it probably won't be decrypted yet, because it takes a while to process the olm keys - const decryptedEvent = await testUtils.awaitDecryption(room.getLiveTimeline().getEvents()[0], { - waitOnDecryptionFailure: true, - }); - expect(decryptedEvent.getContent().body).toEqual("42"); - - const exported = await aliceClient.getCrypto()!.exportRoomKeysAsJson(); - - // start a new client - await aliceClient.stopClient(); - - const homeserverUrl = "https://alice-server2.com"; - aliceClient = createClient({ - baseUrl: homeserverUrl, - userId: "@alice:localhost", - accessToken: "akjgkrgjs", - deviceId: "xzcvb", - }); - - keyReceiver = new E2EKeyReceiver(homeserverUrl); - syncResponder = new SyncResponder(homeserverUrl); - await initCrypto(aliceClient); - await aliceClient.getCrypto()!.importRoomKeysAsJson(exported); - expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); - await startClientAndAwaitFirstSync(); - - aliceClient.startClient(); - - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - - const syncResponse = { - next_batch: 1, - rooms: { - join: { [ROOM_ID]: { timeline: { events: [messageEncrypted] } } }, - }, - }; - - syncResponder.sendOrQueueSyncResponse(syncResponse); - await syncPromise(aliceClient); - - const event = room.getLiveTimeline().getEvents()[0]; - expect(event.getContent().body).toEqual("42"); - }); - it("Alice can decrypt a message with falsey content", async () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); - // 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.getUserByIdentityKey = () => "@bob:xyz"; - } - const p2pSession = await createOlmSession(testOlmAccount, keyReceiver); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -1883,7 +1390,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, describe("getEncryptionInfoForEvent", () => { it("handles outgoing events", async () => { - aliceClient.setGlobalErrorOnUnknownDevices(false); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); @@ -1987,7 +1493,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, // set up the aliceTestClient so that it is a room with no known members expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync({ lazyLoadMembers: true }); - aliceClient.setGlobalErrorOnUnknownDevices(false); syncResponder.sendOrQueueSyncResponse(getSyncResponse([])); await syncPromise(aliceClient); @@ -2790,6 +2295,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await bootstrapSecurity(backupVersion); const check = await aliceClient.getCrypto()!.checkKeyBackupAndEnable(); + fetchMock.get( + `path:/_matrix/client/v3/room_keys/version/${check!.backupInfo.version}`, + check!.backupInfo!, + ); // Import a new key that should be uploaded const newKey = testData.MEGOLM_SESSION_DATA; @@ -2824,9 +2333,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, fetchMock.get("express:/_matrix/client/v3/room_keys/keys", keyBackupData); // should be able to restore from 4S - const importResult = await advanceTimersUntil( - aliceClient.restoreKeyBackupWithSecretStorage(check!.backupInfo!), - ); + await aliceClient.getCrypto()!.loadSessionBackupPrivateKeyFromSecretStorage(); + const importResult = await aliceClient.getCrypto()!.restoreKeyBackup(); expect(importResult.imported).toStrictEqual(1); }); @@ -2891,19 +2399,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, const newBackupUploadPromise = awaitMegolmBackupKeyUpload(); - // Track calls to scheduleAllGroupSessionsForBackup. This is - // only relevant on legacy encryption. - const scheduleAllGroupSessionsForBackup = jest.fn(); - if (backend === "libolm") { - aliceClient.crypto!.backupManager.scheduleAllGroupSessionsForBackup = - scheduleAllGroupSessionsForBackup; - } else { - // With Rust crypto, we don't need to call this function, so - // we call the dummy value here so we pass our later - // expectation. - scheduleAllGroupSessionsForBackup(); - } - await aliceClient.getCrypto()!.resetKeyBackup(); await awaitDeleteCalled; await newBackupStatusUpdate; @@ -2915,11 +2410,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(nextVersion).toBeDefined(); expect(nextVersion).not.toEqual(currentVersion); expect(nextKey).not.toEqual(currentBackupKey); - expect(scheduleAllGroupSessionsForBackup).toHaveBeenCalled(); - // The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend - // ensure that it works anyhow - await aliceClient.deleteKeyBackupVersion(nextVersion!); + await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!); await aliceClient.getCrypto()!.checkKeyBackupAndEnable(); // XXX Legacy crypto does not update 4S when doing that; should ensure that rust implem does it. expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull(); diff --git a/spec/integ/crypto/device-dehydration.spec.ts b/spec/integ/crypto/device-dehydration.spec.ts index cf319a9878c..4a1165128e1 100644 --- a/spec/integ/crypto/device-dehydration.spec.ts +++ b/spec/integ/crypto/device-dehydration.spec.ts @@ -172,8 +172,8 @@ async function initializeSecretStorage( privateKey: new Uint8Array(32), }; } - await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true }); - await matrixClient.bootstrapSecretStorage({ + await matrixClient.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true }); + await matrixClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey, setupNewSecretStorage: true, setupNewKeyBackup: false, diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index eff0ff567e1..b3d6fe335a8 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -117,7 +117,6 @@ function mockUploadEmitter( describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => { // oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the // Rust backend. Once we have full support in the rust sdk, it will go away. - const oldBackendOnly = backend === "rust-sdk" ? test.skip : test; const newBackendOnly = backend === "libolm" ? test.skip : test; const isNewBackend = backend === "rust-sdk"; @@ -344,43 +343,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); const check = await aliceCrypto.checkKeyBackupAndEnable(); - - let onKeyCached: () => void; - const awaitKeyCached = new Promise((resolve) => { - onKeyCached = resolve; - }); - await aliceCrypto.storeSessionBackupPrivateKey( decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58), check!.backupInfo!.version!, ); - const result = await advanceTimersUntil( - isNewBackend - ? aliceCrypto.restoreKeyBackup() - : aliceClient.restoreKeyBackupWithRecoveryKey( - testData.BACKUP_DECRYPTION_KEY_BASE58, - undefined, - undefined, - check!.backupInfo!, - { - cacheCompleteCallback: () => onKeyCached(), - }, - ), - ); + const result = await advanceTimersUntil(aliceCrypto.restoreKeyBackup()); expect(result.imported).toStrictEqual(1); - - if (isNewBackend) return; - - await awaitKeyCached; - - // The key should be now cached - const afterCache = await advanceTimersUntil( - aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!), - ); - - expect(afterCache.imported).toStrictEqual(1); }); /** @@ -434,19 +404,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe ); const progressCallback = jest.fn(); - const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup({ - progressCallback, - }) - : aliceClient.restoreKeyBackupWithRecoveryKey( - testData.BACKUP_DECRYPTION_KEY_BASE58, - undefined, - undefined, - check!.backupInfo!, - { - progressCallback, - }, - )); + const result = await aliceCrypto.restoreKeyBackup({ + progressCallback, + }); expect(result.imported).toStrictEqual(expectedTotal); // Should be called 5 times: 200*4 plus one chunk with the remaining 32 @@ -508,17 +468,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe ); const progressCallback = jest.fn(); - const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup({ progressCallback }) - : aliceClient.restoreKeyBackupWithRecoveryKey( - testData.BACKUP_DECRYPTION_KEY_BASE58, - undefined, - undefined, - check!.backupInfo!, - { - progressCallback, - }, - )); + const result = await aliceCrypto.restoreKeyBackup({ progressCallback }); expect(result.total).toStrictEqual(expectedTotal); // A chunk failed to import @@ -574,40 +524,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe check!.backupInfo!.version!, ); - const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup() - : aliceClient.restoreKeyBackupWithRecoveryKey( - testData.BACKUP_DECRYPTION_KEY_BASE58, - undefined, - undefined, - check!.backupInfo!, - )); + const result = await aliceCrypto.restoreKeyBackup(); expect(result.total).toStrictEqual(expectedTotal); // A chunk failed to import expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount); }); - oldBackendOnly("recover specific session from backup", async function () { - fetchMock.get( - "express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", - testData.CURVE25519_KEY_BACKUP_DATA, - ); - - const check = await aliceCrypto.checkKeyBackupAndEnable(); - - const result = await advanceTimersUntil( - aliceClient.restoreKeyBackupWithRecoveryKey( - testData.BACKUP_DECRYPTION_KEY_BASE58, - ROOM_ID, - testData.MEGOLM_SESSION_DATA.session_id, - check!.backupInfo!, - ), - ); - - expect(result.imported).toStrictEqual(1); - }); - newBackendOnly( "Should get the decryption key from the secret storage and restore the key backup", async function () { @@ -634,31 +557,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe }, ); - oldBackendOnly("Fails on bad recovery key", async function () { - const fullBackup = { - rooms: { - [ROOM_ID]: { - sessions: { - [testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA, - }, - }, - }, - }; - - fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); - - const check = await aliceCrypto.checkKeyBackupAndEnable(); - - await expect( - aliceClient.restoreKeyBackupWithRecoveryKey( - "EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD", - undefined, - undefined, - check!.backupInfo!, - ), - ).rejects.toThrow(); - }); - newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => { await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store"); }); diff --git a/spec/integ/crypto/rust-crypto.spec.ts b/spec/integ/crypto/rust-crypto.spec.ts index 5aee7e83582..2394d9a3ef9 100644 --- a/spec/integ/crypto/rust-crypto.spec.ts +++ b/spec/integ/crypto/rust-crypto.spec.ts @@ -18,12 +18,13 @@ import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; import fetchMock from "fetch-mock-jest"; -import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src"; +import { createClient, IndexedDBCryptoStore } from "../../../src"; import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump"; import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump"; import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified"; import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account"; import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account"; +import { CryptoEvent } from "../../../src/crypto-api"; jest.setTimeout(15000); diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index a4cee9e8365..47d92d3e96e 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -25,7 +25,6 @@ import Olm from "@matrix-org/olm"; import { createClient, - CryptoEvent, DeviceVerification, IContent, ICreateClientOpts, @@ -81,7 +80,7 @@ import { getTestOlmAccountKeys, ToDeviceEvent, } from "./olm-utils"; -import { KeyBackupInfo } from "../../../src/crypto-api"; +import { KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api"; import { encodeBase64 } from "../../../src/base64"; // The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations @@ -907,7 +906,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st describe("Send verification request in DM", () => { beforeEach(async () => { aliceClient = await startTestClient(); - aliceClient.setGlobalErrorOnUnknownDevices(false); e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA); e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA); @@ -990,7 +988,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st testOlmAccount.create(); aliceClient = await startTestClient(); - aliceClient.setGlobalErrorOnUnknownDevices(false); syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID])); await syncPromise(aliceClient); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 11603f53432..6a1544e6f15 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import HttpBackend from "matrix-mock-request"; -import { Mocked } from "jest-mock"; import * as utils from "../test-utils/test-utils"; import { IStoredClientOpts, MatrixClient } from "../../src/client"; @@ -34,7 +33,6 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { IFilterDefinition } from "../../src/filter"; import { ISearchResults } from "../../src/@types/search"; import { IStore } from "../../src/store"; -import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; import { SetPresence } from "../../src/sync"; import { KnownMembership } from "../../src/@types/membership"; @@ -1508,49 +1506,6 @@ describe("MatrixClient", function () { }); }); - describe("uploadKeys", () => { - // uploadKeys() is a no-op nowadays, so there's not much to test here. - it("should complete successfully", async () => { - await client.uploadKeys(); - }); - }); - - describe("getCryptoTrustCrossSignedDevices", () => { - it("should throw if e2e is disabled", () => { - expect(() => client.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled"); - }); - - it("should proxy to the crypto backend", async () => { - const mockBackend = { - getTrustCrossSignedDevices: jest.fn().mockReturnValue(true), - } as unknown as Mocked; - client["cryptoBackend"] = mockBackend; - - expect(client.getCryptoTrustCrossSignedDevices()).toBe(true); - mockBackend.getTrustCrossSignedDevices.mockReturnValue(false); - expect(client.getCryptoTrustCrossSignedDevices()).toBe(false); - }); - }); - - describe("setCryptoTrustCrossSignedDevices", () => { - it("should throw if e2e is disabled", () => { - expect(() => client.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled"); - }); - - it("should proxy to the crypto backend", async () => { - const mockBackend = { - setTrustCrossSignedDevices: jest.fn(), - } as unknown as Mocked; - client["cryptoBackend"] = mockBackend; - - client.setCryptoTrustCrossSignedDevices(true); - expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true); - - client.setCryptoTrustCrossSignedDevices(false); - expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false); - }); - }); - describe("setSyncPresence", () => { it("should pass calls through to the underlying sync api", () => { const setPresence = jest.fn(); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts deleted file mode 100644 index 6fc367959bf..00000000000 --- a/spec/unit/crypto/backup.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* -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 "../../olm-loader"; -import { logger } from "../../../src/logger"; -import * as olmlib from "../../../src/crypto/olmlib"; -import { MatrixClient } from "../../../src/client"; -import { MatrixEvent } from "../../../src/models/event"; -import * as algorithms from "../../../src/crypto/algorithms"; -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 { BackupManager } from "../../../src/crypto/backup"; -import { StubStore } from "../../../src/store/stub"; -import { MatrixScheduler } from "../../../src"; -import { CryptoStore } from "../../../src/crypto/store/base"; -import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm"; - -const Olm = globalThis.Olm; - -const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!; - -const ROOM_ID = "!ROOM:ID"; - -const CURVE25519_BACKUP_INFO = { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - version: "1", - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, -}; - -const keys: Record = {}; - -function getCrossSigningKey(type: string) { - return Promise.resolve(keys[type]); -} - -function saveCrossSigningKeys(k: Record) { - Object.assign(keys, k); -} - -function makeTestScheduler(): MatrixScheduler { - return (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce( - (r, k) => { - r[k] = jest.fn(); - return r; - }, - {} as MatrixScheduler, - ); -} - -function makeTestClient(cryptoStore: CryptoStore) { - 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, - cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, - }); - - // initialising the crypto library will trigger a key upload request, which we can stub out - client.uploadKeysRequest = jest.fn(); - return client; -} - -describe("MegolmBackup", function () { - if (!globalThis.Olm) { - logger.warn("Not running megolm backup unit tests: libolm not present"); - return; - } - - beforeAll(function () { - return Olm.init(); - }); - - let olmDevice: OlmDevice; - let mockOlmLib: typeof olmlib; - let mockCrypto: Crypto; - let cryptoStore: CryptoStore; - let megolmDecryption: MegolmDecryptionClass; - beforeEach(async function () { - mockCrypto = testUtils.mock(Crypto, "Crypto"); - // @ts-ignore making mock - mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); - mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO; - - cryptoStore = new MemoryCryptoStore(); - - olmDevice = new OlmDevice(cryptoStore); - - // we stub out the olm encryption bits - mockOlmLib = {} as unknown as typeof olmlib; - mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); - mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined); - }); - - describe("backup", function () { - let mockBaseApis: MatrixClient; - - beforeEach(function () { - mockBaseApis = {} as unknown as MatrixClient; - - megolmDecryption = new MegolmDecryption({ - userId: "@user:id", - crypto: mockCrypto, - olmDevice: olmDevice, - baseApis: mockBaseApis, - roomId: ROOM_ID, - }) as MegolmDecryptionClass; - - // @ts-ignore private field access - megolmDecryption.olmlib = mockOlmLib; - - // clobber the setTimeout function to run 100x faster. - // ideally we would use lolex, but we have no oportunity - // to tick the clock between the first try and the retry. - const realSetTimeout = globalThis.setTimeout; - jest.spyOn(globalThis, "setTimeout").mockImplementation(function (f, n) { - return realSetTimeout(f!, n! / 100); - }); - }); - - afterEach(function () { - jest.spyOn(globalThis, "setTimeout").mockRestore(); - }); - - test("fail if crypto not enabled", async () => { - const client = makeTestClient(cryptoStore); - const data = { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - version: "1", - auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - }, - }; - await expect(client.restoreKeyBackupWithSecretStorage(data)).rejects.toThrow( - "End-to-end encryption disabled", - ); - }); - - it("automatically calls the key back up", function () { - const groupSession = new Olm.OutboundGroupSession(); - groupSession.create(); - - // construct a fake decrypted key event via the use of a mocked - // 'crypto' implementation. - const event = new MatrixEvent({ - type: "m.room.encrypted", - }); - event.getWireType = () => "m.room.encrypted"; - event.getWireContent = () => { - return { - algorithm: "m.olm.v1.curve25519-aes-sha2", - }; - }; - const decryptedData = { - clearEvent: { - type: "m.room_key", - content: { - algorithm: "m.megolm.v1.aes-sha2", - room_id: ROOM_ID, - session_id: groupSession.session_id(), - session_key: groupSession.session_key(), - }, - }, - senderCurve25519Key: "SENDER_CURVE25519", - claimedEd25519Key: "SENDER_ED25519", - }; - - mockCrypto.decryptEvent = function () { - return Promise.resolve(decryptedData); - }; - mockCrypto.cancelRoomKeyRequest = function () {}; - - // @ts-ignore readonly field write - mockCrypto.backupManager = { - backupGroupSession: jest.fn(), - }; - - return event - .attemptDecryption(mockCrypto) - .then(() => { - return megolmDecryption.onRoomKeyEvent(event); - }) - .then(() => { - expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/unit/crypto/dehydration.spec.ts b/spec/unit/crypto/dehydration.spec.ts deleted file mode 100644 index 8df92e6314c..00000000000 --- a/spec/unit/crypto/dehydration.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 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 { TestClient } from "../../TestClient"; -import { logger } from "../../../src/logger"; -import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration"; - -const Olm = globalThis.Olm; - -describe("Dehydration", () => { - if (!globalThis.Olm) { - logger.warn("Not running dehydration unit tests: libolm not present"); - return; - } - - beforeAll(function () { - return globalThis.Olm.init(); - }); - - it("should rehydrate a dehydrated device", async () => { - const key = new Uint8Array([1, 2, 3]); - const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, { - cryptoCallbacks: { - getDehydrationKey: async (t) => key, - }, - }); - - const dehydratedDevice = new Olm.Account(); - dehydratedDevice.create(); - - alice.httpBackend.when("GET", "/dehydrated_device").respond(200, { - device_id: "ABCDEFG", - device_data: { - algorithm: DEHYDRATION_ALGORITHM, - account: dehydratedDevice.pickle(new Uint8Array(key)), - }, - }); - alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, { - success: true, - }); - - expect((await Promise.all([alice.client.rehydrateDevice(), alice.httpBackend.flushAllExpected()]))[0]).toEqual( - "ABCDEFG", - ); - - expect(alice.client.getDeviceId()).toEqual("ABCDEFG"); - }); -}); diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index c5ef3a6a2c6..fcc315b4574 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -728,7 +728,8 @@ describe("RoomWidgetClient", () => { expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); const payload = { type: "org.example.foo", hello: "world" }; - await client.encryptAndSendToDevices( + const embeddedClient = client as RoomWidgetClient; + await embeddedClient.encryptAndSendToDevices( [ { userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") }, { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") }, diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index fa261c9ce69..d5482ccfaf6 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -68,13 +68,11 @@ import { PolicyRecommendation, PolicyScope, } from "../../src/models/invites-ignorer"; -import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; import { defer, QueryDict } from "../../src/utils"; import { SyncState } from "../../src/sync"; import * as featureUtils from "../../src/feature"; import { StubStore } from "../../src/store/stub"; -import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage"; -import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; +import { ServerSideSecretStorageImpl } from "../../src/secret-storage"; import { KnownMembership } from "../../src/@types/membership"; import { RoomMessageEventContent } from "../../src/@types/events"; import { mockOpenIdConfiguration } from "../test-utils/oidc.ts"; @@ -1937,7 +1935,7 @@ describe("MatrixClient", function () { encryptEvent: jest.fn(), stop: jest.fn(), } as unknown as Mocked; - client.crypto = client["cryptoBackend"] = mockCrypto; + client["cryptoBackend"] = mockCrypto; }); function assertCancelled() { @@ -2323,21 +2321,6 @@ describe("MatrixClient", function () { }); }); - describe("encryptAndSendToDevices", () => { - it("throws an error if crypto is unavailable", () => { - client.crypto = undefined; - expect(() => client.encryptAndSendToDevices([], {})).toThrow(); - }); - - it("is an alias for the crypto method", async () => { - client.crypto = testUtils.mock(Crypto, "Crypto"); - const deviceInfos: IOlmDevice[] = []; - const payload = {}; - await client.encryptAndSendToDevices(deviceInfos, payload); - expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); - }); - }); - describe("support for ignoring invites", () => { beforeEach(() => { // Mockup `getAccountData`/`setAccountData`. @@ -3199,24 +3182,6 @@ describe("MatrixClient", function () { client["_secretStorage"] = mockSecretStorage; }); - it("hasSecretStorageKey", async () => { - mockSecretStorage.hasKey.mockResolvedValue(false); - expect(await client.hasSecretStorageKey("mykey")).toBe(false); - expect(mockSecretStorage.hasKey).toHaveBeenCalledWith("mykey"); - }); - - it("isSecretStored", async () => { - const mockResult = { key: {} as SecretStorageKeyDescriptionAesV1 }; - mockSecretStorage.isStored.mockResolvedValue(mockResult); - expect(await client.isSecretStored("mysecret")).toBe(mockResult); - expect(mockSecretStorage.isStored).toHaveBeenCalledWith("mysecret"); - }); - - it("getDefaultSecretStorageKeyId", async () => { - mockSecretStorage.getDefaultKeyId.mockResolvedValue("bzz"); - expect(await client.getDefaultSecretStorageKeyId()).toEqual("bzz"); - }); - it("isKeyBackupKeyStored", async () => { mockSecretStorage.isStored.mockResolvedValue(null); expect(await client.isKeyBackupKeyStored()).toBe(null); @@ -3224,60 +3189,6 @@ describe("MatrixClient", function () { }); }); - // these wrappers are deprecated, but we need coverage of them to pass the quality gate - describe("Crypto wrappers", () => { - describe("exception if no crypto", () => { - it("isCrossSigningReady", () => { - expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled"); - }); - - it("bootstrapCrossSigning", () => { - expect(() => client.bootstrapCrossSigning({})).toThrow("End-to-end encryption disabled"); - }); - - it("isSecretStorageReady", () => { - expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled"); - }); - }); - - describe("defer to crypto backend", () => { - let mockCryptoBackend: Mocked; - - beforeEach(() => { - mockCryptoBackend = { - isCrossSigningReady: jest.fn(), - bootstrapCrossSigning: jest.fn(), - isSecretStorageReady: jest.fn(), - stop: jest.fn().mockResolvedValue(undefined), - } as unknown as Mocked; - client["cryptoBackend"] = mockCryptoBackend; - }); - - it("isCrossSigningReady", async () => { - const testResult = "test"; - mockCryptoBackend.isCrossSigningReady.mockResolvedValue(testResult as unknown as boolean); - expect(await client.isCrossSigningReady()).toBe(testResult); - expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1); - }); - - it("bootstrapCrossSigning", async () => { - const testOpts = {}; - mockCryptoBackend.bootstrapCrossSigning.mockResolvedValue(undefined); - await client.bootstrapCrossSigning(testOpts); - expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledTimes(1); - expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledWith(testOpts); - }); - - it("isSecretStorageReady", async () => { - client["cryptoBackend"] = mockCryptoBackend; - const testResult = "test"; - mockCryptoBackend.isSecretStorageReady.mockResolvedValue(testResult as unknown as boolean); - expect(await client.isSecretStorageReady()).toBe(testResult); - expect(mockCryptoBackend.isSecretStorageReady).toHaveBeenCalledTimes(1); - }); - }); - }); - describe("paginateEventTimeline()", () => { describe("notifications timeline", () => { const unsafeNotification = { diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 9a31e492f3c..c3d08f9a359 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3774,7 +3774,7 @@ describe("Room", function () { it("should load pending events from from the store and decrypt if needed", async () => { const client = new TestClient(userA).client; - client.crypto = client["cryptoBackend"] = { + client["cryptoBackend"] = { decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }), } as unknown as Crypto; client.store.getPendingEvents = jest.fn(async (roomId) => [ diff --git a/src/client.ts b/src/client.ts index 7447b7f415e..8b4f35f0f6c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,7 +20,7 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import type { IDeviceKeys, IMegolmSessionData, IOneTimeKey } from "./@types/crypto.ts"; +import type { IDeviceKeys, IOneTimeKey } from "./@types/crypto.ts"; import { ISyncStateData, SetPresence, SyncApi, SyncApiOptions, SyncState } from "./sync.ts"; import { EventStatus, @@ -46,11 +46,8 @@ import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./u import { Direction, EventTimeline } from "./models/event-timeline.ts"; import { IActionsObject, PushProcessor } from "./pushprocessor.ts"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery.ts"; -import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts"; -import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice.ts"; -import { IOlmDevice } from "./crypto/algorithms/megolm.ts"; +import { encodeUnpaddedBase64Url } from "./base64.ts"; import { TypedReEmitter } from "./ReEmitter.ts"; -import { IRoomEncryption } from "./crypto/RoomList.ts"; import { logger, Logger } from "./logger.ts"; import { SERVICE_TYPES } from "./service-types.ts"; import { @@ -73,39 +70,16 @@ import { UploadOpts, UploadResponse, } from "./http-api/index.ts"; -import { - Crypto, - CryptoEvent as LegacyCryptoEvent, - CryptoEventHandlerMap as LegacyCryptoEventHandlerMap, - fixBackupKey, - ICheckOwnCrossSigningTrustOpts, - IRoomKeyRequestBody, - isCryptoAvailable, -} from "./crypto/index.ts"; -import { DeviceInfo } from "./crypto/deviceinfo.ts"; import { User, UserEvent, UserEventHandlerMap } from "./models/user.ts"; import { getHttpUriForMxc } from "./content-repo.ts"; import { SearchResult } from "./models/search-result.ts"; -import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration.ts"; -import { - IKeyBackupInfo, - IKeyBackupPrepareOpts, - IKeyBackupRestoreOpts, - IKeyBackupRestoreResult, - IKeyBackupRoomSessions, - IKeyBackupSession, -} from "./crypto/keybackup.ts"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider.ts"; import { MatrixScheduler } from "./scheduler.ts"; import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon.ts"; import { AuthDict } from "./interactive-auth.ts"; import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator.ts"; -import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./crypto/api.ts"; import { EventTimelineSet } from "./models/event-timeline-set.ts"; -import { VerificationRequest } from "./crypto/verification/request/VerificationRequest.ts"; -import { VerificationBase as Verification } from "./crypto/verification/Base.ts"; import * as ContentHelpers from "./content-helpers.ts"; -import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning.ts"; import { NotificationCountType, Room, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room.ts"; import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member.ts"; import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state.ts"; @@ -161,11 +135,9 @@ import { } from "./@types/partials.ts"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts"; import { secureRandomString } from "./randomstring.ts"; -import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup.ts"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace.ts"; import { ISignatures } from "./@types/signed.ts"; import { IStore } from "./store/index.ts"; -import { ISecretRequest } from "./crypto/SecretStorage.ts"; import { IEventWithRoomId, ISearchRequestBody, @@ -187,7 +159,7 @@ import { RuleId, } from "./@types/PushRules.ts"; import { IThreepid } from "./@types/threepids.ts"; -import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base.ts"; +import { CryptoStore } from "./crypto/store/base.ts"; import { GroupCall, GroupCallIntent, GroupCallType, IGroupCallDataChannelOptions } from "./webrtc/groupCall.ts"; import { MediaHandler } from "./webrtc/mediaHandler.ts"; import { @@ -218,26 +190,16 @@ import { IgnoredInvites } from "./models/invites-ignorer.ts"; import { UIARequest, UIAResponse } from "./@types/uia.ts"; import { LocalNotificationSettings } from "./@types/local_notifications.ts"; import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature.ts"; -import { BackupDecryptor, CryptoBackend } from "./common-crypto/CryptoBackend.ts"; +import { CryptoBackend } from "./common-crypto/CryptoBackend.ts"; import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants.ts"; import { - BootstrapCrossSigningOpts, CrossSigningKeyInfo, CryptoApi, - decodeRecoveryKey, - ImportRoomKeysOpts, CryptoEvent, CryptoEventHandlerMap, CryptoCallbacks, } from "./crypto-api/index.ts"; -import { DeviceInfoMap } from "./crypto/DeviceList.ts"; -import { - AddSecretStorageKeyOpts, - SecretStorageKey, - SecretStorageKeyDescription, - ServerSideSecretStorage, - ServerSideSecretStorageImpl, -} from "./secret-storage.ts"; +import { SecretStorageKeyDescription, ServerSideSecretStorage, ServerSideSecretStorageImpl } from "./secret-storage.ts"; import { RegisterRequest, RegisterResponse } from "./@types/registration.ts"; import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager.ts"; import { getRelationsThreadFilter } from "./thread-utils.ts"; @@ -246,7 +208,6 @@ import { RoomMessageEventContent, StickerEventContent } from "./@types/events.ts import { ImageInfo } from "./@types/media.ts"; import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts"; import { sha256 } from "./digest.ts"; -import { keyFromAuthData } from "./common-crypto/key-passphrase.ts"; import { discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig, validateAuthMetadataAndKeys } from "./oidc/index.ts"; export type Store = IStore; @@ -254,10 +215,7 @@ export type Store = IStore; export type ResetTimelineCallback = (roomId: string) => boolean; const SCROLLBACK_DELAY_MS = 3000; -/** - * @deprecated Not supported for Rust Cryptography. - */ -export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); + const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue( @@ -265,12 +223,6 @@ export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue( "org.matrix.msc3852.last_seen_user_agent", ); -interface IExportedDevice { - olmDevice: IExportedOlmDevice; - userId: string; - deviceId: string; -} - export interface IKeysUploadResponse { one_time_key_counts: { // eslint-disable-line camelcase @@ -378,15 +330,6 @@ export interface ICreateClientOpts { */ queryParams?: Record; - /** - * Device data exported with - * "exportDevice" method that must be imported to recreate this device. - * Should only be useful for devices with end-to-end crypto enabled. - * If provided, deviceId and userId should **NOT** be provided at the top - * level (they are present in the exported data). - */ - deviceToImport?: IExportedDevice; - /** * Encryption key used for encrypting sensitive data (such as e2ee keys) in {@link ICreateClientOpts#cryptoStore}. * @@ -884,14 +827,6 @@ export interface RoomSummary extends Omit; -} - interface IRoomHierarchy { rooms: IHierarchyRoom[]; next_batch?: string; @@ -950,24 +885,6 @@ type RoomStateEvents = | RoomStateEvent.Update | RoomStateEvent.Marker; -type LegacyCryptoEvents = - | LegacyCryptoEvent.KeySignatureUploadFailure - | LegacyCryptoEvent.KeyBackupStatus - | LegacyCryptoEvent.KeyBackupFailed - | LegacyCryptoEvent.KeyBackupSessionsRemaining - | LegacyCryptoEvent.KeyBackupDecryptionKeyCached - | LegacyCryptoEvent.RoomKeyRequest - | LegacyCryptoEvent.RoomKeyRequestCancellation - | LegacyCryptoEvent.VerificationRequest - | LegacyCryptoEvent.VerificationRequestReceived - | LegacyCryptoEvent.DeviceVerificationChanged - | LegacyCryptoEvent.UserTrustStatusChanged - | LegacyCryptoEvent.KeysChanged - | LegacyCryptoEvent.Warning - | LegacyCryptoEvent.DevicesUpdated - | LegacyCryptoEvent.WillUpdateDevices - | LegacyCryptoEvent.LegacyCryptoStoreMigrationProgress; - type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent]; type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange; @@ -989,7 +906,6 @@ export type EmittedEvents = | ClientEvent | RoomEvents | RoomStateEvents - | LegacyCryptoEvents | CryptoEvents | MatrixEventEvents | RoomMemberEvents @@ -1201,7 +1117,6 @@ export type ClientEventHandlerMap = { [ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void; } & RoomEventHandlerMap & RoomStateEventHandlerMap & - LegacyCryptoEventHandlerMap & CryptoEventHandlerMap & MatrixEventHandlerMap & RoomMemberEventHandlerMap & @@ -1235,12 +1150,9 @@ export class MatrixClient extends TypedEventEmitter; // XXX: Intended private, used in code. - /** - * The libolm crypto implementation, if it is in use. - * - * @deprecated This should not be used. Instead, use the methods exposed directly on this class or - * (where they are available) via {@link getCrypto}. - */ - public crypto?: Crypto; // XXX: Intended private, used in code. Being replaced by cryptoBackend - private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code. public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code. @@ -1278,8 +1182,12 @@ export class MatrixClient extends TypedEventEmitter; errorTs?: number } } = {}; protected notifTimelineSet: EventTimelineSet | null = null; - /* @deprecated */ - protected cryptoStore?: CryptoStore; + + /** + * Legacy crypto store used for migration from the legacy crypto to the rust crypto + * @private + */ + private readonly legacyCryptoStore?: CryptoStore; protected verificationMethods?: string[]; protected fallbackICEServerAllowed = false; protected syncApi?: SlidingSyncSdk | SyncApi; @@ -1305,7 +1213,6 @@ export class MatrixClient extends TypedEventEmitter; - protected exportedOlmDeviceToImport?: IExportedOlmDevice; protected txnCtr = 0; protected mediaHandler = new MediaHandler(this); protected sessionId: string; @@ -1367,27 +1274,8 @@ export class MatrixClient extends TypedEventEmitter { - if (this.crypto) { - throw new Error("Cannot rehydrate device after crypto is initialized"); - } - - if (!this.cryptoCallbacks.getDehydrationKey) { - return; - } - - const getDeviceResult = await this.getDehydratedDevice(); - if (!getDeviceResult) { - return; - } - - if (!getDeviceResult.device_data || !getDeviceResult.device_id) { - this.logger.info("no dehydrated device found"); - return; - } - - const account = new globalThis.Olm.Account(); - try { - const deviceData = getDeviceResult.device_data; - if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { - this.logger.warn("Wrong algorithm for dehydrated device"); - return; - } - this.logger.debug("unpickling dehydrated device"); - const key = await this.cryptoCallbacks.getDehydrationKey(deviceData, (k) => { - // copy the key so that it doesn't get clobbered - account.unpickle(new Uint8Array(k), deviceData.account); - }); - account.unpickle(key, deviceData.account); - this.logger.debug("unpickled device"); - - const rehydrateResult = await this.http.authedRequest<{ success: boolean }>( - Method.Post, - "/dehydrated_device/claim", - undefined, - { - device_id: getDeviceResult.device_id, - }, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", - }, - ); - - if (rehydrateResult.success) { - this.deviceId = getDeviceResult.device_id; - this.logger.info("using dehydrated device"); - const pickleKey = this.pickleKey || "DEFAULT_KEY"; - this.exportedOlmDeviceToImport = { - pickledAccount: account.pickle(pickleKey), - sessions: [], - pickleKey: pickleKey, - }; - account.free(); - return this.deviceId; - } else { - account.free(); - this.logger.info("not using dehydrated device"); - return; - } - } catch (e) { - account.free(); - this.logger.warn("could not unpickle", e); - } - } - - /** - * Get the current dehydrated device, if any - * @returns A promise of an object containing the dehydrated device - * - * @deprecated MSC2697 device dehydration is not supported for rust cryptography. - */ - public async getDehydratedDevice(): Promise { - try { - return await this.http.authedRequest( - Method.Get, - "/dehydrated_device", - undefined, - undefined, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", - }, - ); - } catch (e) { - this.logger.info("could not get dehydrated device", e); - return; - } - } - - /** - * Set the dehydration key. This will also periodically dehydrate devices to - * the server. - * - * @param key - the dehydration key - * @param keyInfo - Information about the key. Primarily for - * information about how to generate the key from a passphrase. - * @param deviceDisplayName - The device display name for the - * dehydrated device. - * @returns A promise that resolves when the dehydrated device is stored. - * - * @deprecated Not supported for Rust Cryptography. - */ - public async setDehydrationKey( - key: Uint8Array, - keyInfo: IDehydratedDeviceKeyInfo, - deviceDisplayName?: string, - ): Promise { - if (!this.crypto) { - this.logger.warn("not dehydrating device if crypto is not enabled"); - return; - } - return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName); - } - - /** - * Creates a new MSC2967 dehydrated device (without queuing periodic dehydration) - * @param key - the dehydration key - * @param keyInfo - Information about the key. Primarily for - * information about how to generate the key from a passphrase. - * @param deviceDisplayName - The device display name for the - * dehydrated device. - * @returns the device id of the newly created dehydrated device - * - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.startDehydration}. - */ - public async createDehydratedDevice( - key: Uint8Array, - keyInfo: IDehydratedDeviceKeyInfo, - deviceDisplayName?: string, - ): Promise { - if (!this.crypto) { - this.logger.warn("not dehydrating device if crypto is not enabled"); - return; - } - await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName); - return this.crypto.dehydrationManager.dehydrateDevice(); - } - - /** @deprecated Not supported for Rust Cryptography. */ - public async exportDevice(): Promise { - if (!this.crypto) { - this.logger.warn("not exporting device if crypto is not enabled"); - return; - } - return { - userId: this.credentials.userId!, - deviceId: this.deviceId!, - // XXX: Private member access. - olmDevice: await this.crypto.olmDevice.export(), - }; - } - /** * Clear any data out of the persistent stores used by the client. * @@ -1782,8 +1505,8 @@ export class MatrixClient extends TypedEventEmitter[] = []; promises.push(this.store.deleteAllData()); - if (this.cryptoStore) { - promises.push(this.cryptoStore.deleteAllData()); + if (this.legacyCryptoStore) { + promises.push(this.legacyCryptoStore.deleteAllData()); } // delete the stores used by the rust matrix-sdk-crypto, in case they were used @@ -2069,1992 +1792,231 @@ export class MatrixClient extends TypedEventEmitterexplicitly attempts to retry their lost connection. - * Will also retry any outbound to-device messages currently in the queue to be sent - * (retries of regular outgoing events are handled separately, per-event). - * @returns True if this resulted in a request being retried. - */ - public retryImmediately(): boolean { - // don't await for this promise: we just want to kick it off - this.toDeviceMessageQueue.sendQueue(); - return this.syncApi?.retryImmediately() ?? false; - } - - /** - * Return the global notification EventTimelineSet, if any - * - * @returns the globl notification EventTimelineSet - */ - public getNotifTimelineSet(): EventTimelineSet | null { - return this.notifTimelineSet; - } - - /** - * Set the global notification EventTimelineSet - * - */ - public setNotifTimelineSet(set: EventTimelineSet): void { - this.notifTimelineSet = set; - } - - /** - * Gets the cached capabilities of the homeserver, returning cached ones if available. - * If there are no cached capabilities and none can be fetched, throw an exception. - * - * @returns Promise resolving with The capabilities of the homeserver - */ - public async getCapabilities(): Promise { - const caps = this.serverCapabilitiesService.getCachedCapabilities(); - if (caps) return caps; - return this.serverCapabilitiesService.fetchCapabilities(); - } - - /** - * Gets the cached capabilities of the homeserver. If none have been fetched yet, - * return undefined. - * - * @returns The capabilities of the homeserver - */ - public getCachedCapabilities(): Capabilities | undefined { - return this.serverCapabilitiesService.getCachedCapabilities(); - } - - /** - * Fetches the latest capabilities from the homeserver, ignoring any cached - * versions. The newly returned version is cached. - * - * @returns A promise which resolves to the capabilities of the homeserver - */ - public fetchCapabilities(): Promise { - return this.serverCapabilitiesService.fetchCapabilities(); - } - - /** - * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto. - * - * **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to - * the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for - * ensuring that only one `MatrixClient` issue is instantiated at a time. - * - * @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'. - * @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly - * 32 bytes of data, and must be the same each time the client is initialised for a given device. - * If both this and `storagePassword` are unspecified, the store will be unencrypted. - * @param args.storagePassword - An alternative to `storageKey`. A password which will be used to derive a key to - * encrypt the store with. Deriving a key from a password is (deliberately) a slow operation, so prefer - * to pass a `storageKey` directly where possible. - * - * @returns a Promise which will resolve when the crypto layer has been - * successfully initialised. - */ - public async initRustCrypto( - args: { - useIndexedDB?: boolean; - storageKey?: Uint8Array; - storagePassword?: string; - } = {}, - ): Promise { - if (this.cryptoBackend) { - this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); - return; - } - - const userId = this.getUserId(); - if (userId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown userId: ` + - `ensure userId is passed in createClient().`, - ); - } - const deviceId = this.getDeviceId(); - if (deviceId === null) { - throw new Error( - `Cannot enable encryption on MatrixClient with unknown deviceId: ` + - `ensure deviceId is passed in createClient().`, - ); - } - - // importing rust-crypto will download the webassembly, so we delay it until we know it will be - // needed. - this.logger.debug("Downloading Rust crypto library"); - const RustCrypto = await import("./rust-crypto/index.ts"); - - const rustCrypto = await RustCrypto.initRustCrypto({ - logger: this.logger, - http: this.http, - userId: userId, - deviceId: deviceId, - secretStorage: this.secretStorage, - cryptoCallbacks: this.cryptoCallbacks, - storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX, - storeKey: args.storageKey, - storePassphrase: args.storagePassword, - - legacyCryptoStore: this.cryptoStore, - legacyPickleKey: this.pickleKey ?? "DEFAULT_KEY", - legacyMigrationProgressListener: (progress: number, total: number): void => { - this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total); - }, - }); - - rustCrypto.setSupportedVerificationMethods(this.verificationMethods); - - this.cryptoBackend = rustCrypto; - - // attach the event listeners needed by RustCrypto - this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); - this.on(ClientEvent.Event, (event) => { - rustCrypto.onLiveEventFromSync(event); - }); - - // re-emit the events emitted by the crypto impl - this.reEmitter.reEmit(rustCrypto, [ - CryptoEvent.VerificationRequestReceived, - CryptoEvent.UserTrustStatusChanged, - CryptoEvent.KeyBackupStatus, - CryptoEvent.KeyBackupSessionsRemaining, - CryptoEvent.KeyBackupFailed, - CryptoEvent.KeyBackupDecryptionKeyCached, - CryptoEvent.KeysChanged, - CryptoEvent.DevicesUpdated, - CryptoEvent.WillUpdateDevices, - ]); - } - - /** - * Access the server-side secret storage API for this client. - */ - public get secretStorage(): ServerSideSecretStorage { - return this._secretStorage; - } - - /** - * Access the crypto API for this client. - * - * If end-to-end encryption has been enabled for this client (via {@link initRustCrypto}), - * returns an object giving access to the crypto API. Otherwise, returns `undefined`. - */ - public getCrypto(): CryptoApi | undefined { - return this.cryptoBackend; - } - - /** - * Is end-to-end crypto enabled for this client. - * @returns True if end-to-end is enabled. - * @deprecated prefer {@link getCrypto} - */ - public isCryptoEnabled(): boolean { - return !!this.cryptoBackend; - } - - /** - * Get the Ed25519 key for this device - * - * @returns base64-encoded ed25519 key. Null if crypto is - * disabled. - * - * @deprecated Not supported for Rust Cryptography.Prefer {@link CryptoApi.getOwnDeviceKeys} - */ - public getDeviceEd25519Key(): string | null { - return this.crypto?.getDeviceEd25519Key() ?? null; - } - - /** - * Get the Curve25519 key for this device - * - * @returns base64-encoded curve25519 key. Null if crypto is - * disabled. - * - * @deprecated Not supported for Rust Cryptography. Use {@link CryptoApi.getOwnDeviceKeys} - */ - public getDeviceCurve25519Key(): string | null { - return this.crypto?.getDeviceCurve25519Key() ?? null; - } - - /** - * @deprecated Does nothing. - */ - public async uploadKeys(): Promise { - this.logger.warn("MatrixClient.uploadKeys is deprecated"); - } - - /** - * Download the keys for a list of users and stores the keys in the session - * store. - * @param userIds - The users to fetch. - * @param forceDownload - Always download the keys even if cached. - * - * @returns A promise which resolves to a map userId-\>deviceId-\>`DeviceInfo` - * - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo} - */ - public downloadKeys(userIds: string[], forceDownload?: boolean): Promise { - if (!this.crypto) { - return Promise.reject(new Error("End-to-end encryption disabled")); - } - return this.crypto.downloadKeys(userIds, forceDownload); - } - - /** - * Get the stored device keys for a user id - * - * @param userId - the user to list keys for. - * - * @returns list of devices - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo} - */ - public getStoredDevicesForUser(userId: string): DeviceInfo[] { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.getStoredDevicesForUser(userId) || []; - } - - /** - * Get the stored device key for a user id and device id - * - * @param userId - the user to list keys for. - * @param deviceId - unique identifier for the device - * - * @returns device or null - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo} - */ - public getStoredDevice(userId: string, deviceId: string): DeviceInfo | null { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.getStoredDevice(userId, deviceId) || null; - } - - /** - * Mark the given device as verified - * - * @param userId - owner of the device - * @param deviceId - unique identifier for the device or user's - * cross-signing public key ID. - * - * @param verified - whether to mark the device as verified. defaults - * to 'true'. - * - * @returns - * - * @remarks - * Fires {@link CryptoEvent#DeviceVerificationChanged} - * - * @deprecated Not supported for Rust Cryptography. - */ - public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise { - const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); - - // if one of the user's own devices is being marked as verified / unverified, - // check the key backup status, since whether or not we use this depends on - // whether it has a signature from a verified device - if (userId == this.credentials.userId) { - this.checkKeyBackup(); - } - return prom; - } - - /** - * Mark the given device as blocked/unblocked - * - * @param userId - owner of the device - * @param deviceId - unique identifier for the device or user's - * cross-signing public key ID. - * - * @param blocked - whether to mark the device as blocked. defaults - * to 'true'. - * - * @returns - * - * @remarks - * Fires {@link LegacyCryptoEvent.DeviceVerificationChanged} - * - * @deprecated Not supported for Rust Cryptography. - */ - public setDeviceBlocked(userId: string, deviceId: string, blocked = true): Promise { - return this.setDeviceVerification(userId, deviceId, null, blocked, null); - } - - /** - * Mark the given device as known/unknown - * - * @param userId - owner of the device - * @param deviceId - unique identifier for the device or user's - * cross-signing public key ID. - * - * @param known - whether to mark the device as known. defaults - * to 'true'. - * - * @returns - * - * @remarks - * Fires {@link CryptoEvent#DeviceVerificationChanged} - * - * @deprecated Not supported for Rust Cryptography. - */ - public setDeviceKnown(userId: string, deviceId: string, known = true): Promise { - return this.setDeviceVerification(userId, deviceId, null, null, known); - } - - private async setDeviceVerification( - userId: string, - deviceId: string, - verified?: boolean | null, - blocked?: boolean | null, - known?: boolean | null, - ): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - await this.crypto.setDeviceVerification(userId, deviceId, verified, blocked, known); - } - - /** - * Request a key verification from another user, using a DM. - * - * @param userId - the user to request verification with - * @param roomId - the room to use for verification - * - * @returns resolves to a VerificationRequest - * when the request has been sent to the other party. - * - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.requestVerificationDM}. - */ - public requestVerificationDM(userId: string, roomId: string): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.requestVerificationDM(userId, roomId); - } - - /** - * Finds a DM verification request that is already in progress for the given room id - * - * @param roomId - the room to use for verification - * - * @returns the VerificationRequest that is in progress, if any - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.findVerificationRequestDMInProgress}. - */ - public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } else if (!this.crypto) { - // Hack for element-R to avoid breaking the cypress tests. We can get rid of this once the react-sdk is - // updated to use CryptoApi.findVerificationRequestDMInProgress. - return undefined; - } - return this.crypto.findVerificationRequestDMInProgress(roomId); - } - - /** - * Returns all to-device verification requests that are already in progress for the given user id - * - * @param userId - the ID of the user to query - * - * @returns the VerificationRequests that are in progress - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getVerificationRequestsToDeviceInProgress}. - */ - public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.getVerificationRequestsToDeviceInProgress(userId); - } - - /** - * Request a key verification from another user. - * - * @param userId - the user to request verification with - * @param devices - array of device IDs to send requests to. Defaults to - * all devices owned by the user - * - * @returns resolves to a VerificationRequest - * when the request has been sent to the other party. - * - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}. - */ - public requestVerification(userId: string, devices?: string[]): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.requestVerification(userId, devices); - } - - /** - * Begin a key verification. - * - * @param method - the verification method to use - * @param userId - the user to verify keys with - * @param deviceId - the device to verify - * - * @returns a verification object - * @deprecated Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}. - */ - public beginKeyVerification(method: string, userId: string, deviceId: string): Verification { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.beginKeyVerification(method, userId, deviceId); - } - - /** - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#checkKey}. - */ - public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise { - return this.secretStorage.checkKey(key, info); - } - - /** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. This provides the default for rooms which - * do not specify a value. - * - * @param value - whether to blacklist all unverified devices by default - * - * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}: - * - * ```javascript - * client.getCrypto().globalBlacklistUnverifiedDevices = value; - * ``` - */ - public setGlobalBlacklistUnverifiedDevices(value: boolean): boolean { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - this.cryptoBackend.globalBlacklistUnverifiedDevices = value; - return value; - } - - /** - * @returns whether to blacklist all unverified devices by default - * - * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}: - * - * ```javascript - * value = client.getCrypto().globalBlacklistUnverifiedDevices; - * ``` - */ - public getGlobalBlacklistUnverifiedDevices(): boolean { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.globalBlacklistUnverifiedDevices; - } - - /** - * Set whether sendMessage in a room with unknown and unverified devices - * should throw an error and not send them message. This has 'Global' for - * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently - * no room-level equivalent for this setting. - * - * This API is currently UNSTABLE and may change or be removed without notice. - * - * It has no effect with the Rust crypto implementation. - * - * @param value - whether error on unknown devices - * - * ```ts - * client.getCrypto().globalErrorOnUnknownDevices = value; - * ``` - */ - public setGlobalErrorOnUnknownDevices(value: boolean): void { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - this.cryptoBackend.globalErrorOnUnknownDevices = value; - } - - /** - * @returns whether to error on unknown devices - * - * This API is currently UNSTABLE and may change or be removed without notice. - */ - public getGlobalErrorOnUnknownDevices(): boolean { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.globalErrorOnUnknownDevices; - } - - /** - * Get the ID of one of the user's cross-signing keys - * - * @param type - The type of key to get the ID of. One of - * "master", "self_signing", or "user_signing". Defaults to "master". - * - * @returns the key ID - * @deprecated Not supported for Rust Cryptography. prefer {@link Crypto.CryptoApi#getCrossSigningKeyId} - */ - public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string | null { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.getCrossSigningId(type); - } - - /** - * Get the cross signing information for a given user. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param userId - the user ID to get the cross-signing info for. - * - * @returns the cross signing information for the user. - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#userHasCrossSigningKeys} - */ - public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.getStoredCrossSigningForUser(userId); - } - - /** - * Check whether a given user is trusted. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param userId - The ID of the user to check. - * - * @deprecated Use {@link Crypto.CryptoApi.getUserVerificationStatus | `CryptoApi.getUserVerificationStatus`} - */ - public checkUserTrust(userId: string): UserTrustLevel { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.checkUserTrust(userId); - } - - /** - * Check whether a given device is trusted. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param userId - The ID of the user whose devices is to be checked. - * @param deviceId - The ID of the device to check - * - * @deprecated Use {@link Crypto.CryptoApi.getDeviceVerificationStatus | `CryptoApi.getDeviceVerificationStatus`} - */ - public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.checkDeviceTrust(userId, deviceId); - } - - /** - * Check whether one of our own devices is cross-signed by our - * user's stored keys, regardless of whether we trust those keys yet. - * - * @param deviceId - The ID of the device to check - * - * @returns true if the device is cross-signed - * - * @deprecated Not supported for Rust Cryptography. - */ - public checkIfOwnDeviceCrossSigned(deviceId: string): boolean { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.checkIfOwnDeviceCrossSigned(deviceId); - } - - /** - * Check the copy of our cross-signing key that we have in the device list and - * see if we can get the private key. If so, mark it as trusted. - * @param opts - ICheckOwnCrossSigningTrustOpts object - * - * @deprecated Unneeded for the new crypto - */ - public checkOwnCrossSigningTrust(opts?: ICheckOwnCrossSigningTrustOpts): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.checkOwnCrossSigningTrust(opts); - } - - /** - * Checks that a given cross-signing private key matches a given public key. - * This can be used by the getCrossSigningKey callback to verify that the - * private key it is about to supply is the one that was requested. - * @param privateKey - The private key - * @param expectedPublicKey - The public key - * @returns true if the key matches, otherwise false - * - * @deprecated Not supported for Rust Cryptography. - */ - public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey); - } - - /** - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#requestDeviceVerification}. - */ - public legacyDeviceVerification(userId: string, deviceId: string, method: string): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.legacyDeviceVerification(userId, deviceId, method); - } - - /** - * Perform any background tasks that can be done before a message is ready to - * send, in order to speed up sending of the message. - * @param room - the room the event is in - * - * @deprecated Prefer {@link CryptoApi.prepareToEncrypt | `CryptoApi.prepareToEncrypt`}: - * - * ```javascript - * client.getCrypto().prepareToEncrypt(room); - * ``` - */ - public prepareToEncrypt(room: Room): void { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - this.cryptoBackend.prepareToEncrypt(room); - } - - /** - * Checks if the user has previously published cross-signing keys - * - * This means downloading the devicelist for the user and checking if the list includes - * the cross-signing pseudo-device. - * - * @deprecated Prefer {@link CryptoApi.userHasCrossSigningKeys | `CryptoApi.userHasCrossSigningKeys`}: - * - * ```javascript - * result = client.getCrypto().userHasCrossSigningKeys(); - * ``` - */ - public userHasCrossSigningKeys(): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.userHasCrossSigningKeys(); - } - - /** - * Checks whether cross signing: - * - is enabled on this account and trusted by this device - * - has private keys either cached locally or stored in secret storage - * - * If this function returns false, bootstrapCrossSigning() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapCrossSigning() completes successfully, this function should - * return true. - * @returns True if cross-signing is ready to be used on this device - * @deprecated Prefer {@link CryptoApi.isCrossSigningReady | `CryptoApi.isCrossSigningReady`}: - */ - public isCrossSigningReady(): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.isCrossSigningReady(); - } - - /** - * Bootstrap cross-signing by creating keys if needed. If everything is already - * set up, then no changes are made, so this is safe to run to ensure - * cross-signing is ready for use. - * - * This function: - * - creates new cross-signing keys if they are not found locally cached nor in - * secret storage (if it has been set up) - * - * @deprecated Prefer {@link CryptoApi.bootstrapCrossSigning | `CryptoApi.bootstrapCrossSigning`}. - */ - public bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.bootstrapCrossSigning(opts); - } - - /** - * Whether to trust a others users signatures of their devices. - * If false, devices will only be considered 'verified' if we have - * verified that device individually (effectively disabling cross-signing). - * - * Default: true - * - * @returns True if trusting cross-signed devices - * - * @deprecated Prefer {@link CryptoApi.getTrustCrossSignedDevices | `CryptoApi.getTrustCrossSignedDevices`}. - */ - public getCryptoTrustCrossSignedDevices(): boolean { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.getTrustCrossSignedDevices(); - } - - /** - * See getCryptoTrustCrossSignedDevices - * - * @param val - True to trust cross-signed devices - * - * @deprecated Prefer {@link CryptoApi.setTrustCrossSignedDevices | `CryptoApi.setTrustCrossSignedDevices`}. - */ - public setCryptoTrustCrossSignedDevices(val: boolean): void { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - this.cryptoBackend.setTrustCrossSignedDevices(val); - } - - /** - * Counts the number of end to end session keys that are waiting to be backed up - * @returns Promise which resolves to the number of sessions requiring backup - * - * @deprecated Not supported for Rust Cryptography. - */ - public countSessionsNeedingBackup(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.countSessionsNeedingBackup(); - } - - /** - * Get information about the encryption of an event - * - * @param event - event to be checked - * @returns The event information. - * @deprecated Prefer {@link Crypto.CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}. - */ - public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.getEventEncryptionInfo(event); - } - - /** - * Create a recovery key from a user-supplied passphrase. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param password - Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * @returns Object with public key metadata, encoded private - * recovery key which should be disposed of after displaying to the user, - * and raw private key to avoid round tripping if needed. - * - * @deprecated Prefer {@link CryptoApi.createRecoveryKeyFromPassphrase | `CryptoApi.createRecoveryKeyFromPassphrase`}. - */ - public createRecoveryKeyFromPassphrase(password?: string): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.createRecoveryKeyFromPassphrase(password); - } - - /** - * Checks whether secret storage: - * - is enabled on this account - * - is storing cross-signing private keys - * - is storing session backup key (if enabled) - * - * If this function returns false, bootstrapSecretStorage() can be used - * to fix things such that it returns true. That is to say, after - * bootstrapSecretStorage() completes successfully, this function should - * return true. - * - * @returns True if secret storage is ready to be used on this device - * @deprecated Prefer {@link CryptoApi.isSecretStorageReady | `CryptoApi.isSecretStorageReady`}. - */ - public isSecretStorageReady(): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.isSecretStorageReady(); - } - - /** - * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is - * already set up, then no changes are made, so this is safe to run to ensure secret - * storage is ready for use. - * - * This function - * - creates a new Secure Secret Storage key if no default key exists - * - if a key backup exists, it is migrated to store the key in the Secret - * Storage - * - creates a backup if none exists, and one is requested - * - migrates Secure Secret Storage to use the latest algorithm, if an outdated - * algorithm is found - * - * @deprecated Use {@link CryptoApi.bootstrapSecretStorage | `CryptoApi.bootstrapSecretStorage`}. - */ - public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.bootstrapSecretStorage(opts); - } - - /** - * Add a key for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param algorithm - the algorithm used by the key - * @param opts - the options for the algorithm. The properties used - * depend on the algorithm given. - * @param keyName - the name of the key. If not given, a random name will be generated. - * - * @returns An object with: - * keyId: the ID of the key - * keyInfo: details about the key (iv, mac, passphrase) - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#addKey}. - */ - public addSecretStorageKey( - algorithm: string, - opts: AddSecretStorageKeyOpts, - keyName?: string, - ): Promise<{ keyId: string; keyInfo: SecretStorageKeyDescription }> { - return this.secretStorage.addKey(algorithm, opts, keyName); - } - - /** - * Check whether we have a key with a given ID. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param keyId - The ID of the key to check - * for. Defaults to the default key ID if not provided. - * @returns Whether we have the key. - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#hasKey}. - */ - public hasSecretStorageKey(keyId?: string): Promise { - return this.secretStorage.hasKey(keyId); - } - - /** - * Store an encrypted secret on the server. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param name - The name of the secret - * @param secret - The secret contents. - * @param keys - The IDs of the keys to use to encrypt the secret or null/undefined - * to use the default (will throw if no default key is set). - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}. - */ - public storeSecret(name: string, secret: string, keys?: string[]): Promise { - return this.secretStorage.store(name, secret, keys); - } - - /** - * Get a secret from storage. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param name - the name of the secret - * - * @returns the contents of the secret - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}. - */ - public getSecret(name: string): Promise { - return this.secretStorage.get(name); - } - - /** - * Check if a secret is stored on the server. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param name - the name of the secret - * @returns map of key name to key info the secret is encrypted - * with, or null if it is not present or not encrypted with a trusted - * key - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}. - */ - public isSecretStored(name: SecretStorageKey): Promise | null> { - return this.secretStorage.isStored(name); - } - - /** - * Request a secret from another device. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param name - the name of the secret to request - * @param devices - the devices to request the secret from - * - * @returns the secret request object - * @deprecated Not supported for Rust Cryptography. - */ - public requestSecret(name: string, devices: string[]): ISecretRequest { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.requestSecret(name, devices); - } - - /** - * Get the current default key ID for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @returns The default key ID or null if no default key ID is set - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#getDefaultKeyId}. - */ - public getDefaultSecretStorageKeyId(): Promise { - return this.secretStorage.getDefaultKeyId(); - } - - /** - * Set the current default key ID for encrypting secrets. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param keyId - The new default key ID - * - * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#setDefaultKeyId}. - */ - public setDefaultSecretStorageKeyId(keyId: string): Promise { - return this.secretStorage.setDefaultKeyId(keyId); - } - - /** - * Checks that a given secret storage private key matches a given public key. - * This can be used by the getSecretStorageKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @param privateKey - The private key - * @param expectedPublicKey - The public key - * @returns true if the key matches, otherwise false - * - * @deprecated The use of asymmetric keys for SSSS is deprecated. - * Use {@link SecretStorage.ServerSideSecretStorage#checkKey} for symmetric keys. - */ - public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.checkSecretStoragePrivateKey(privateKey, expectedPublicKey); - } - - /** - * Get e2e information on the device that sent an event - * - * @param event - event to be checked - * @deprecated Not supported for Rust Cryptography. - */ - public async getEventSenderDeviceInfo(event: MatrixEvent): Promise { - if (!this.crypto) { - return null; - } - return this.crypto.getEventSenderDeviceInfo(event); - } - - /** - * Check if the sender of an event is verified - * - * @param event - event to be checked - * - * @returns true if the sender of this event has been verified using - * {@link MatrixClient#setDeviceVerified}. - * - * @deprecated Not supported for Rust Cryptography. - */ - public async isEventSenderVerified(event: MatrixEvent): Promise { - const device = await this.getEventSenderDeviceInfo(event); - if (!device) { - return false; - } - return device.isVerified(); - } - - /** - * Get outgoing room key request for this event if there is one. - * @param event - The event to check for - * - * @returns A room key request, or null if there is none - * - * @deprecated Not supported for Rust Cryptography. - */ - public getOutgoingRoomKeyRequest(event: MatrixEvent): Promise { - if (!this.crypto) { - throw new Error("End-to-End encryption disabled"); - } - const wireContent = event.getWireContent(); - const requestBody: IRoomKeyRequestBody = { - session_id: wireContent.session_id, - sender_key: wireContent.sender_key, - algorithm: wireContent.algorithm, - room_id: event.getRoomId()!, - }; - if (!requestBody.session_id || !requestBody.sender_key || !requestBody.algorithm || !requestBody.room_id) { - return Promise.resolve(null); - } - return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody); - } - - /** - * Cancel a room key request for this event if one is ongoing and resend the - * request. - * @param event - event of which to cancel and resend the room - * key request. - * @returns A promise that will resolve when the key request is queued - * - * @deprecated Not supported for Rust Cryptography. - */ - public cancelAndResendEventRoomKeyRequest(event: MatrixEvent): Promise { - if (!this.crypto) { - throw new Error("End-to-End encryption disabled"); - } - return event.cancelAndResendKeyRequest(this.crypto, this.getUserId()!); - } - - /** - * Enable end-to-end encryption for a room. This does not modify room state. - * Any messages sent before the returned promise resolves will be sent unencrypted. - * @param roomId - The room ID to enable encryption in. - * @param config - The encryption config for the room. - * @returns A promise that will resolve when encryption is set up. - * - * @deprecated Not supported for Rust Cryptography. To enable encryption in a room, send an `m.room.encryption` - * state event. - */ - public setRoomEncryption(roomId: string, config: IRoomEncryption): Promise { - if (!this.crypto) { - throw new Error("End-to-End encryption disabled"); - } - return this.crypto.setRoomEncryption(roomId, config); - } - - /** - * Whether encryption is enabled for a room. - * @param roomId - the room id to query. - * @returns whether encryption is enabled. - * - * @deprecated Not correctly supported for Rust Cryptography. Use {@link CryptoApi.isEncryptionEnabledInRoom} and/or - * {@link Room.hasEncryptionStateEvent}. - */ - public isRoomEncrypted(roomId: string): boolean { - const room = this.getRoom(roomId); - if (!room) { - // we don't know about this room, so can't determine if it should be - // encrypted. Let's assume not. - return false; - } - - // if there is an 'm.room.encryption' event in this room, it should be - // encrypted (independently of whether we actually support encryption) - if (room.hasEncryptionStateEvent()) { - return true; - } - - // we don't have an m.room.encrypted event, but that might be because - // the server is hiding it from us. Check the store to see if it was - // previously encrypted. - return this.crypto?.isRoomEncrypted(roomId) ?? false; - } - - /** - * Encrypts and sends a given object via Olm to-device messages to a given - * set of devices. - * - * @param userDeviceInfoArr - list of deviceInfo objects representing the devices to send to - * - * @param payload - fields to include in the encrypted payload - * - * @returns Promise which - * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` - * of the successfully sent messages. - * - * @deprecated Instead use {@link CryptoApi.encryptToDeviceMessages} followed by {@link queueToDevice}. - */ - public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice[], payload: object): Promise { - if (!this.crypto) { - throw new Error("End-to-End encryption disabled"); - } - return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); - } - - /** - * Forces the current outbound group session to be discarded such - * that another one will be created next time an event is sent. - * - * @param roomId - The ID of the room to discard the session for - * - * @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}: - */ - public forceDiscardSession(roomId: string): void { - if (!this.cryptoBackend) { - throw new Error("End-to-End encryption disabled"); - } - this.cryptoBackend.forceDiscardSession(roomId); - } - - /** - * Get a list containing all of the room keys - * - * This should be encrypted before returning it to the user. - * - * @returns a promise which resolves to a list of session export objects - * - * @deprecated Prefer {@link CryptoApi.exportRoomKeys | `CryptoApi.exportRoomKeys`}: - * - * ```javascript - * sessionData = await client.getCrypto().exportRoomKeys(); - * ``` - */ - public exportRoomKeys(): Promise { - if (!this.cryptoBackend) { - return Promise.reject(new Error("End-to-end encryption disabled")); - } - return this.cryptoBackend.exportRoomKeys(); - } - - /** - * Import a list of room keys previously exported by exportRoomKeys - * - * @param keys - a list of session export objects - * @param opts - options object - * - * @returns a promise which resolves when the keys have been imported - * - * @deprecated Prefer {@link CryptoApi.importRoomKeys | `CryptoApi.importRoomKeys`}: - * ```javascript - * await client.getCrypto()?.importRoomKeys([..]); - * ``` - */ - public importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - return this.cryptoBackend.importRoomKeys(keys, opts); - } - - /** - * Force a re-check of the local key backup status against - * what's on the server. - * - * @returns Object with backup info (as returned by - * getKeyBackupVersion) in backupInfo and - * trust information (as returned by isKeyBackupTrusted) - * in trustInfo. - * - * @deprecated Prefer {@link Crypto.CryptoApi.checkKeyBackupAndEnable}. - */ - public checkKeyBackup(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.backupManager.checkKeyBackup(); - } - - /** - * Get information about the current key backup from the server. - * - * Performs some basic validity checks on the shape of the result, and raises an error if it is not as expected. - * - * **Note**: there is no (supported) way to distinguish between "failure to talk to the server" and "another client - * uploaded a key backup version using an algorithm I don't understand. - * - * @returns Information object from API, or null if no backup is present on the server. - * - * @deprecated Prefer {@link CryptoApi.getKeyBackupInfo}. - */ - public async getKeyBackupVersion(): Promise { - let res: IKeyBackupInfo; - try { - res = await this.http.authedRequest( - Method.Get, - "/room_keys/version", - undefined, - undefined, - { prefix: ClientPrefix.V3 }, - ); - } catch (e) { - if ((e).errcode === "M_NOT_FOUND") { - return null; - } else { - throw e; - } - } - BackupManager.checkBackupVersion(res); - return res; - } - - /** - * @param info - key backup info dict from getKeyBackupVersion() - * - * @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.isKeyBackupTrusted | `CryptoApi.isKeyBackupTrusted`}. - */ - public isKeyBackupTrusted(info: IKeyBackupInfo): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.backupManager.isKeyBackupTrusted(info); - } - - /** - * @returns true if the client is configured to back up keys to - * the server, otherwise false. If we haven't completed a successful check - * of key backup status yet, returns null. - * - * @deprecated Not supported for Rust Cryptography. Prefer direct access to {@link Crypto.CryptoApi.getActiveSessionBackupVersion}: - * - * ```javascript - * let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null; - * ``` - */ - public getKeyBackupEnabled(): boolean | null { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - return this.crypto.backupManager.getKeyBackupEnabled(); - } - - /** - * Enable backing up of keys, using data previously returned from - * getKeyBackupVersion. - * - * @param info - Backup information object as returned by getKeyBackupVersion - * @returns Promise which resolves when complete. - * - * @deprecated Do not call this directly. Instead call {@link Crypto.CryptoApi.checkKeyBackupAndEnable}. - */ - public enableKeyBackup(info: IKeyBackupInfo): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - return this.crypto.backupManager.enableKeyBackup(info); - } - - /** - * Disable backing up of keys. - * - * @deprecated Not supported for Rust Cryptography. It should be unnecessary to disable key backup. - */ - public disableKeyBackup(): void { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - this.crypto.backupManager.disableKeyBackup(); - } - - /** - * Set up the data required to create a new backup version. The backup version - * will not be created and enabled until createKeyBackupVersion is called. - * - * @param password - Passphrase string that can be entered by the user - * when restoring the backup as an alternative to entering the recovery key. - * Optional. - * - * @returns Object that can be passed to createKeyBackupVersion and - * additionally has a 'recovery_key' member with the user-facing recovery key string. - * - * @deprecated Not supported for Rust cryptography. Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}. - */ - public async prepareKeyBackupVersion( - password?: string | Uint8Array | null, - opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, - ): Promise> { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - // eslint-disable-next-line camelcase - const { algorithm, auth_data, recovery_key, privateKey } = - await this.crypto.backupManager.prepareKeyBackupVersion(password); - - if (opts.secureSecretStorage) { - await this.secretStorage.store("m.megolm_backup.v1", encodeBase64(privateKey)); - this.logger.info("Key backup private key stored in secret storage"); - } - - return { - algorithm, - /* eslint-disable camelcase */ - auth_data, - recovery_key, - /* eslint-enable camelcase */ - }; - } - - /** - * Check whether the key backup private key is stored in secret storage. - * @returns map of key name to key info the secret is - * encrypted with, or null if it is not present or not encrypted with a - * trusted key - */ - public isKeyBackupKeyStored(): Promise | null> { - return Promise.resolve(this.secretStorage.isStored("m.megolm_backup.v1")); - } - - /** - * Create a new key backup version and enable it, using the information return - * from prepareKeyBackupVersion. - * - * @param info - Info object from prepareKeyBackupVersion - * @returns Object with 'version' param indicating the version created - * - * @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}. - */ - public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - await this.crypto.backupManager.createKeyBackupVersion(info); - - const data = { - algorithm: info.algorithm, - auth_data: info.auth_data, - }; - - // Sign the backup auth data with the device key for backwards compat with - // older devices with cross-signing. This can probably go away very soon in - // favour of just signing with the cross-singing master key. - // XXX: Private member access - await this.crypto.signObject(data.auth_data); - - if ( - this.cryptoCallbacks.getCrossSigningKey && - // XXX: Private member access - this.crypto.crossSigningInfo.getId() - ) { - // now also sign the auth data with the cross-signing master key - // we check for the callback explicitly here because we still want to be able - // to create an un-cross-signed key backup if there is a cross-signing key but - // no callback supplied. - // XXX: Private member access - await this.crypto.crossSigningInfo.signObject(data.auth_data, "master"); - } - - const res = await this.http.authedRequest(Method.Post, "/room_keys/version", undefined, data); - - // We could assume everything's okay and enable directly, but this ensures - // we run the same signature verification that will be used for future - // sessions. - await this.checkKeyBackup(); - if (!this.getKeyBackupEnabled()) { - this.logger.error("Key backup not usable even though we just created it"); - } - - return res; - } - - /** - * @deprecated Use {@link Crypto.CryptoApi.deleteKeyBackupVersion | `CryptoApi.deleteKeyBackupVersion`}. - */ - public async deleteKeyBackupVersion(version: string): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - - await this.cryptoBackend.deleteKeyBackupVersion(version); - } - - private makeKeyBackupPath(roomId?: string, sessionId?: string, version?: string): IKeyBackupPath { - let path: string; - if (sessionId !== undefined) { - path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { - $roomId: roomId!, - $sessionId: sessionId, - }); - } else if (roomId !== undefined) { - path = utils.encodeUri("/room_keys/keys/$roomId", { - $roomId: roomId, - }); - } else { - path = "/room_keys/keys"; - } - const queryData = version === undefined ? undefined : { version }; - return { path, queryData }; - } - - /** - * Back up session keys to the homeserver. - * @param roomId - ID of the room that the keys are for Optional. - * @param sessionId - ID of the session that the keys are for Optional. - * @param version - backup version Optional. - * @param data - Object keys to send - * @returns a promise that will resolve when the keys - * are uploaded - * - * @deprecated Not supported for Rust Cryptography. - */ - public sendKeyBackup( - roomId: undefined, - sessionId: undefined, - version: string | undefined, - data: IKeyBackup, - ): Promise; - public sendKeyBackup( - roomId: string, - sessionId: undefined, - version: string | undefined, - data: IKeyBackup, - ): Promise; - public sendKeyBackup( - roomId: string, - sessionId: string, - version: string | undefined, - data: IKeyBackup, - ): Promise; - public async sendKeyBackup( - roomId: string | undefined, - sessionId: string | undefined, - version: string | undefined, - data: IKeyBackup, - ): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - const path = this.makeKeyBackupPath(roomId!, sessionId!, version); - await this.http.authedRequest(Method.Put, path.path, path.queryData, data, { prefix: ClientPrefix.V3 }); - } - - /** - * Marks all group sessions as needing to be backed up and schedules them to - * upload in the background as soon as possible. - * - * @deprecated Not supported for Rust Cryptography. This is done automatically as part of - * {@link CryptoApi.resetKeyBackup}, so there is probably no need to call this manually. - */ - public async scheduleAllGroupSessionsForBackup(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - await this.crypto.backupManager.scheduleAllGroupSessionsForBackup(); - } - - /** - * Marks all group sessions as needing to be backed up without scheduling - * them to upload in the background. - * - * (This is done automatically as part of {@link CryptoApi.resetKeyBackup}, - * so there is probably no need to call this manually.) - * - * @returns Promise which resolves to the number of sessions requiring a backup. - * @deprecated Not supported for Rust Cryptography. + * Return the provided scheduler, if any. + * @returns The scheduler or undefined */ - public flagAllGroupSessionsForBackup(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - return this.crypto.backupManager.flagAllGroupSessionsForBackup(); + public getScheduler(): MatrixScheduler | undefined { + return this.scheduler; } /** - * Return true if recovery key is valid. - * Try to decode the recovery key and check if it's successful. - * @param recoveryKey - * @deprecated Use {@link decodeRecoveryKey} directly + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * Will also retry any outbound to-device messages currently in the queue to be sent + * (retries of regular outgoing events are handled separately, per-event). + * @returns True if this resulted in a request being retried. */ - public isValidRecoveryKey(recoveryKey: string): boolean { - try { - decodeRecoveryKey(recoveryKey); - return true; - } catch { - return false; - } + public retryImmediately(): boolean { + // don't await for this promise: we just want to kick it off + this.toDeviceMessageQueue.sendQueue(); + return this.syncApi?.retryImmediately() ?? false; } /** - * Get the raw key for a key backup from the password - * Used when migrating key backups into SSSS - * - * The cross-signing API is currently UNSTABLE and may change without notice. + * Return the global notification EventTimelineSet, if any * - * @param password - Passphrase - * @param backupInfo - Backup metadata from `checkKeyBackup` - * @returns key backup key - * @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S. + * @returns the globl notification EventTimelineSet */ - public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise { - return keyFromAuthData(backupInfo.auth_data, password); + public getNotifTimelineSet(): EventTimelineSet | null { + return this.notifTimelineSet; } /** - * Get the raw key for a key backup from the recovery key - * Used when migrating key backups into SSSS - * - * The cross-signing API is currently UNSTABLE and may change without notice. + * Set the global notification EventTimelineSet * - * @param recoveryKey - The recovery key - * @returns key backup key - * @deprecated Use {@link decodeRecoveryKey} directly */ - public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array { - return decodeRecoveryKey(recoveryKey); + public setNotifTimelineSet(set: EventTimelineSet): void { + this.notifTimelineSet = set; } /** - * Restore from an existing key backup via a passphrase. - * - * @param password - Passphrase - * @param targetRoomId - Room ID to target a specific room. - * Restores all rooms if omitted. - * @param targetSessionId - Session ID to target a specific session. - * Restores all sessions if omitted. - * @param backupInfo - Backup metadata from `getKeyBackupVersion` or `checkKeyBackup`.`backupInfo` - * @param opts - Optional params such as callbacks - * @returns Status of restoration with `total` and `imported` - * key counts. + * Gets the cached capabilities of the homeserver, returning cached ones if available. + * If there are no cached capabilities and none can be fetched, throw an exception. * - * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. - */ - public async restoreKeyBackupWithPassword( - password: string, - targetRoomId: undefined, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. - */ - public async restoreKeyBackupWithPassword( - password: string, - targetRoomId: string, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. - */ - public async restoreKeyBackupWithPassword( - password: string, - targetRoomId: string, - targetSessionId: string, - backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. + * @returns Promise resolving with The capabilities of the homeserver */ - public async restoreKeyBackupWithPassword( - password: string, - targetRoomId: string | undefined, - targetSessionId: string | undefined, - backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, - ): Promise { - const privKey = await keyFromAuthData(backupInfo.auth_data, password); - return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); - } - - /** - * Restore from an existing key backup via a private key stored in secret - * storage. - * - * @param backupInfo - Backup metadata from `checkKeyBackup` - * @param targetRoomId - Room ID to target a specific room. - * Restores all rooms if omitted. - * @param targetSessionId - Session ID to target a specific session. - * Restores all sessions if omitted. - * @param opts - Optional params such as callbacks - * @returns Status of restoration with `total` and `imported` - * key counts. - * - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public async restoreKeyBackupWithSecretStorage( - backupInfo: IKeyBackupInfo, - targetRoomId?: string, - targetSessionId?: string, - opts?: IKeyBackupRestoreOpts, - ): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - const storedKey = await this.secretStorage.get("m.megolm_backup.v1"); - - // ensure that the key is in the right format. If not, fix the key and - // store the fixed version - const fixedKey = fixBackupKey(storedKey); - if (fixedKey) { - const keys = await this.secretStorage.getKey(); - await this.secretStorage.store("m.megolm_backup.v1", fixedKey, [keys![0]]); - } - - const privKey = decodeBase64(fixedKey || storedKey!); - return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); + public async getCapabilities(): Promise { + const caps = this.serverCapabilitiesService.getCachedCapabilities(); + if (caps) return caps; + return this.serverCapabilitiesService.fetchCapabilities(); } /** - * Restore from an existing key backup via an encoded recovery key. - * - * @param recoveryKey - Encoded recovery key - * @param targetRoomId - Room ID to target a specific room. - * Restores all rooms if omitted. - * @param targetSessionId - Session ID to target a specific session. - * Restores all sessions if omitted. - * @param backupInfo - Backup metadata from `checkKeyBackup` - * @param opts - Optional params such as callbacks - - * @returns Status of restoration with `total` and `imported` - * key counts. + * Gets the cached capabilities of the homeserver. If none have been fetched yet, + * return undefined. * - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public restoreKeyBackupWithRecoveryKey( - recoveryKey: string, - targetRoomId: undefined, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public restoreKeyBackupWithRecoveryKey( - recoveryKey: string, - targetRoomId: string, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public restoreKeyBackupWithRecoveryKey( - recoveryKey: string, - targetRoomId: string, - targetSessionId: string, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + * @returns The capabilities of the homeserver */ - public restoreKeyBackupWithRecoveryKey( - recoveryKey: string, - targetRoomId: string | undefined, - targetSessionId: string | undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise { - const privKey = decodeRecoveryKey(recoveryKey); - return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); + public getCachedCapabilities(): Capabilities | undefined { + return this.serverCapabilitiesService.getCachedCapabilities(); } /** - * Restore from an existing key backup via a private key stored locally - * @param targetRoomId - * @param targetSessionId - * @param backupInfo - * @param opts + * Fetches the latest capabilities from the homeserver, ignoring any cached + * versions. The newly returned version is cached. * - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public async restoreKeyBackupWithCache( - targetRoomId: undefined, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + * @returns A promise which resolves to the capabilities of the homeserver */ - public async restoreKeyBackupWithCache( - targetRoomId: string, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; + public fetchCapabilities(): Promise { + return this.serverCapabilitiesService.fetchCapabilities(); + } + /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. - */ - public async restoreKeyBackupWithCache( - targetRoomId: string, - targetSessionId: string, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - /** - * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto. + * + * **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to + * the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for + * ensuring that only one `MatrixClient` issue is instantiated at a time. + * + * @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'. + * @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly + * 32 bytes of data, and must be the same each time the client is initialised for a given device. + * If both this and `storagePassword` are unspecified, the store will be unencrypted. + * @param args.storagePassword - An alternative to `storageKey`. A password which will be used to derive a key to + * encrypt the store with. Deriving a key from a password is (deliberately) a slow operation, so prefer + * to pass a `storageKey` directly where possible. + * + * @returns a Promise which will resolve when the crypto layer has been + * successfully initialised. */ - public async restoreKeyBackupWithCache( - targetRoomId: string | undefined, - targetSessionId: string | undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise { - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); - } - const privKey = await this.cryptoBackend.getSessionBackupPrivateKey(); - if (!privKey) { - throw new Error("Couldn't get key"); - } - return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); - } - - private async restoreKeyBackup( - privKey: Uint8Array, - targetRoomId: undefined, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - private async restoreKeyBackup( - privKey: Uint8Array, - targetRoomId: string, - targetSessionId: undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - private async restoreKeyBackup( - privKey: Uint8Array, - targetRoomId: string, - targetSessionId: string, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise; - private async restoreKeyBackup( - privKey: Uint8Array, - targetRoomId: string | undefined, - targetSessionId: string | undefined, - backupInfo: IKeyBackupInfo, - opts?: IKeyBackupRestoreOpts, - ): Promise { - const cacheCompleteCallback = opts?.cacheCompleteCallback; - const progressCallback = opts?.progressCallback; - - if (!this.cryptoBackend) { - throw new Error("End-to-end encryption disabled"); + public async initRustCrypto( + args: { + useIndexedDB?: boolean; + storageKey?: Uint8Array; + storagePassword?: string; + } = {}, + ): Promise { + if (this.cryptoBackend) { + this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return; } - if (!backupInfo.version) { - throw new Error("Backup version must be defined"); + const userId = this.getUserId(); + if (userId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown userId: ` + + `ensure userId is passed in createClient().`, + ); + } + const deviceId = this.getDeviceId(); + if (deviceId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown deviceId: ` + + `ensure deviceId is passed in createClient().`, + ); } - const backupVersion = backupInfo.version!; - - let totalKeyCount = 0; - let totalFailures = 0; - let totalImported = 0; - - const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupVersion); - - const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey); - - const untrusted = !backupDecryptor.sourceTrusted; - - try { - if (!(privKey instanceof Uint8Array)) { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`); - } - // Cache the key, if possible. - // This is async. - this.cryptoBackend - .storeSessionBackupPrivateKey(privKey, backupVersion) - .catch((e) => { - this.logger.warn("Error caching session backup key:", e); - }) - .then(cacheCompleteCallback); - if (progressCallback) { - progressCallback({ - stage: "fetch", - }); - } + // importing rust-crypto will download the webassembly, so we delay it until we know it will be + // needed. + this.logger.debug("Downloading Rust crypto library"); + const RustCrypto = await import("./rust-crypto/index.ts"); - const res = await this.http.authedRequest( - Method.Get, - path.path, - path.queryData, - undefined, - { prefix: ClientPrefix.V3 }, - ); + const rustCrypto = await RustCrypto.initRustCrypto({ + logger: this.logger, + http: this.http, + userId: userId, + deviceId: deviceId, + secretStorage: this.secretStorage, + cryptoCallbacks: this.cryptoCallbacks, + storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX, + storeKey: args.storageKey, + storePassphrase: args.storagePassword, - // We have finished fetching the backup, go to next step - if (progressCallback) { - progressCallback({ - stage: "load_keys", - }); - } + legacyCryptoStore: this.legacyCryptoStore, + legacyPickleKey: this.legacyPickleKey ?? "DEFAULT_KEY", + legacyMigrationProgressListener: (progress: number, total: number): void => { + this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total); + }, + }); - if ((res as IRoomsKeysResponse).rooms) { - // We have a full backup here, it can get quite big, so we need to decrypt and import it in chunks. - - // Get the total count as a first pass - totalKeyCount = this.getTotalKeyCount(res as IRoomsKeysResponse); - // Now decrypt and import the keys in chunks - await this.handleDecryptionOfAFullBackup( - res as IRoomsKeysResponse, - backupDecryptor, - 200, - async (chunk) => { - // We have a chunk of decrypted keys: import them - try { - const backupVersion = backupInfo.version!; - await this.cryptoBackend!.importBackedUpRoomKeys(chunk, backupVersion, { - untrusted, - }); - totalImported += chunk.length; - } catch (e) { - totalFailures += chunk.length; - // We failed to import some keys, but we should still try to import the rest? - // Log the error and continue - logger.error("Error importing keys from backup", e); - } + rustCrypto.setSupportedVerificationMethods(this.verificationMethods); - if (progressCallback) { - progressCallback({ - total: totalKeyCount, - successes: totalImported, - stage: "load_keys", - failures: totalFailures, - }); - } - }, - ); - } else if ((res as IRoomKeysResponse).sessions) { - // For now we don't chunk for a single room backup, but we could in the future. - // Currently it is not used by the application. - const sessions = (res as IRoomKeysResponse).sessions; - totalKeyCount = Object.keys(sessions).length; - const keys = await backupDecryptor.decryptSessions(sessions); - for (const k of keys) { - k.room_id = targetRoomId!; - } - await this.cryptoBackend.importBackedUpRoomKeys(keys, backupVersion, { - progressCallback, - untrusted, - }); - totalImported = keys.length; - } else { - totalKeyCount = 1; - try { - const [key] = await backupDecryptor.decryptSessions({ - [targetSessionId!]: res as IKeyBackupSession, - }); - key.room_id = targetRoomId!; - key.session_id = targetSessionId!; + this.cryptoBackend = rustCrypto; - await this.cryptoBackend.importBackedUpRoomKeys([key], backupVersion, { - progressCallback, - untrusted, - }); - totalImported = 1; - } catch (e) { - this.logger.debug("Failed to decrypt megolm session from backup", e); - } - } - } finally { - backupDecryptor.free(); - } + // attach the event listeners needed by RustCrypto + this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); + this.on(ClientEvent.Event, (event) => { + rustCrypto.onLiveEventFromSync(event); + }); - /// in case entering the passphrase would add a new signature? - await this.cryptoBackend.checkKeyBackupAndEnable(); + // re-emit the events emitted by the crypto impl + this.reEmitter.reEmit(rustCrypto, [ + CryptoEvent.VerificationRequestReceived, + CryptoEvent.UserTrustStatusChanged, + CryptoEvent.KeyBackupStatus, + CryptoEvent.KeyBackupSessionsRemaining, + CryptoEvent.KeyBackupFailed, + CryptoEvent.KeyBackupDecryptionKeyCached, + CryptoEvent.KeysChanged, + CryptoEvent.DevicesUpdated, + CryptoEvent.WillUpdateDevices, + ]); + } - return { total: totalKeyCount, imported: totalImported }; + /** + * Access the server-side secret storage API for this client. + */ + public get secretStorage(): ServerSideSecretStorage { + return this._secretStorage; } /** - * This method calculates the total number of keys present in the response of a `/room_keys/keys` call. - * - * @param res - The response from the server containing the keys to be counted. + * Access the crypto API for this client. * - * @returns The total number of keys in the backup. + * If end-to-end encryption has been enabled for this client (via {@link initRustCrypto}), + * returns an object giving access to the crypto API. Otherwise, returns `undefined`. */ - private getTotalKeyCount(res: IRoomsKeysResponse): number { - const rooms = res.rooms; - let totalKeyCount = 0; - for (const roomData of Object.values(rooms)) { - if (!roomData.sessions) continue; - totalKeyCount += Object.keys(roomData.sessions).length; - } - return totalKeyCount; + public getCrypto(): CryptoApi | undefined { + return this.cryptoBackend; } /** - * This method handles the decryption of a full backup, i.e a call to `/room_keys/keys`. - * It will decrypt the keys in chunks and call the `block` callback for each chunk. - * - * @param res - The response from the server containing the keys to be decrypted. - * @param backupDecryptor - An instance of the BackupDecryptor class used to decrypt the keys. - * @param chunkSize - The size of the chunks to be processed at a time. - * @param block - A callback function that is called for each chunk of keys. + * Whether encryption is enabled for a room. + * @param roomId - the room id to query. + * @returns whether encryption is enabled. * - * @returns A promise that resolves when the decryption is complete. + * @deprecated Not correctly supported for Rust Cryptography. Use {@link CryptoApi.isEncryptionEnabledInRoom} and/or + * {@link Room.hasEncryptionStateEvent}. */ - private async handleDecryptionOfAFullBackup( - res: IRoomsKeysResponse, - backupDecryptor: BackupDecryptor, - chunkSize: number, - block: (chunk: IMegolmSessionData[]) => Promise, - ): Promise { - const rooms = (res as IRoomsKeysResponse).rooms; - - let groupChunkCount = 0; - let chunkGroupByRoom: Map = new Map(); - - const handleChunkCallback = async (roomChunks: Map): Promise => { - const currentChunk: IMegolmSessionData[] = []; - for (const roomId of roomChunks.keys()) { - const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!); - for (const sessionId in decryptedSessions) { - const k = decryptedSessions[sessionId]; - k.room_id = roomId; - currentChunk.push(k); - } - } - await block(currentChunk); - }; - - for (const [roomId, roomData] of Object.entries(rooms)) { - if (!roomData.sessions) continue; - - chunkGroupByRoom.set(roomId, {}); - - for (const [sessionId, session] of Object.entries(roomData.sessions)) { - const sessionsForRoom = chunkGroupByRoom.get(roomId)!; - sessionsForRoom[sessionId] = session; - groupChunkCount += 1; - if (groupChunkCount >= chunkSize) { - // We have enough chunks to decrypt - await handleChunkCallback(chunkGroupByRoom); - chunkGroupByRoom = new Map(); - // There might be remaining keys for that room, so add back an entry for the current room. - chunkGroupByRoom.set(roomId, {}); - groupChunkCount = 0; - } - } + public isRoomEncrypted(roomId: string): boolean { + const room = this.getRoom(roomId); + if (!room) { + // we don't know about this room, so can't determine if it should be + // encrypted. Let's assume not. + return false; } - // Handle remaining chunk if needed - if (groupChunkCount > 0) { - await handleChunkCallback(chunkGroupByRoom); + // if there is an 'm.room.encryption' event in this room, it should be + // encrypted (independently of whether we actually support encryption) + return room.hasEncryptionStateEvent(); + } + + /** + * Check whether the key backup private key is stored in secret storage. + * @returns map of key name to key info the secret is + * encrypted with, or null if it is not present or not encrypted with a + * trusted key + */ + public isKeyBackupKeyStored(): Promise | null> { + return Promise.resolve(this.secretStorage.isStored("m.megolm_backup.v1")); + } + + private makeKeyBackupPath(roomId?: string, sessionId?: string, version?: string): IKeyBackupPath { + let path: string; + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId!, + $sessionId: sessionId, + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId, + }); + } else { + path = "/room_keys/keys"; } + const queryData = version === undefined ? undefined : { version }; + return { path, queryData }; } public deleteKeysFromBackup(roomId: undefined, sessionId: undefined, version?: string): Promise; @@ -7898,17 +5860,6 @@ export class MatrixClient extends TypedEventEmitterThis * method is experimental and may change. @@ -7924,7 +5875,7 @@ export class MatrixClient extends TypedEventEmitter { - if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) { + if (event.shouldAttemptDecryption() && this.getCrypto()) { event.attemptDecryption(this.cryptoBackend!, options); } @@ -8269,17 +6220,6 @@ export class MatrixClient extends TypedEventEmitter { - if (this.crypto?.backupManager?.getKeyBackupEnabled()) { - try { - while ((await this.crypto.backupManager.backupPendingKeys(200)) > 0); - } catch (err) { - this.logger.error( - "Key backup request failed when logging out. Some keys may be missing from backup", - err, - ); - } - } - if (stopClient) { this.stopClient(); this.http.abort(); From b1567073564b15b051d439a416bf1399ba83bfbc Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 12:32:50 +0100 Subject: [PATCH 07/14] Remove usage of legacy crypto in `event.ts` (#4666) * feat(legacy crypto!): remove legacy crypto usage in `event.ts` * test(legacy crypto): update event.spec.ts to not use legacy crypto types --- spec/unit/models/event.spec.ts | 15 +++++++-------- src/models/event.ts | 34 ---------------------------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 0234eab3e09..2ed91267273 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -18,7 +18,6 @@ import { MockedObject } from "jest-mock"; import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { emitPromise } from "../../test-utils/test-utils"; -import { Crypto, IEventDecryptionResult } from "../../../src/crypto"; import { IAnnotatedPushRule, MatrixClient, @@ -28,7 +27,7 @@ import { TweakName, } from "../../../src"; import { DecryptionFailureCode } from "../../../src/crypto-api"; -import { DecryptionError } from "../../../src/common-crypto/CryptoBackend"; +import { CryptoBackend, DecryptionError, EventDecryptionResult } from "../../../src/common-crypto/CryptoBackend"; describe("MatrixEvent", () => { it("should create copies of itself", () => { @@ -369,7 +368,7 @@ describe("MatrixEvent", () => { const testError = new Error("test error"); const crypto = { decryptEvent: jest.fn().mockRejectedValue(testError), - } as unknown as Crypto; + } as unknown as CryptoBackend; await encryptedEvent.attemptDecryption(crypto); expect(encryptedEvent.isEncrypted()).toBeTruthy(); @@ -391,7 +390,7 @@ describe("MatrixEvent", () => { const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi"); const crypto = { decryptEvent: jest.fn().mockRejectedValue(testError), - } as unknown as Crypto; + } as unknown as CryptoBackend; await encryptedEvent.attemptDecryption(crypto); expect(encryptedEvent.isEncrypted()).toBeTruthy(); @@ -418,7 +417,7 @@ describe("MatrixEvent", () => { "The sender has disabled encrypting to unverified devices.", ), ), - } as unknown as Crypto; + } as unknown as CryptoBackend; await encryptedEvent.attemptDecryption(crypto); expect(encryptedEvent.isEncrypted()).toBeTruthy(); @@ -453,7 +452,7 @@ describe("MatrixEvent", () => { }, }); }), - } as unknown as Crypto; + } as unknown as CryptoBackend; await encryptedEvent.attemptDecryption(crypto); @@ -478,7 +477,7 @@ describe("MatrixEvent", () => { const crypto = { decryptEvent: jest.fn().mockImplementationOnce(() => { - return Promise.resolve({ + return Promise.resolve({ clearEvent: { type: "m.room.message", content: { @@ -491,7 +490,7 @@ describe("MatrixEvent", () => { }, }); }), - } as unknown as Crypto; + } as unknown as CryptoBackend; await encryptedEvent.attemptDecryption(crypto); expect(encryptedEvent.getType()).toEqual("m.room.message"); diff --git a/src/models/event.ts b/src/models/event.ts index 6aea68d5e4e..99c1dcafb42 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -23,7 +23,6 @@ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; import type { IEventDecryptionResult } from "../@types/crypto.ts"; import { logger } from "../logger.ts"; -import { VerificationRequest } from "../crypto/verification/request/VerificationRequest.ts"; import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, @@ -33,7 +32,6 @@ import { UNSIGNED_THREAD_ID_FIELD, UNSIGNED_MEMBERSHIP_FIELD, } from "../@types/event.ts"; -import { Crypto } from "../crypto/index.ts"; import { deepSortedObjectEntries, internaliseString } from "../utils.ts"; import { RoomMember } from "./room-member.ts"; import { Thread, THREAD_RELATION_TYPE, ThreadEvent, ThreadEventHandlerMap } from "./thread.ts"; @@ -406,12 +404,6 @@ export class MatrixEvent extends TypedEventEmitter; /** @@ -889,28 +881,6 @@ export class MatrixEvent extends TypedEventEmitter { - const wireContent = this.getWireContent(); - return crypto.requestRoomKey( - { - algorithm: wireContent.algorithm, - room_id: this.getRoomId()!, - session_id: wireContent.session_id, - sender_key: wireContent.sender_key, - }, - this.getKeyRequestRecipients(userId), - true, - ); - } - /** * Calculate the recipients for keyshare requests. * @@ -1720,10 +1690,6 @@ export class MatrixEvent extends TypedEventEmitter Date: Wed, 29 Jan 2025 12:37:59 +0100 Subject: [PATCH 08/14] Remove legacy crypto export in `matrix.ts` (#4667) * feat(legacy crypto!): remove legacy crypto export in `matrix.ts` * test(legacy crypto): update `megolm-backup.spec.ts` to import directly `CryptoApi` --- spec/integ/crypto/megolm-backup.spec.ts | 5 ++--- src/matrix.ts | 8 -------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index b3d6fe335a8..7a20d4e6904 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -21,7 +21,6 @@ import { Mocked } from "jest-mock"; import { createClient, - Crypto, encodeBase64, ICreateClientOpts, IEvent, @@ -44,7 +43,7 @@ import * as testData from "../../test-utils/test-data"; import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup"; import { flushPromises } from "../../test-utils/flushPromises"; import { defer, IDeferred } from "../../../src/utils"; -import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent } from "../../../src/crypto-api"; +import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, CryptoApi } from "../../../src/crypto-api"; import { KeyBackup } from "../../../src/rust-crypto/backup.ts"; const ROOM_ID = testData.TEST_ROOM_ID; @@ -311,7 +310,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe }); describe("recover from backup", () => { - let aliceCrypto: Crypto.CryptoApi; + let aliceCrypto: CryptoApi; beforeEach(async () => { fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); diff --git a/src/matrix.ts b/src/matrix.ts index dcad1b5f4bc..4f49ddcf0b8 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -85,7 +85,6 @@ export * from "./models/related-relations.ts"; export type { RoomSummary } from "./client.ts"; export * as ContentHelpers from "./content-helpers.ts"; export * as SecretStorage from "./secret-storage.ts"; -export type { ICryptoCallbacks } from "./crypto/index.ts"; // used to be located here export { createNewMatrixCall, CallEvent } from "./webrtc/call.ts"; export type { MatrixCall } from "./webrtc/call.ts"; export { @@ -97,10 +96,6 @@ export { GroupCallStatsReportEvent, } from "./webrtc/groupCall.ts"; -export { - /** @deprecated Use {@link Crypto.CryptoEvent} instead */ - CryptoEvent, -} from "./crypto/index.ts"; export { SyncState, SetPresence } from "./sync.ts"; export type { ISyncStateData as SyncStateData } from "./sync.ts"; export { SlidingSyncEvent } from "./sliding-sync.ts"; @@ -115,9 +110,6 @@ export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth.ts"; export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces.ts"; export { LocationAssetType } from "./@types/location.ts"; -/** @deprecated Backwards-compatibility re-export. Import from `crypto-api` directly. */ -export * as Crypto from "./crypto-api/index.ts"; - let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore(); /** From 69db213d3d0a49b4289eece33d279fa09963b24b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 13:11:47 +0100 Subject: [PATCH 09/14] Remove usage of legacy crypto in integ tests (#4669) --- spec/unit/matrix-client.spec.ts | 9 ++++----- spec/unit/room.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index d5482ccfaf6..538368555c2 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -31,8 +31,6 @@ import { UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "../../src/@types/event"; -import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib"; -import { Crypto } from "../../src/crypto"; import { EventStatus, MatrixEvent } from "../../src/models/event"; import { Preset } from "../../src/@types/partials"; import { ReceiptType } from "../../src/@types/read_receipts"; @@ -76,6 +74,7 @@ import { ServerSideSecretStorageImpl } from "../../src/secret-storage"; import { KnownMembership } from "../../src/@types/membership"; import { RoomMessageEventContent } from "../../src/@types/events"; import { mockOpenIdConfiguration } from "../test-utils/oidc.ts"; +import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; jest.useFakeTimers(); @@ -1188,7 +1187,7 @@ describe("MatrixClient", function () { type: EventType.RoomEncryption, state_key: "", content: { - algorithm: MEGOLM_ALGORITHM, + algorithm: "m.megolm.v1.aes-sha2", }, }, ], @@ -1914,7 +1913,7 @@ describe("MatrixClient", function () { hasEncryptionStateEvent: jest.fn().mockReturnValue(true), } as unknown as Room; - let mockCrypto: Mocked; + let mockCrypto: Mocked; let event: MatrixEvent; beforeEach(async () => { @@ -1934,7 +1933,7 @@ describe("MatrixClient", function () { isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true), encryptEvent: jest.fn(), stop: jest.fn(), - } as unknown as Mocked; + } as unknown as Mocked; client["cryptoBackend"] = mockCrypto; }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index c3d08f9a359..07e7b4725ed 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -53,12 +53,12 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread"; -import { Crypto } from "../../src/crypto"; import * as threadUtils from "../test-utils/thread"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client"; import { logger } from "../../src/logger"; import { flushPromises } from "../test-utils/flushPromises"; import { KnownMembership } from "../../src/@types/membership"; +import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; describe("Room", function () { const roomId = "!foo:bar"; @@ -3776,7 +3776,7 @@ describe("Room", function () { const client = new TestClient(userA).client; client["cryptoBackend"] = { decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }), - } as unknown as Crypto; + } as unknown as CryptoBackend; client.store.getPendingEvents = jest.fn(async (roomId) => [ { event_id: "$1:server", From 33afeba0fc19700dd038ef233d9e1c5023b58003 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 14:36:45 +0100 Subject: [PATCH 10/14] feat(crypto api!): remove deprecated methods and types --- src/crypto-api/index.ts | 119 ---------------------------------------- 1 file changed, 119 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 821f84baa3b..ab378fb9854 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -424,16 +424,6 @@ export interface CryptoApi { */ getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[]; - /** - * Finds a DM verification request that is already in progress for the given room id - * - * @param roomId - the room to use for verification - * - * @returns the VerificationRequest that is in progress, if any - * @deprecated prefer `userId` parameter variant. - */ - findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined; - /** * Finds a DM verification request that is already in progress for the given room and user. * @@ -501,18 +491,6 @@ export interface CryptoApi { */ getSessionBackupPrivateKey(): Promise; - /** - * Store the backup decryption key. - * - * This should be called if the client has received the key from another device via secret sharing (gossiping). - * It is the responsability of the caller to check that the decryption key is valid for the current backup version. - * - * @param key - the backup decryption key - * - * @deprecated prefer the variant with a `version` parameter. - */ - storeSessionBackupPrivateKey(key: Uint8Array): Promise; - /** * Store the backup decryption key. * @@ -732,45 +710,6 @@ export enum DecryptionFailureCode { /** Unknown or unclassified error. */ UNKNOWN_ERROR = "UNKNOWN_ERROR", - - /** @deprecated only used in legacy crypto */ - MEGOLM_BAD_ROOM = "MEGOLM_BAD_ROOM", - - /** @deprecated only used in legacy crypto */ - MEGOLM_MISSING_FIELDS = "MEGOLM_MISSING_FIELDS", - - /** @deprecated only used in legacy crypto */ - OLM_DECRYPT_GROUP_MESSAGE_ERROR = "OLM_DECRYPT_GROUP_MESSAGE_ERROR", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_ENCRYPTED_MESSAGE = "OLM_BAD_ENCRYPTED_MESSAGE", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_RECIPIENT = "OLM_BAD_RECIPIENT", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_RECIPIENT_KEY = "OLM_BAD_RECIPIENT_KEY", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_ROOM = "OLM_BAD_ROOM", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_SENDER_CHECK_FAILED = "OLM_BAD_SENDER_CHECK_FAILED", - - /** @deprecated only used in legacy crypto */ - OLM_BAD_SENDER = "OLM_BAD_SENDER", - - /** @deprecated only used in legacy crypto */ - OLM_FORWARDED_MESSAGE = "OLM_FORWARDED_MESSAGE", - - /** @deprecated only used in legacy crypto */ - OLM_MISSING_CIPHERTEXT = "OLM_MISSING_CIPHERTEXT", - - /** @deprecated only used in legacy crypto */ - OLM_NOT_INCLUDED_IN_RECIPIENTS = "OLM_NOT_INCLUDED_IN_RECIPIENTS", - - /** @deprecated only used in legacy crypto */ - UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM", } /** Base {@link DeviceIsolationMode} kind. */ @@ -862,7 +801,6 @@ export class UserVerificationStatus { public constructor( private readonly crossSigningVerified: boolean, private readonly crossSigningVerifiedBefore: boolean, - private readonly tofu: boolean, needsUserApproval: boolean = false, ) { this.needsUserApproval = needsUserApproval; @@ -889,15 +827,6 @@ export class UserVerificationStatus { public wasCrossSigningVerified(): boolean { return this.crossSigningVerifiedBefore; } - - /** - * @returns true if this user's key is trusted on first use - * - * @deprecated No longer supported, with the Rust crypto stack. - */ - public isTofu(): boolean { - return this.tofu; - } } export class DeviceVerificationStatus { @@ -981,10 +910,6 @@ export interface ImportRoomKeyProgressData { export interface ImportRoomKeysOpts { /** Reports ongoing progress of the import process. Can be used for feedback. */ progressCallback?: (stage: ImportRoomKeyProgressData) => void; - /** @deprecated the rust SDK will always such imported keys as untrusted */ - untrusted?: boolean; - /** @deprecated not useful externally */ - source?: string; } /** @@ -1070,13 +995,6 @@ export interface CryptoCallbacks { name: string, ) => Promise<[string, Uint8Array] | null>; - /** @deprecated: unused with the Rust crypto stack. */ - getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; - /** @deprecated: unused with the Rust crypto stack. */ - saveCrossSigningKeys?: (keys: Record) => void; - /** @deprecated: unused with the Rust crypto stack. */ - shouldUpgradeDeviceVerifications?: (users: Record) => Promise; - /** * Called by {@link CryptoApi.bootstrapSecretStorage} when a new default secret storage key is created. * @@ -1088,24 +1006,6 @@ export interface CryptoCallbacks { * @param key - private key to store */ cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void; - - /** @deprecated: unused with the Rust crypto stack. */ - onSecretRequested?: ( - userId: string, - deviceId: string, - requestId: string, - secretName: string, - deviceTrust: DeviceVerificationStatus, - ) => Promise; - - /** @deprecated: unused with the Rust crypto stack. */ - getDehydrationKey?: ( - keyInfo: SecretStorageKeyDescription, - checkFunc: (key: Uint8Array) => void, - ) => Promise; - - /** @deprecated: unused with the Rust crypto stack. */ - getBackupKey?: () => Promise; } /** @@ -1120,13 +1020,6 @@ export interface CreateSecretStorageOpts { */ createSecretStorageKey?: () => Promise; - /** - * The current key backup object. If passed, - * the passphrase and recovery key from this backup will be used. - * @deprecated Not used by the Rust crypto stack. - */ - keyBackupInfo?: KeyBackupInfo; - /** * If true, a new key backup version will be * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo @@ -1138,18 +1031,6 @@ export interface CreateSecretStorageOpts { * Reset even if keys already exist. */ setupNewSecretStorage?: boolean; - - /** - * Function called to get the user's current key backup passphrase. - * - * Should return a promise that resolves with a Uint8Array - * containing the key, or rejects if the key cannot be obtained. - * - * Only used when the client has existing key backup, but no secret storage. - * - * @deprecated Not used by the Rust crypto stack. - */ - getKeyBackupPassphrase?: () => Promise; } /** Types of cross-signing key */ From 1ffa618b24d271b2bd4f2097fe996d01eeb537f5 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 14:39:11 +0100 Subject: [PATCH 11/14] feat(rust-crypto!): remove deprecated optional parameters --- src/rust-crypto/rust-crypto.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d995b165de5..438830ed5b6 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1030,9 +1030,7 @@ export class RustCrypto extends TypedEventEmitter { + public async storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise { const base64Key = encodeBase64(key); - - if (!version) { - throw new Error("storeSessionBackupPrivateKey: version is required"); - } - await this.backupManager.saveBackupDecryptionKey( RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), version, From 644185dc4cf8a106b8b5c444608dc8baa1bb8b0f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 14:39:59 +0100 Subject: [PATCH 12/14] refactor(rust-crypto): remove `tofu` parameter of `UserVerificationStatus` --- src/rust-crypto/rust-crypto.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 438830ed5b6..eb57fae110c 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -657,7 +657,7 @@ export class RustCrypto extends TypedEventEmitter Date: Wed, 29 Jan 2025 14:47:30 +0100 Subject: [PATCH 13/14] test(rust-crypto): remove tests on removed optional parameters --- spec/unit/rust-crypto/rust-crypto.spec.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 50ec50c5e05..2fde57c341d 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -1455,16 +1455,6 @@ describe("RustCrypto", () => { const fetched = await rustCrypto.getSessionBackupPrivateKey(); expect(new TextDecoder().decode(fetched!)).toEqual(key); }); - - it("fails to save a key if version not provided", async () => { - const key = "testtesttesttesttesttesttesttest"; - const rustCrypto = await makeTestRustCrypto(); - await expect(() => rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key))).rejects.toThrow( - "storeSessionBackupPrivateKey: version is required", - ); - const fetched = await rustCrypto.getSessionBackupPrivateKey(); - expect(fetched).toBeNull(); - }); }); describe("getActiveSessionBackupVersion", () => { @@ -1474,15 +1464,6 @@ describe("RustCrypto", () => { }); }); - describe("findVerificationRequestDMInProgress", () => { - it("throws an error if the userId is not provided", async () => { - const rustCrypto = await makeTestRustCrypto(); - expect(() => rustCrypto.findVerificationRequestDMInProgress(testData.TEST_ROOM_ID)).toThrow( - "missing userId", - ); - }); - }); - describe("requestVerificationDM", () => { it("send verification request to an unknown user", async () => { const rustCrypto = await makeTestRustCrypto(); From 2c795ccf30c8f2cbb1ec41175ba08f69ea473733 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 Jan 2025 14:47:57 +0100 Subject: [PATCH 14/14] test(rust-crypto): update tests on `UserVerificationStatus` --- spec/unit/rust-crypto/rust-crypto.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 2fde57c341d..9bbdfe62155 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -1495,7 +1495,6 @@ describe("RustCrypto", () => { it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => { const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID); expect(userVerificationStatus.isVerified()).toBeFalsy(); - expect(userVerificationStatus.isTofu()).toBeFalsy(); expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy(); expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy(); }); @@ -1509,7 +1508,6 @@ describe("RustCrypto", () => { const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID); expect(userVerificationStatus.isVerified()).toBeTruthy(); - expect(userVerificationStatus.isTofu()).toBeFalsy(); expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy(); expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy(); });