From 43b453804bce7f6addf6cb02b9abc8aad184ca3f Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Thu, 4 Aug 2022 11:11:12 -0400 Subject: [PATCH] Always block sending keys to unverified devices of verified users (#2562) --- spec/unit/crypto/algorithms/megolm.spec.ts | 121 ++++++++++++++++++++- src/crypto/algorithms/megolm.ts | 8 +- 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 9aa3c5c78..058824ca3 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -30,7 +30,8 @@ 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'; +import { DeviceTrustLevel, UserTrustLevel } from '../../../../src/crypto/CrossSigning'; +import { resetCrossSigningKeys } from "../crypto-utils"; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -344,6 +345,10 @@ describe("MegolmDecryption", function() { }, })); + mockCrypto.checkUserTrust.mockReturnValue({ + isVerified: () => false, + } as UserTrustLevel); + mockCrypto.checkDeviceTrust.mockReturnValue({ isVerified: () => false, } as DeviceTrustLevel); @@ -577,6 +582,120 @@ describe("MegolmDecryption", function() { bobClient2.stopClient(); }); + it("always blocks unverified devices of verified users", async function() { + const keys = {}; + function getCrossSigningKey(keyType: string) { + return keys[keyType]; + } + + function saveCrossSigningKeys(k: Record) { + Object.assign(keys, k); + } + + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + undefined, undefined, + { cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys } }, + )).client; + const bobClient1 = (new TestClient( + "@bob:example.com", "bobdevice1", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient1.initCrypto(), + ]); + const aliceDevice = aliceClient.crypto.olmDevice; + const bobDevice1 = bobClient1.crypto.olmDevice; + + aliceClient.uploadDeviceSigningKeys = async () => ({}); + aliceClient.uploadKeySignatures = async () => ({ failures: {} }); + // set Alice's cross-signing key + await resetCrossSigningKeys(aliceClient); + // Alice downloads Bob's device key + aliceClient.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, + }); + await aliceClient.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true); + + 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(false); + aliceClient.store.storeRoom(room); + await aliceClient.setRoomEncryption(roomId, encryptionCfg); + + const BOB_DEVICES = { + 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: true, + }, + }; + + aliceClient.crypto.deviceList.storeDevicesForUser( + "@bob:example.com", BOB_DEVICES, + ); + aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { + 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["@bob:example.com"].bobdevice1.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, + }, + }, + }); + + aliceClient.stopClient(); + bobClient1.stopClient(); + }); + it("notifies devices when unable to create olm session", async function() { const aliceClient = (new TestClient( "@alice:example.com", "alicedevice", diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 395c834c5..50fed9ebb 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1156,10 +1156,16 @@ class MegolmEncryption extends EncryptionAlgorithm { continue; } + const userTrust = this.crypto.checkUserTrust(userId); const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId); if (userDevices[deviceId].isBlocked() || - (!deviceTrust.isVerified() && isBlacklisting) + (!deviceTrust.isVerified() && isBlacklisting) || + // Always withhold keys from unverified devices of verified users + (!deviceTrust.isVerified() && + userTrust.isVerified() && + this.crypto.getCryptoTrustCrossSignedDevices() + ) ) { if (!blocked[userId]) { blocked[userId] = {};