Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 2b67c12

Browse files
authored
Merge pull request #636 from matrix-org/matthew/blacklist-unverified
UI for blacklisting unverified devices per-room & globally
2 parents 917be72 + 3ea746d commit 2b67c12

File tree

8 files changed

+222
-44
lines changed

8 files changed

+222
-44
lines changed

src/Resend.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ module.exports = {
3232
if (err.name === "UnknownDeviceError") {
3333
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
3434
Modal.createDialog(UnknownDeviceDialog, {
35-
devices: err.devices
35+
devices: err.devices,
36+
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
3637
}, "mx_Dialog_unknownDevice");
3738
}
3839

src/UserSettingsStore.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,23 @@ module.exports = {
149149
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
150150
},
151151

152+
getLocalSettings: function() {
153+
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
154+
return JSON.parse(localSettingsString);
155+
},
156+
157+
getLocalSetting: function(type, defaultValue = null) {
158+
var settings = this.getLocalSettings();
159+
return settings.hasOwnProperty(type) ? settings[type] : null;
160+
},
161+
162+
setLocalSetting: function(type, value) {
163+
var settings = this.getLocalSettings();
164+
settings[type] = value;
165+
// FIXME: handle errors
166+
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
167+
},
168+
152169
isFeatureEnabled: function(feature: string): boolean {
153170
// Disable labs for guests.
154171
if (MatrixClientPeg.get().isGuest()) return false;

src/components/structures/UserSettings.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ const SETTINGS_LABELS = [
5959
*/
6060
];
6161

62+
const CRYPTO_SETTINGS_LABELS = [
63+
{
64+
id: 'blacklistUnverifiedDevices',
65+
label: 'Never send encrypted messages to unverified devices from this device',
66+
},
67+
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
68+
// {
69+
// id: 'blacklistUnverifiedDevicesPerRoom'
70+
// label: 'Never send encrypted messages to unverified devices in this room',
71+
// }
72+
];
73+
6274
// Enumerate the available themes, with a nice human text label.
6375
// 'id' gives the key name in the im.vector.web.settings account data event
6476
// 'value' is the value for that key in the event
@@ -151,6 +163,8 @@ module.exports = React.createClass({
151163
syncedSettings.theme = 'light';
152164
}
153165
this._syncedSettings = syncedSettings;
166+
167+
this._localSettings = UserSettingsStore.getLocalSettings();
154168
},
155169

156170
componentDidMount: function() {
@@ -566,10 +580,34 @@ module.exports = React.createClass({
566580
{exportButton}
567581
{importButton}
568582
</div>
583+
<div className="mx_UserSettings_section">
584+
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
585+
</div>
569586
</div>
570587
);
571588
},
572589

590+
_renderLocalSetting: function(setting) {
591+
const client = MatrixClientPeg.get();
592+
return <div className="mx_UserSettings_toggle" key={ setting.id }>
593+
<input id={ setting.id }
594+
type="checkbox"
595+
defaultChecked={ this._localSettings[setting.id] }
596+
onChange={
597+
e => {
598+
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
599+
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
600+
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
601+
}
602+
}
603+
}
604+
/>
605+
<label htmlFor={ setting.id }>
606+
{ setting.label }
607+
</label>
608+
</div>;
609+
},
610+
573611
_renderDevicesPanel: function() {
574612
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
575613
return (

src/components/views/dialogs/UnknownDeviceDialog.js

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,47 @@ import sdk from '../../../index';
1919
import MatrixClientPeg from '../../../MatrixClientPeg';
2020
import GeminiScrollbar from 'react-gemini-scrollbar';
2121

22+
function DeviceListEntry(props) {
23+
const {userId, device} = props;
24+
25+
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
26+
27+
return (
28+
<li>
29+
<DeviceVerifyButtons device={ device } userId={ userId } />
30+
{ device.deviceId }
31+
<br/>
32+
{ device.getDisplayName() }
33+
</li>
34+
);
35+
}
36+
37+
DeviceListEntry.propTypes = {
38+
userId: React.PropTypes.string.isRequired,
39+
40+
// deviceinfo
41+
device: React.PropTypes.object.isRequired,
42+
};
43+
44+
2245
function UserUnknownDeviceList(props) {
23-
const {userDevices} = props;
46+
const {userId, userDevices} = props;
2447

2548
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
26-
<li key={ deviceId }>
27-
{ deviceId } ( { userDevices[deviceId].getDisplayName() } )
28-
</li>,
49+
<DeviceListEntry key={ deviceId } userId={ userId }
50+
device={ userDevices[deviceId] } />,
2951
);
3052

31-
return <ul>{deviceListEntries}</ul>;
53+
return (
54+
<ul className="mx_UnknownDeviceDialog_deviceList">
55+
{deviceListEntries}
56+
</ul>
57+
);
3258
}
3359

3460
UserUnknownDeviceList.propTypes = {
61+
userId: React.PropTypes.string.isRequired,
62+
3563
// map from deviceid -> deviceinfo
3664
userDevices: React.PropTypes.object.isRequired,
3765
};
@@ -43,7 +71,7 @@ function UnknownDeviceList(props) {
4371
const userListEntries = Object.keys(devices).map((userId) =>
4472
<li key={ userId }>
4573
<p>{ userId }:</p>
46-
<UserUnknownDeviceList userDevices={devices[userId]} />
74+
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
4775
</li>,
4876
);
4977

@@ -60,6 +88,8 @@ export default React.createClass({
6088
displayName: 'UnknownEventDialog',
6189

6290
propTypes: {
91+
room: React.PropTypes.object.isRequired,
92+
6393
// map from userid -> deviceid -> deviceinfo
6494
devices: React.PropTypes.object.isRequired,
6595
onFinished: React.PropTypes.func.isRequired,
@@ -76,24 +106,48 @@ export default React.createClass({
76106
},
77107

78108
render: function() {
109+
const client = MatrixClientPeg.get();
110+
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
111+
this.props.room.getBlacklistUnverifiedDevices();
112+
113+
let warning;
114+
if (blacklistUnverified) {
115+
warning = (
116+
<h4>
117+
You are currently blacklisting unverified devices; to send
118+
messages to these devices you must verify them.
119+
</h4>
120+
);
121+
} else {
122+
warning = (
123+
<div>
124+
<p>
125+
This means there is no guarantee that the devices
126+
belong to the users they claim to.
127+
</p>
128+
<p>
129+
We recommend you go through the verification process
130+
for each device before continuing, but you can resend
131+
the message without verifying if you prefer.
132+
</p>
133+
</div>
134+
);
135+
}
136+
79137
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
80138
return (
81139
<BaseDialog className='mx_UnknownDeviceDialog'
82140
onFinished={this.props.onFinished}
83141
title='Room contains unknown devices'
84142
>
85143
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
86-
<h4>This room contains devices which have not been
87-
verified.</h4>
88-
<p>
89-
This means there is no guarantee that the devices belong
90-
to a rightful user of the room.
91-
</p><p>
92-
We recommend you go through the verification process
93-
for each device before continuing, but you can resend
94-
the message without verifying if you prefer.
95-
</p>
96-
<p>Unknown devices:</p>
144+
<h4>
145+
This room contains unknown devices which have not been
146+
verified.
147+
</h4>
148+
{ warning }
149+
Unknown devices:
150+
97151
<UnknownDeviceList devices={this.props.devices} />
98152
</GeminiScrollbar>
99153
<div className="mx_Dialog_buttons">
@@ -104,5 +158,7 @@ export default React.createClass({
104158
</div>
105159
</BaseDialog>
106160
);
161+
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
162+
// It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
107163
},
108164
});

src/components/views/elements/DeviceVerifyButtons.js

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@ export default React.createClass({
2727
device: React.PropTypes.object.isRequired,
2828
},
2929

30+
getInitialState: function() {
31+
return {
32+
device: this.props.device
33+
};
34+
},
35+
36+
componentWillMount: function() {
37+
const cli = MatrixClientPeg.get();
38+
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
39+
},
40+
41+
componentWillUnmount: function() {
42+
const cli = MatrixClientPeg.get();
43+
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
44+
},
45+
46+
onDeviceVerificationChanged: function(userId, deviceId) {
47+
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
48+
this.setState({ device: MatrixClientPeg.get().getStoredDevice(userId, deviceId) });
49+
}
50+
},
51+
3052
onVerifyClick: function() {
3153
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
3254
Modal.createDialog(QuestionDialog, {
@@ -41,9 +63,9 @@ export default React.createClass({
4163
</p>
4264
<div className="mx_UserSettings_cryptoSection">
4365
<ul>
44-
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
45-
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
46-
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
66+
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
67+
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
68+
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
4769
</ul>
4870
</div>
4971
<p>
@@ -60,7 +82,7 @@ export default React.createClass({
6082
onFinished: confirm=>{
6183
if (confirm) {
6284
MatrixClientPeg.get().setDeviceVerified(
63-
this.props.userId, this.props.device.deviceId, true
85+
this.props.userId, this.state.device.deviceId, true
6486
);
6587
}
6688
},
@@ -69,26 +91,26 @@ export default React.createClass({
6991

7092
onUnverifyClick: function() {
7193
MatrixClientPeg.get().setDeviceVerified(
72-
this.props.userId, this.props.device.deviceId, false
94+
this.props.userId, this.state.device.deviceId, false
7395
);
7496
},
7597

7698
onBlacklistClick: function() {
7799
MatrixClientPeg.get().setDeviceBlocked(
78-
this.props.userId, this.props.device.deviceId, true
100+
this.props.userId, this.state.device.deviceId, true
79101
);
80102
},
81103

82104
onUnblacklistClick: function() {
83105
MatrixClientPeg.get().setDeviceBlocked(
84-
this.props.userId, this.props.device.deviceId, false
106+
this.props.userId, this.state.device.deviceId, false
85107
);
86108
},
87109

88110
render: function() {
89111
var blacklistButton = null, verifyButton = null;
90112

91-
if (this.props.device.isBlocked()) {
113+
if (this.state.device.isBlocked()) {
92114
blacklistButton = (
93115
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
94116
onClick={this.onUnblacklistClick}>
@@ -104,7 +126,7 @@ export default React.createClass({
104126
);
105127
}
106128

107-
if (this.props.device.isVerified()) {
129+
if (this.state.device.isVerified()) {
108130
verifyButton = (
109131
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
110132
onClick={this.onUnverifyClick}>

src/components/views/rooms/MessageComposerInput.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ export default class MessageComposerInput extends React.Component {
558558
dis.dispatch({
559559
action: 'message_sent',
560560
});
561-
}, onSendMessageFailed);
561+
}, (e) => onSendMessageFailed(e, this.props.room));
562562

563563
this.setState({
564564
editorState: this.createEditorState(),

src/components/views/rooms/MessageComposerInputOld.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ var TYPING_USER_TIMEOUT = 10000;
2929
var TYPING_SERVER_TIMEOUT = 30000;
3030
var MARKDOWN_ENABLED = true;
3131

32-
export function onSendMessageFailed(err) {
32+
export function onSendMessageFailed(err, room) {
3333
if (err.name === "UnknownDeviceError") {
3434
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
3535
Modal.createDialog(UnknownDeviceDialog, {
3636
devices: err.devices,
37+
room: room,
3738
}, "mx_Dialog_unknownDevice");
3839
}
3940
dis.dispatch({
@@ -353,7 +354,7 @@ export default React.createClass({
353354
dis.dispatch({
354355
action: 'message_sent'
355356
});
356-
}, onSendMessageFailed);
357+
}, (e) => onSendMessageFailed(e, this.props.room));
357358

358359
this.refs.textarea.value = '';
359360
this.resizeInput();

0 commit comments

Comments
 (0)