Merge pull request #636 from matrix-org/matthew/blacklist-unverified

UI for blacklisting unverified devices per-room & globally
pull/21833/head
Richard van der Hoff 2017-02-03 11:32:24 +00:00 committed by GitHub
commit 2b67c1245f
8 changed files with 222 additions and 44 deletions

View File

@ -32,7 +32,8 @@ module.exports = {
if (err.name === "UnknownDeviceError") {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
Modal.createDialog(UnknownDeviceDialog, {
devices: err.devices
devices: err.devices,
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
}, "mx_Dialog_unknownDevice");
}

View File

@ -149,6 +149,23 @@ module.exports = {
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
},
getLocalSettings: function() {
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
var settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : null;
},
setLocalSetting: function(type, value) {
var settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
},
isFeatureEnabled: function(feature: string): boolean {
// Disable labs for guests.
if (MatrixClientPeg.get().isGuest()) return false;

View File

@ -59,6 +59,18 @@ const SETTINGS_LABELS = [
*/
];
const CRYPTO_SETTINGS_LABELS = [
{
id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device',
},
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
// id: 'blacklistUnverifiedDevicesPerRoom'
// label: 'Never send encrypted messages to unverified devices in this room',
// }
];
// Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
@ -151,6 +163,8 @@ module.exports = React.createClass({
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
},
componentDidMount: function() {
@ -566,10 +580,34 @@ module.exports = React.createClass({
{exportButton}
{importButton}
</div>
<div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
</div>
</div>
);
},
_renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
e => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
}
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},
_renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (

View File

@ -19,19 +19,47 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
function DeviceListEntry(props) {
const {userId, device} = props;
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
return (
<li>
<DeviceVerifyButtons device={ device } userId={ userId } />
{ device.deviceId }
<br/>
{ device.getDisplayName() }
</li>
);
}
DeviceListEntry.propTypes = {
userId: React.PropTypes.string.isRequired,
// deviceinfo
device: React.PropTypes.object.isRequired,
};
function UserUnknownDeviceList(props) {
const {userDevices} = props;
const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<li key={ deviceId }>
{ deviceId } ( { userDevices[deviceId].getDisplayName() } )
</li>,
<DeviceListEntry key={ deviceId } userId={ userId }
device={ userDevices[deviceId] } />,
);
return <ul>{deviceListEntries}</ul>;
return (
<ul className="mx_UnknownDeviceDialog_deviceList">
{deviceListEntries}
</ul>
);
}
UserUnknownDeviceList.propTypes = {
userId: React.PropTypes.string.isRequired,
// map from deviceid -> deviceinfo
userDevices: React.PropTypes.object.isRequired,
};
@ -43,7 +71,7 @@ function UnknownDeviceList(props) {
const userListEntries = Object.keys(devices).map((userId) =>
<li key={ userId }>
<p>{ userId }:</p>
<UserUnknownDeviceList userDevices={devices[userId]} />
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
</li>,
);
@ -60,6 +88,8 @@ export default React.createClass({
displayName: 'UnknownEventDialog',
propTypes: {
room: React.PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
@ -76,6 +106,34 @@ export default React.createClass({
},
render: function() {
const client = MatrixClientPeg.get();
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
this.props.room.getBlacklistUnverifiedDevices();
let warning;
if (blacklistUnverified) {
warning = (
<h4>
You are currently blacklisting unverified devices; to send
messages to these devices you must verify them.
</h4>
);
} else {
warning = (
<div>
<p>
This means there is no guarantee that the devices
belong to the users they claim to.
</p>
<p>
We recommend you go through the verification process
for each device before continuing, but you can resend
the message without verifying if you prefer.
</p>
</div>
);
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
@ -83,17 +141,13 @@ export default React.createClass({
title='Room contains unknown devices'
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>This room contains devices which have not been
verified.</h4>
<p>
This means there is no guarantee that the devices belong
to a rightful user of the room.
</p><p>
We recommend you go through the verification process
for each device before continuing, but you can resend
the message without verifying if you prefer.
</p>
<p>Unknown devices:</p>
<h4>
This room contains unknown devices which have not been
verified.
</h4>
{ warning }
Unknown devices:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
@ -104,5 +158,7 @@ export default React.createClass({
</div>
</BaseDialog>
);
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
// It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
},
});

View File

@ -27,6 +27,28 @@ export default React.createClass({
device: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
device: this.props.device
};
},
componentWillMount: function() {
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
onDeviceVerificationChanged: function(userId, deviceId) {
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
this.setState({ device: MatrixClientPeg.get().getStoredDevice(userId, deviceId) });
}
},
onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
@ -41,9 +63,9 @@ export default React.createClass({
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
</ul>
</div>
<p>
@ -60,7 +82,7 @@ export default React.createClass({
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true
this.props.userId, this.state.device.deviceId, true
);
}
},
@ -69,26 +91,26 @@ export default React.createClass({
onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, false
this.props.userId, this.state.device.deviceId, false
);
},
onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.deviceId, true
this.props.userId, this.state.device.deviceId, true
);
},
onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.deviceId, false
this.props.userId, this.state.device.deviceId, false
);
},
render: function() {
var blacklistButton = null, verifyButton = null;
if (this.props.device.isBlocked()) {
if (this.state.device.isBlocked()) {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
@ -104,7 +126,7 @@ export default React.createClass({
);
}
if (this.props.device.isVerified()) {
if (this.state.device.isVerified()) {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>

View File

@ -558,7 +558,7 @@ export default class MessageComposerInput extends React.Component {
dis.dispatch({
action: 'message_sent',
});
}, onSendMessageFailed);
}, (e) => onSendMessageFailed(e, this.props.room));
this.setState({
editorState: this.createEditorState(),

View File

@ -29,11 +29,12 @@ var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
var MARKDOWN_ENABLED = true;
export function onSendMessageFailed(err) {
export function onSendMessageFailed(err, room) {
if (err.name === "UnknownDeviceError") {
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
Modal.createDialog(UnknownDeviceDialog, {
devices: err.devices,
room: room,
}, "mx_Dialog_unknownDevice");
}
dis.dispatch({
@ -353,7 +354,7 @@ export default React.createClass({
dis.dispatch({
action: 'message_sent'
});
}, onSendMessageFailed);
}, (e) => onSendMessageFailed(e, this.props.room));
this.refs.textarea.value = '';
this.resizeInput();

View File

@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
var ScalarAuthClient = require("../../../ScalarAuthClient");
var ScalarMessaging = require('../../../ScalarMessaging');
var UserSettingsStore = require('../../../UserSettingsStore');
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
@ -228,11 +230,13 @@ module.exports = React.createClass({
}
// encryption
p = this.saveEncryption();
p = this.saveEnableEncryption();
if (!q.isFulfilled(p)) {
promises.push(p);
}
this.saveBlacklistUnverifiedDevicesPerRoom();
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
return promises;
},
@ -252,7 +256,7 @@ module.exports = React.createClass({
return this.refs.url_preview_settings.saveSettings();
},
saveEncryption: function() {
saveEnableEncryption: function() {
if (!this.refs.encrypt) { return q(); }
var encrypt = this.refs.encrypt.checked;
@ -265,6 +269,29 @@ module.exports = React.createClass({
);
},
saveBlacklistUnverifiedDevicesPerRoom: function() {
if (!this.refs.blacklistUnverified) return;
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
}
},
_isRoomBlacklistUnverified: function() {
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
if (blacklistUnverifiedDevicesPerRoom) {
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
}
return false;
},
_setRoomBlacklistUnverified: function(value) {
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
this.props.room.setBlacklistUnverifiedDevices(value);
},
_hasDiff: function(strA, strB) {
// treat undefined as an empty string because other components may blindly
// call setName("") when there has been no diff made to the name!
@ -477,26 +504,42 @@ module.exports = React.createClass({
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
var settings =
<label>
<input type="checkbox" ref="blacklistUnverified"
defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified }
disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/>
Never send encrypted messages to unverified devices in this room from this device.
</label>;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return (
<label>
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
Enable encryption (warning: cannot be disabled again!)
</label>
<div>
<label>
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
Enable encryption (warning: cannot be disabled again!)
</label>
{ settings }
</div>
);
}
else {
return (
<label>
{ isEncrypted
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
}
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
</label>
<div>
<label>
{ isEncrypted
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
}
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
</label>
{ settings }
</div>
);
}
},