diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.ts similarity index 82% rename from spec/unit/crypto/algorithms/megolm.spec.js rename to spec/unit/crypto/algorithms/megolm.spec.ts index 9bb401b4d84..fec37296f10 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -1,15 +1,36 @@ +/* +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 { mocked, MockedObject } from 'jest-mock'; + import '../../../olm-loader'; import * as algorithms from "../../../../src/crypto/algorithms"; import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store"; -import { MockStorageApi } from "../../../MockStorageApi"; import * as testUtils from "../../../test-utils/test-utils"; import { OlmDevice } from "../../../../src/crypto/OlmDevice"; -import { Crypto } from "../../../../src/crypto"; +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 { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -28,17 +49,19 @@ describe("MegolmDecryption", function() { return Olm.init(); }); - let megolmDecryption; - let mockOlmLib; - let mockCrypto; - let mockBaseApis; + let megolmDecryption: algorithms.DecryptionAlgorithm; + let mockOlmLib: MockedObject; + let mockCrypto: MockedObject; + let mockBaseApis: MockedObject; beforeEach(async function() { - mockCrypto = testUtils.mock(Crypto, 'Crypto'); - mockBaseApis = {}; + mockCrypto = testUtils.mock(Crypto, 'Crypto') as MockedObject; + mockBaseApis = { + claimOneTimeKeys: jest.fn(), + sendToDevice: jest.fn(), + } as unknown as MockedObject; - const mockStorage = new MockStorageApi(); - const cryptoStore = new MemoryCryptoStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(); const olmDevice = new OlmDevice(cryptoStore); @@ -51,11 +74,15 @@ describe("MegolmDecryption", function() { }); // we stub out the olm encryption bits - mockOlmLib = {}; - mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); - mockOlmLib.encryptMessageForDevice = - jest.fn().mockResolvedValue(undefined); + mockOlmLib = { + encryptMessageForDevice: jest.fn().mockResolvedValue(undefined), + ensureOlmSessionsForDevices: jest.fn(), + } as unknown as MockedObject; + + // @ts-ignore illegal assignment that makes these tests work :/ megolmDecryption.olmlib = mockOlmLib; + + jest.clearAllMocks(); }); describe('receives some keys:', function() { @@ -87,7 +114,7 @@ describe("MegolmDecryption", function() { decryptEvent: function() { return Promise.resolve(decryptedData); }, - }; + } as unknown as Crypto; await event.attemptDecryption(mockCrypto).then(() => { megolmDecryption.onRoomKeyEvent(event); @@ -115,10 +142,13 @@ describe("MegolmDecryption", function() { }); it('can respond to a key request event', function() { - const keyRequest = { + const keyRequest: IncomingRoomKeyRequest = { + requestId: '123', + share: jest.fn(), userId: '@alice:foo', deviceId: 'alidevice', requestBody: { + algorithm: '', room_id: ROOM_ID, sender_key: "SENDER_CURVE25519", session_id: groupSession.session_id(), @@ -131,23 +161,24 @@ describe("MegolmDecryption", function() { expect(hasKeys).toBe(true); // set up some pre-conditions for the share call - const deviceInfo = {}; + const deviceInfo = {} as DeviceInfo; mockCrypto.getStoredDevice.mockReturnValue(deviceInfo); mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({ '@alice:foo': { 'alidevice': { sessionId: 'alisession', + device: new DeviceInfo('alidevice'), } }, }); - const awaitEncryptForDevice = new Promise((res, rej) => { + const awaitEncryptForDevice = new Promise((res, rej) => { mockOlmLib.encryptMessageForDevice.mockImplementation(() => { res(); return Promise.resolve(); }); }); - mockBaseApis.sendToDevice = jest.fn(); + mockBaseApis.sendToDevice.mockReset(); // do the share megolmDecryption.shareKeysWithDevice(keyRequest); @@ -265,17 +296,18 @@ describe("MegolmDecryption", function() { let olmDevice; beforeEach(async () => { + // @ts-ignore assigning to readonly prop mockCrypto.backupManager = { backupGroupSession: () => {}, }; - const mockStorage = new MockStorageApi(); - const cryptoStore = new MemoryCryptoStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(); olmDevice = new OlmDevice(cryptoStore); olmDevice.verifySignature = jest.fn(); await olmDevice.init(); - mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({ + mockBaseApis.claimOneTimeKeys.mockResolvedValue({ + failures: {}, one_time_keys: { '@alice:home.server': { aliceDevice: { @@ -290,8 +322,8 @@ describe("MegolmDecryption", function() { }, }, }, - })); - mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined); + }); + mockBaseApis.sendToDevice.mockResolvedValue(undefined); aliceDeviceInfo = { deviceId: 'aliceDevice', @@ -311,15 +343,17 @@ describe("MegolmDecryption", function() { mockCrypto.checkDeviceTrust.mockReturnValue({ isVerified: () => false, - }); + } as DeviceTrustLevel); megolmEncryption = new MegolmEncryption({ userId: '@user:id', + deviceId: '12345', crypto: mockCrypto, olmDevice: olmDevice, baseApis: mockBaseApis, roomId: ROOM_ID, config: { + algorithm: 'm.megolm.v1.aes-sha2', rotation_period_ms: rotationPeriodMs, }, }); @@ -449,33 +483,33 @@ describe("MegolmDecryption", function() { }; 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 [{ userId: "@bob:example.com" }]; + return [bobMember]; }; room.setBlacklistUnverifiedDevices(true); aliceClient.store.storeRoom(room); await aliceClient.setRoomEncryption(roomId, encryptionCfg); - const BOB_DEVICES = { + const BOB_DEVICES: Record = { bobdevice1: { - user_id: "@bob:example.com", - device_id: "bobdevice1", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { "ed25519:Dynabook": bobDevice1.deviceEd25519Key, "curve25519:Dynabook": bobDevice1.deviceCurve25519Key, }, verified: 0, + known: false, }, bobdevice2: { - user_id: "@bob:example.com", - device_id: "bobdevice2", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { "ed25519:Dynabook": bobDevice2.deviceEd25519Key, "curve25519:Dynabook": bobDevice2.deviceCurve25519Key, }, verified: -1, + known: false, }, }; @@ -486,32 +520,7 @@ describe("MegolmDecryption", function() { return this.getDevicesFromStore(userIds); }; - let run = false; - aliceClient.sendToDevice = async (msgtype, contentMap) => { - run = true; - expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); - delete contentMap["@bob:example.com"].bobdevice1.session_id; - delete contentMap["@bob:example.com"].bobdevice2.session_id; - expect(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.sendToDevice = jest.fn().mockResolvedValue({}); const event = new MatrixEvent({ type: "m.room.message", @@ -525,7 +534,30 @@ describe("MegolmDecryption", function() { }); await aliceClient.crypto.encryptEvent(event, room); - expect(run).toBe(true); + expect(aliceClient.sendToDevice).toHaveBeenCalled(); + const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; + expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); + delete contentMap["@bob:example.com"].bobdevice1.session_id; + delete contentMap["@bob:example.com"].bobdevice2.session_id; + expect(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(); @@ -557,18 +589,16 @@ describe("MegolmDecryption", function() { await aliceClient.setRoomEncryption(roomId, encryptionCfg); await bobClient.setRoomEncryption(roomId, encryptionCfg); - aliceRoom.getEncryptionTargetMembers = async () => { - return [ - { - userId: "@alice:example.com", - membership: "join", - }, - { - userId: "@bob:example.com", - membership: "join", - }, - ]; - }; + aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([ + { + userId: "@alice:example.com", + membership: "join", + }, + { + userId: "@bob:example.com", + membership: "join", + }, + ]); const BOB_DEVICES = { bobdevice: { user_id: "@bob:example.com", @@ -590,30 +620,14 @@ describe("MegolmDecryption", function() { return this.getDevicesFromStore(userIds); }; - aliceClient.claimOneTimeKeys = async () => { + aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({ // Bob has no one-time keys - return { - one_time_keys: {}, - }; - }; - - const sendPromise = new Promise((resolve, reject) => { - aliceClient.sendToDevice = async (msgtype, contentMap) => { - expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); - expect(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, - }, - }, - }); - resolve(); - }; + one_time_keys: {}, + failures: {}, }); + aliceClient.sendToDevice = jest.fn().mockResolvedValue({}); + const event = new MatrixEvent({ type: "m.room.message", sender: "@alice:example.com", @@ -622,7 +636,21 @@ describe("MegolmDecryption", function() { content: {}, }); await aliceClient.crypto.encryptEvent(event, aliceRoom); - await sendPromise; + + expect(aliceClient.sendToDevice).toHaveBeenCalled(); + const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; + expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); + expect(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(); }); @@ -640,9 +668,12 @@ describe("MegolmDecryption", function() { ]); const bobDevice = bobClient.crypto.olmDevice; + const aliceEventEmitter = new TypedEventEmitter(); + aliceClient.crypto.registerEventHandlers(aliceEventEmitter); + const roomId = "!someroom"; - aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ + aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({ type: "m.room_key.withheld", sender: "@bob:example.com", content: { @@ -669,7 +700,7 @@ describe("MegolmDecryption", function() { }, }))).rejects.toThrow("The sender has blocked you."); - aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ + aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({ type: "m.room_key.withheld", sender: "@bob:example.com", content: { @@ -710,14 +741,18 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - aliceClient.crypto.downloadKeys = async () => {}; + + 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(); - aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ + aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({ type: "m.room_key.withheld", sender: "@bob:example.com", content: { @@ -749,7 +784,7 @@ describe("MegolmDecryption", function() { origin_server_ts: now, }))).rejects.toThrow("The sender was unable to establish a secure channel."); - aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ + aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({ type: "m.room_key.withheld", sender: "@bob:example.com", content: { @@ -795,15 +830,18 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); + const aliceEventEmitter = new TypedEventEmitter(); + aliceClient.crypto.registerEventHandlers(aliceEventEmitter); + const bobDevice = bobClient.crypto.olmDevice; - aliceClient.crypto.downloadKeys = async () => {}; + aliceClient.crypto.downloadKeys = jest.fn(); const roomId = "!someroom"; const now = Date.now(); // pretend we got an event that we can't decrypt - aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ + aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", content: { diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.ts similarity index 94% rename from spec/unit/crypto/algorithms/olm.spec.js rename to spec/unit/crypto/algorithms/olm.spec.ts index ee7adbb73fc..7ed0c34f4be 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.ts @@ -1,6 +1,6 @@ /* Copyright 2018,2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -15,17 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from 'jest-mock'; + import '../../../olm-loader'; import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store"; -import { MockStorageApi } from "../../../MockStorageApi"; import { logger } from "../../../../src/logger"; import { OlmDevice } from "../../../../src/crypto/OlmDevice"; import * as olmlib from "../../../../src/crypto/olmlib"; import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; +import { MatrixClient } from '../../../../src'; function makeOlmDevice() { - const mockStorage = new MockStorageApi(); - const cryptoStore = new MemoryCryptoStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(); const olmDevice = new OlmDevice(cryptoStore); return olmDevice; } @@ -51,8 +52,8 @@ describe("OlmDevice", function() { return global.Olm.init(); }); - let aliceOlmDevice; - let bobOlmDevice; + let aliceOlmDevice: OlmDevice; + let bobOlmDevice: OlmDevice; beforeEach(async function() { aliceOlmDevice = makeOlmDevice(); @@ -69,7 +70,7 @@ describe("OlmDevice", function() { bobOlmDevice.deviceCurve25519Key, sid, "The olm or proteus is an aquatic salamander in the family Proteidae", - ); + ) as any; // OlmDevice.encryptMessage has incorrect return type const result = await bobOlmDevice.createInboundSession( aliceOlmDevice.deviceCurve25519Key, @@ -96,7 +97,7 @@ describe("OlmDevice", function() { bobOlmDevice.deviceCurve25519Key, sessionId, MESSAGE, - ); + ) as any; // OlmDevice.encryptMessage has incorrect return type const bobRecreatedOlmDevice = makeOlmDevice(); bobRecreatedOlmDevice.init({ fromExportedDevice: exported }); @@ -120,7 +121,7 @@ describe("OlmDevice", function() { bobOlmDevice.deviceCurve25519Key, sessionId, MESSAGE_2, - ); + ) as any; // OlmDevice.encryptMessage has incorrect return type const bobRecreatedAgainOlmDevice = makeOlmDevice(); bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain }); @@ -148,7 +149,7 @@ describe("OlmDevice", function() { setTimeout(reject, 500); }); }, - }; + } as unknown as MockedObject; const devicesByUser = { "@bob:example.com": [ DeviceInfo.fromStorage({ @@ -205,7 +206,7 @@ describe("OlmDevice", function() { setTimeout(reject, 500); }); }, - }; + } as unknown as MockedObject; const deviceBobA = DeviceInfo.fromStorage({ keys: {