From 28df1e28cf4234aaf9ecbd8f64006f4e8beec782 Mon Sep 17 00:00:00 2001 From: Zoe Date: Thu, 13 Feb 2020 14:13:10 +0000 Subject: [PATCH 1/4] Start verification sessions in an E2E DM where possible Fixes https://github.com/vector-im/riot-web/issues/12187 --- src/createRoom.js | 37 ++++++++++++++++++++++++++++++++++++- test/createRoom-test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 test/createRoom-test.js diff --git a/src/createRoom.js b/src/createRoom.js index d4575633b3..8924808024 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -23,6 +23,7 @@ import dis from "./dispatcher"; import * as Rooms from "./Rooms"; import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; +import SettingsStore from "./settings/SettingsStore"; /** * Create a new room, and switch to it. @@ -174,13 +175,47 @@ export function findDMForUser(client, userId) { } } +/* + * Try to ensure the user is already in the megolm session before continuing + * NOTE: this assumes you've just created the room and there's not been an opportunity + * for other code to run, so we shouldn't miss RoomState.newMember when it comes by. + */ +export async function _waitForMember(client, roomId, userId, opts = { timeout: 1500 }) { + const { timeout } = opts; + let handler; + return new Promise((resolve) => { + handler = function(_event, _roomstate, member) { + if (member.userId !== userId) return; + if (member.roomId !== roomId) return; + resolve(true); + }; + client.on("RoomState.newMember", handler); + + /* We don't want to hang if this goes wrong, so we proceed and hope the other + user is already in the megolm session */ + setTimeout(resolve, timeout, false); + }).finally(() => { + client.removeListener("RoomState.newMember", handler); + }); +} + export async function ensureDMExists(client, userId) { const existingDMRoom = findDMForUser(client, userId); let roomId; if (existingDMRoom) { roomId = existingDMRoom.roomId; } else { - roomId = await createRoom({dmUserId: userId, spinner: false, andView: false}); + let encryption; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + /* If the user's devices can all do encryption, start an encrypted DM */ + const userDeviceMap = await client.downloadKeys([userId]); + // => { "@userId:host": { DEVICE: DeviceInfo, ... }} + const userDevices = Object.values(userDeviceMap[userId]); + // => [DeviceInfo, DeviceInfo...] + encryption = userDevices.every((device) => device.keys); + } + roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false}); + await _waitForMember(client, roomId, userId); } return roomId; } diff --git a/test/createRoom-test.js b/test/createRoom-test.js new file mode 100644 index 0000000000..4bb92b14a1 --- /dev/null +++ b/test/createRoom-test.js @@ -0,0 +1,41 @@ +import {_waitForMember} from '../src/createRoom'; +import {EventEmitter} from 'events'; + +/* Shorter timeout, we've got tests to run */ +const timeout = 30; + +describe("waitForMember", () => { + let client; + + beforeEach(() => { + client = new EventEmitter(); + }); + + it("resolves with false if the timeout is reached", (done) => { + _waitForMember(client, "", "", { timeout: 0 }).then((r) => { + expect(r).toBe(false); + done(); + }); + }); + + it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", (done) => { + const roomId = "!roomId:domain"; + const userId = "@clientId:domain"; + _waitForMember(client, roomId, userId, { timeout }).then((r) => { + expect(r).toBe(false); + done(); + }); + client.emit("RoomState.newMember", undefined, undefined, { roomId, userId: "@anotherClient:domain" }); + }) + + it("resolves with true if RoomState.newMember fires", (done) => { + const roomId = "!roomId:domain"; + const userId = "@clientId:domain"; + _waitForMember(client, roomId, userId, { timeout }).then((r) => { + expect(r).toBe(true); + expect(client.listeners("RoomState.newMember").length).toBe(0); + done(); + }); + client.emit("RoomState.newMember", undefined, undefined, { roomId, userId }); + }) +}); \ No newline at end of file From 23596031db461bf4e5e3d0acd183796a1afb431b Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 17 Feb 2020 16:00:25 +0000 Subject: [PATCH 2/4] rip foldleft, died of lint issues --- test/createRoom-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/createRoom-test.js b/test/createRoom-test.js index 4bb92b14a1..5955395c48 100644 --- a/test/createRoom-test.js +++ b/test/createRoom-test.js @@ -26,7 +26,7 @@ describe("waitForMember", () => { done(); }); client.emit("RoomState.newMember", undefined, undefined, { roomId, userId: "@anotherClient:domain" }); - }) + }); it("resolves with true if RoomState.newMember fires", (done) => { const roomId = "!roomId:domain"; @@ -37,5 +37,5 @@ describe("waitForMember", () => { done(); }); client.emit("RoomState.newMember", undefined, undefined, { roomId, userId }); - }) -}); \ No newline at end of file + }); +}); From 164b355ffea785f69c683ede1137bb3926378dc9 Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 18 Feb 2020 11:25:19 +0000 Subject: [PATCH 3/4] abstract out the check for available target devices --- src/components/views/dialogs/InviteDialog.js | 6 +--- src/createRoom.js | 20 ++++++++---- test/createRoom-test.js | 33 +++++++++++++++++++- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 8fec2437f6..5a5f2233bf 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -535,11 +535,7 @@ export default class InviteDialog extends React.PureComponent { // Check whether all users have uploaded device keys before. // If so, enable encryption in the new room. const client = MatrixClientPeg.get(); - const usersToDevicesMap = await client.downloadKeys(targetIds); - const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => { - // `devices` is an object of the form { deviceId: deviceInfo, ... }. - return Object.keys(devices).length > 0; - }); + const allHaveDeviceKeys = await createRoom.canEncryptToAllUsers(client, targetIds); if (allHaveDeviceKeys) { createRoomOptions.encryption = true; } diff --git a/src/createRoom.js b/src/createRoom.js index 8924808024..07eaee3e8f 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -199,6 +199,19 @@ export async function _waitForMember(client, roomId, userId, opts = { timeout: 1 }); } +/* + * Ensure that for every user in a room, there is at least one device that we + * can encrypt to. + */ +export async function canEncryptToAllUsers(client, userIds) { + const usersDeviceMap = await client.downloadKeys(userIds); + // { "@user:host": { "DEVICE": {...}, ... }, ... } + return Object.values(usersDeviceMap).every((userDevices) => + // { "DEVICE": {...}, ... } + Object.keys(userDevices).length > 0, + ); +} + export async function ensureDMExists(client, userId) { const existingDMRoom = findDMForUser(client, userId); let roomId; @@ -207,12 +220,7 @@ export async function ensureDMExists(client, userId) { } else { let encryption; if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - /* If the user's devices can all do encryption, start an encrypted DM */ - const userDeviceMap = await client.downloadKeys([userId]); - // => { "@userId:host": { DEVICE: DeviceInfo, ... }} - const userDevices = Object.values(userDeviceMap[userId]); - // => [DeviceInfo, DeviceInfo...] - encryption = userDevices.every((device) => device.keys); + encryption = canEncryptToAllUsers(client, [userId]); } roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false}); await _waitForMember(client, roomId, userId); diff --git a/test/createRoom-test.js b/test/createRoom-test.js index 5955395c48..f7e8617c3f 100644 --- a/test/createRoom-test.js +++ b/test/createRoom-test.js @@ -1,4 +1,4 @@ -import {_waitForMember} from '../src/createRoom'; +import {_waitForMember, canEncryptToAllUsers} from '../src/createRoom'; import {EventEmitter} from 'events'; /* Shorter timeout, we've got tests to run */ @@ -39,3 +39,34 @@ describe("waitForMember", () => { client.emit("RoomState.newMember", undefined, undefined, { roomId, userId }); }); }); + +describe("canEncryptToAllUsers", () => { + const trueUser = { + "@goodUser:localhost": { + "DEV1": {}, + "DEV2": {}, + }, + }; + const falseUser = { + "@badUser:localhost": {}, + }; + + it("returns true if all devices have crypto", async (done) => { + const client = { + downloadKeys: async function(userIds) { return trueUser; }, + }; + const response = await canEncryptToAllUsers(client, ["@goodUser:localhost"]); + expect(response).toBe(true); + done(); + }); + + + it("returns false if not all users have crypto", async (done) => { + const client = { + downloadKeys: async function(userIds) { return {...trueUser, ...falseUser}; }, + }; + const response = await canEncryptToAllUsers(client, ["@goodUser:localhost", "@badUser:localhost"]); + expect(response).toBe(false); + done(); + }); +}); From 0e0aadd3f2f555293394bfa46b4f30f0f12cd372 Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 18 Feb 2020 11:30:10 +0000 Subject: [PATCH 4/4] fixed inexplicible scope bug --- src/components/views/dialogs/InviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 5a5f2233bf..8c9c5f75ef 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -31,7 +31,7 @@ import dis from "../../../dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; -import createRoom from "../../../createRoom"; +import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; import SettingsStore from '../../../settings/SettingsStore'; @@ -535,7 +535,7 @@ export default class InviteDialog extends React.PureComponent { // Check whether all users have uploaded device keys before. // If so, enable encryption in the new room. const client = MatrixClientPeg.get(); - const allHaveDeviceKeys = await createRoom.canEncryptToAllUsers(client, targetIds); + const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds); if (allHaveDeviceKeys) { createRoomOptions.encryption = true; }