diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 2797887f29..3ac8a93e3d 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -63,7 +63,6 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise";
-import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
import ToastStore from "../../stores/ToastStore";
/** constants for MatrixChat.state.view */
@@ -1454,18 +1453,13 @@ export default createReactClass({
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
cli.on("crypto.verification.request", request => {
- let requestObserver;
- if (request.event.getRoomId()) {
- requestObserver = new KeyVerificationStateObserver(
- request.event, MatrixClientPeg.get());
- }
-
- if (!requestObserver || requestObserver.pending) {
+ console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
+ if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({
- key: 'verifreq_' + request.event.getId(),
+ key: 'verifreq_' + request.channel.transactionId,
title: _t("Verification Request"),
icon: "verification",
- props: {request, requestObserver},
+ props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"),
});
}
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index a725b73d9a..dca89d0c35 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -160,6 +160,7 @@ export default class RightPanel extends React.Component {
groupId: payload.groupId,
member: payload.member,
event: payload.event,
+ verificationRequest: payload.verificationRequest,
});
}
}
@@ -168,6 +169,7 @@ export default class RightPanel extends React.Component {
const MemberList = sdk.getComponent('rooms.MemberList');
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
+ const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
@@ -235,6 +237,8 @@ export default class RightPanel extends React.Component {
panel =
{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}
+{_t("For maximum security, do this in person.")}
+Not a member nor request, not sure what to render
; + } + } + + _onStartVerification = async () => { + const client = MatrixClientPeg.get(); + const {member} = this.props; + const roomId = await ensureDMExists(client, member.userId); + const verificationRequest = await client.requestVerificationDM(member.userId, roomId); + this.setState({verificationRequest}); + }; +} diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index 449ab3d686..a7accc23d8 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -28,6 +28,7 @@ import RightPanelStore from "../../../stores/RightPanelStore"; const MEMBER_PHASES = [ RIGHT_PANEL_PHASES.RoomMemberList, RIGHT_PANEL_PHASES.RoomMemberInfo, + RIGHT_PANEL_PHASES.EncryptionPanel, RIGHT_PANEL_PHASES.Room3pidMemberInfo, ]; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index adeedc7884..5f7de42368 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,6 +40,7 @@ import E2EIcon from "../rooms/E2EIcon"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {textualPowerLevel} from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -117,6 +118,14 @@ function verifyDevice(userId, device) { }, null, /* priority = */ false, /* static = */ true); } +function verifyUser(user) { + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: {member: user}, + }); +} + function DeviceItem({userId, device}) { const cli = useContext(MatrixClientContext); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); @@ -1225,15 +1234,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { setDevices(null); } } - if (isRoomEncrypted) { - _downloadDeviceList(); - } + _downloadDeviceList(); // Handle being unmounted return () => { cancelled = true; }; - }, [cli, user.userId, isRoomEncrypted]); + }, [cli, user.userId]); // Listen to changes useEffect(() => { @@ -1249,18 +1256,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { }); } }; - - if (isRoomEncrypted) { - cli.on("deviceVerificationChanged", onDeviceVerificationChanged); - } + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); // Handle being unmounted return () => { cancel = true; - if (isRoomEncrypted) { - cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); - } + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); }; - }, [cli, user.userId, isRoomEncrypted]); + }, [cli, user.userId]); let text; if (!isRoomEncrypted) { @@ -1275,22 +1277,24 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { text = _t("Messages in this room are end-to-end encrypted."); } - const devicesSection = isRoomEncrypted ? - ({ text }
- {verifyButton} + { verifyButton } { devicesSection }Waiting for {request.otherUserId} to accept ...
{request.otherUserId} is ready, start {verifyButton}
); + } else if (request.started) { + if (this.state.sasWaitingForOtherParty) { + returnWaiting for {request.otherUserId} to confirm ...
; + } else if (this.state.sasEvent) { + const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + return (Setting up SAS verification...
); + } + } else if (request.done) { + returnverified {request.otherUserId}!!
; + } else if (request.cancelled) { + returncancelled by {request.cancellingUserId}!
; + } + } + + _startSAS = async () => { + const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); + try { + await verifier.verify(); + } catch (err) { + console.error(err); + } finally { + this.setState({sasEvent: null}); + } + }; + + _onSasMatchesClick = () => { + this.setState({sasWaitingForOtherParty: true}); + this.state.sasEvent.confirm(); + }; + + _onSasMismatchesClick = () => { + this.state.sasEvent.cancel(); + }; + + _onVerifierShowSas = (sasEvent) => { + this.setState({sasEvent}); + }; + + _onRequestChange = async () => { + const {request} = this.props; + if (!this._hasVerifier && !!request.verifier) { + request.verifier.on('show_sas', this._onVerifierShowSas); + try { + // on the requester side, this is also awaited in _startSAS, + // but that's ok as verify should return the same promise. + await request.verifier.verify(); + } catch (err) { + console.error("error verify", err); + } + } else if (this._hasVerifier && !request.verifier) { + request.verifier.removeListener('show_sas', this._onVerifierShowSas); + } + this._hasVerifier = !!request.verifier; + this.forceUpdate(); + }; + + componentDidMount() { + this.props.request.on("change", this._onRequestChange); + } + + componentWillUnmount() { + this.props.request.off("change", this._onRequestChange); + } +} diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index 18db5eae66..479a3e3f93 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -18,55 +18,40 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; -import Modal from "../../../Modal"; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; import dis from "../../../dispatcher"; import ToastStore from "../../../stores/ToastStore"; export default class VerificationRequestToast extends React.PureComponent { constructor(props) { super(props); - const {event, timeout} = props.request; - // to_device requests don't have a timestamp, so consider them age=0 - const age = event.getTs() ? event.getLocalAge() : 0; - const remaining = Math.max(0, timeout - age); - const counter = Math.ceil(remaining / 1000); - this.state = {counter}; - if (this.props.requestObserver) { - this.props.requestObserver.setCallback(this._checkRequestIsPending); - } + this.state = {counter: Math.ceil(props.request.timeout / 1000)}; } componentDidMount() { - if (this.props.requestObserver) { - this.props.requestObserver.attach(); - this._checkRequestIsPending(); - } + const {request} = this.props; this._intervalHandle = setInterval(() => { let {counter} = this.state; - counter -= 1; - if (counter <= 0) { - this.cancel(); - } else { - this.setState({counter}); - } + counter = Math.max(0, counter - 1); + this.setState({counter}); }, 1000); + request.on("change", this._checkRequestIsPending); } componentWillUnmount() { clearInterval(this._intervalHandle); - if (this.props.requestObserver) { - this.props.requestObserver.detach(); - } + const {request} = this.props; + request.off("change", this._checkRequestIsPending); } _checkRequestIsPending = () => { - if (!this.props.requestObserver.pending) { + const {request} = this.props; + if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) { ToastStore.sharedInstance().dismissToast(this.props.toastKey); } - } + }; cancel = () => { ToastStore.sharedInstance().dismissToast(this.props.toastKey); @@ -77,9 +62,10 @@ export default class VerificationRequestToast extends React.PureComponent { } } - accept = () => { + accept = async () => { ToastStore.sharedInstance().dismissToast(this.props.toastKey); - const {event} = this.props.request; + const {request} = this.props; + const {event} = request; // no room id for to_device requests if (event.getRoomId()) { dis.dispatch({ @@ -88,18 +74,23 @@ export default class VerificationRequestToast extends React.PureComponent { should_peek: false, }); } - - const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); - const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); - Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, - }, null, /* priority = */ false, /* static = */ true); + try { + await request.accept(); + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: {verificationRequest: request}, + }); + } catch (err) { + console.error(err.message); + } }; render() { const FormButton = sdk.getComponent("elements.FormButton"); - const {event} = this.props.request; - const userId = event.getSender(); + const {request} = this.props; + const {event} = request; + const userId = request.otherUserId; let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; // for legacy to_device verification requests if (nameLabel === userId) { @@ -121,6 +112,5 @@ export default class VerificationRequestToast extends React.PureComponent { VerificationRequestToast.propTypes = { request: PropTypes.object.isRequired, - requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver), toastKey: PropTypes.string.isRequired, }; diff --git a/src/createRoom.js b/src/createRoom.js index 867d6409eb..cde9e8b03e 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket 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. @@ -20,7 +21,7 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import dis from "./dispatcher"; import * as Rooms from "./Rooms"; - +import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; /** @@ -139,3 +140,23 @@ export default function createRoom(opts) { return null; }); } + +export async function ensureDMExists(client, userId) { + const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId); + const rooms = roomIds.map(id => client.getRoom(id)); + const suitableDMRooms = rooms.filter(r => { + if (r && r.getMyMembership() === "join") { + const member = r.getMember(userId); + return member && (member.membership === "invite" || member.membership === "join"); + } + return false; + }); + let roomId; + if (suitableDMRooms.length) { + const room = suitableDMRooms[0]; + roomId = room.roomId; + } else { + roomId = await createRoom({dmUserId: userId, spinner: false, andView: false}); + } + return roomId; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4af203177c..5f1886cd56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1117,6 +1117,10 @@ "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", + "Verify User": "Verify User", + "For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.", + "For maximum security, do this in person.": "For maximum security, do this in person.", + "Start Verification": "Start Verification", "Members": "Members", "Files": "Files", "Trusted": "Trusted", diff --git a/src/stores/RightPanelStore.js b/src/stores/RightPanelStore.js index 02775b847b..1b3cb3d64b 100644 --- a/src/stores/RightPanelStore.js +++ b/src/stores/RightPanelStore.js @@ -123,7 +123,11 @@ export default class RightPanelStore extends Store { if (payload.action === 'view_room' || payload.action === 'view_group') { // Reset to the member list if we're viewing member info - const memberInfoPhases = [RIGHT_PANEL_PHASES.RoomMemberInfo, RIGHT_PANEL_PHASES.Room3pidMemberInfo]; + const memberInfoPhases = [ + RIGHT_PANEL_PHASES.RoomMemberInfo, + RIGHT_PANEL_PHASES.Room3pidMemberInfo, + RIGHT_PANEL_PHASES.EncryptionPanel, + ]; if (memberInfoPhases.includes(this._state.lastRoomPhase)) { this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}}); } diff --git a/src/stores/RightPanelStorePhases.js b/src/stores/RightPanelStorePhases.js index 96807ebf5b..d9af320233 100644 --- a/src/stores/RightPanelStorePhases.js +++ b/src/stores/RightPanelStorePhases.js @@ -21,8 +21,9 @@ export const RIGHT_PANEL_PHASES = Object.freeze({ FilePanel: 'FilePanel', NotificationPanel: 'NotificationPanel', RoomMemberInfo: 'RoomMemberInfo', - Room3pidMemberInfo: 'Room3pidMemberInfo', + EncryptionPanel: 'EncryptionPanel', + Room3pidMemberInfo: 'Room3pidMemberInfo', // Group stuff GroupMemberList: 'GroupMemberList', GroupRoomList: 'GroupRoomList', diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index d84a5f584e..1a35319186 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -17,153 +17,6 @@ limitations under the License. import {MatrixClientPeg} from '../MatrixClientPeg'; import { _t } from '../languageHandler'; -const SUB_EVENT_TYPES_OF_INTEREST = ["start", "cancel", "done"]; - -export default class KeyVerificationStateObserver { - constructor(requestEvent, client, updateCallback) { - this._requestEvent = requestEvent; - this._client = client; - this._updateCallback = updateCallback; - this.accepted = false; - this.done = false; - this.cancelled = false; - this._updateVerificationState(); - } - - get concluded() { - return this.accepted || this.done || this.cancelled; - } - - get pending() { - return !this.concluded; - } - - setCallback(callback) { - this._updateCallback = callback; - } - - attach() { - this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated); - for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { - this._tryListenOnRelationsForType(`m.key.verification.${phaseName}`); - } - } - - detach() { - const roomId = this._requestEvent.getRoomId(); - const room = this._client.getRoom(roomId); - - for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { - const relations = room.getUnfilteredTimelineSet() - .getRelationsForEvent(this._requestEvent.getId(), "m.reference", `m.key.verification.${phaseName}`); - if (relations) { - relations.removeListener("Relations.add", this._onRelationsUpdated); - relations.removeListener("Relations.remove", this._onRelationsUpdated); - relations.removeListener("Relations.redaction", this._onRelationsUpdated); - } - } - this._requestEvent.removeListener("Event.relationsCreated", this._onRelationsCreated); - } - - _onRelationsCreated = (relationType, eventType) => { - if (relationType !== "m.reference") { - return; - } - if ( - eventType !== "m.key.verification.start" && - eventType !== "m.key.verification.cancel" && - eventType !== "m.key.verification.done" - ) { - return; - } - this._tryListenOnRelationsForType(eventType); - this._updateVerificationState(); - this._updateCallback(); - }; - - _tryListenOnRelationsForType(eventType) { - const roomId = this._requestEvent.getRoomId(); - const room = this._client.getRoom(roomId); - const relations = room.getUnfilteredTimelineSet() - .getRelationsForEvent(this._requestEvent.getId(), "m.reference", eventType); - if (relations) { - relations.on("Relations.add", this._onRelationsUpdated); - relations.on("Relations.remove", this._onRelationsUpdated); - relations.on("Relations.redaction", this._onRelationsUpdated); - } - } - - _onRelationsUpdated = (event) => { - this._updateVerificationState(); - this._updateCallback && this._updateCallback(); - }; - - _updateVerificationState() { - const roomId = this._requestEvent.getRoomId(); - const room = this._client.getRoom(roomId); - const timelineSet = room.getUnfilteredTimelineSet(); - const fromUserId = this._requestEvent.getSender(); - const content = this._requestEvent.getContent(); - const toUserId = content.to; - - this.cancelled = false; - this.done = false; - this.accepted = false; - this.otherPartyUserId = null; - this.cancelPartyUserId = null; - - const startRelations = timelineSet.getRelationsForEvent( - this._requestEvent.getId(), "m.reference", "m.key.verification.start"); - if (startRelations) { - for (const startEvent of startRelations.getRelations()) { - if (startEvent.getSender() === toUserId) { - this.accepted = true; - } - } - } - - const doneRelations = timelineSet.getRelationsForEvent( - this._requestEvent.getId(), "m.reference", "m.key.verification.done"); - if (doneRelations) { - let senderDone = false; - let receiverDone = false; - for (const doneEvent of doneRelations.getRelations()) { - if (doneEvent.getSender() === toUserId) { - receiverDone = true; - } else if (doneEvent.getSender() === fromUserId) { - senderDone = true; - } - } - if (senderDone && receiverDone) { - this.done = true; - } - } - - if (!this.done) { - const cancelRelations = timelineSet.getRelationsForEvent( - this._requestEvent.getId(), "m.reference", "m.key.verification.cancel"); - - if (cancelRelations) { - let earliestCancelEvent; - for (const cancelEvent of cancelRelations.getRelations()) { - // only accept cancellation from the users involved - if (cancelEvent.getSender() === toUserId || cancelEvent.getSender() === fromUserId) { - this.cancelled = true; - if (!earliestCancelEvent || cancelEvent.getTs() < earliestCancelEvent.getTs()) { - earliestCancelEvent = cancelEvent; - } - } - } - if (earliestCancelEvent) { - this.cancelPartyUserId = earliestCancelEvent.getSender(); - } - } - } - - this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId; - } -} - export function getNameForEventRoom(userId, mxEvent) { const roomId = mxEvent.getRoomId(); const client = MatrixClientPeg.get();