First working version of SAS

This commit is contained in:
David Baker 2019-01-15 18:08:13 +00:00
parent 2a20d9d67e
commit 1d2538a7bc
6 changed files with 517 additions and 49 deletions

View file

@ -29,6 +29,7 @@ import SettingsStore from './settings/SettingsStore';
import MatrixActionCreators from './actions/MatrixActionCreators';
import {phasedRollOutExpiredForUser} from "./PhasedRollOut";
import Modal from './Modal';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
interface MatrixClientCreds {
homeserverUrl: string,
@ -184,6 +185,7 @@ class MatrixClientPeg {
deviceId: creds.deviceId,
timelineSupport: true,
forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
verificationMethods: [verificationMethods.SAS]
};
this.matrixClient = createMatrixClient(opts, useIndexedDb);

View file

@ -1277,6 +1277,7 @@ export default React.createClass({
this.firstSyncComplete = false;
this.firstSyncPromise = Promise.defer();
const cli = MatrixClientPeg.get();
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
// Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room
@ -1476,6 +1477,12 @@ export default React.createClass({
}
});
cli.on("crypto.verification.start", (verifier) => {
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
});
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
const colorScheme = SettingsStore.getValue("roomColor");

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,58 +22,273 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import SettingsStore from '../../../settings/SettingsStore';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import {renderSasWaitAccept} from '../../../sas_ui';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<div>
<p>
{ _t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:") }
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.") }
</p>
<p>
{ _t("In future this verification process will be more sophisticated.") }
</p>
</div>
);
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_SHOW_SAS = 2;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
const PHASE_VERIFIED = 4;
const PHASE_CANCELLED = 5;
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this._verifier = null;
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY,
};
}
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={onFinished}
/>
);
componentWillUnmount() {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
}
}
_onSwitchToLegacyClick = () => {
this.setState({mode: MODE_LEGACY});
}
_onSwitchToSasClick = () => {
this.setState({mode: MODE_SAS});
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
}
_onSasRequestClick = () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
this._verifier = MatrixClientPeg.get().beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
this._verifier.on('show_sas', this._onVerifierShowSas);
this._verifier.verify().then(() => {
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
}).catch((e) => {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
});
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
});
}
_renderSasVerification() {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderSasVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
//body = this._renderSasVerificationPhaseWaitForPartnerToAccept();
body = renderSasWaitAccept(this.props.userId);
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderSasVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderSasVerificationPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify device")}
onFinished={this._onCancelClick}
>
{body}
</BaseDialog>
);
}
_renderSasVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToLegacyClick}
>
{_t("Use Legacy Verification (for older clients)")}
</AccessibleButton>
<p>
{ _t("Do clicky clicky button press request verify user send to do.") }
</p>
<DialogButtons
primaryButton={_t('Send Verification Request')}
hasCancel={true}
onPrimaryButtonClick={this._onSasRequestClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderSasVerificationPhaseShowSas() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Verify this user by confirming the following number appears on their screen"
)}</p>
<p>{_t(
"For maximum security, we reccommend you do this in person or use another " +
"trusted means of communication"
)}</p>
<pre>{this._showSasEvent.sas}</pre>
<DialogButtons onPrimaryButtonClick={this._onSasMatchesClick}
primaryButton={_t("This Matches")}
hasCancel={true}
onCancel={this._onCancelClick}
/>
</div>;
}
_renderSasVerificationPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
<p>{_t(
"Waiting for %(userId)s to confirm...", {userId: this.props.userId},
)}</p>
</div>;
}
_renderSasVerificationPhaseVerified() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Verification complete!")}</p>
<DialogButtons onPrimaryButtonClick={this._onVerifiedDoneClick}
primaryButton={_t("Done")}
hasCancel={false}
/>
</div>;
}
_renderSasVerificationPhaseCancelled() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<p>{_t(
"%(userId)s cancelled the verification.", {userId: this.props.userId},
)}</p>
<DialogButtons
primaryButton={_t('Cancel')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
/>
</div>
);
}
_renderLegacyVerification() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToSasClick}
>
{_t("Use two-way text verification")}
</AccessibleButton>
<p>
{ _t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:") }
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.") }
</p>
<p>
{ _t("In future this verification process will be more sophisticated.") }
</p>
</div>
);
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
/>
);
}
render() {
if (this.state.mode === MODE_LEGACY) {
return this._renderLegacyVerification();
} else {
return <div>
{this._renderSasVerification()}
</div>;
}
}
}
DeviceVerifyDialog.propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,218 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4;
export default class IncomingSasDialog extends React.Component {
static propTypes = {
verifier: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
};
this.props.verifier.on('show_sas', this._onVerifierShowSas);
this.props.verifier.on('cancel', this._onVerifierCancel);
}
componentWillUnmount() {
if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) {
this.props.verifier.cancel('User cancel');
}
this.props.verifier.removeListener('show_sas', this._onVerifierShowSas);
}
_onFinished = () => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
}
_onCancelClick = () => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
}
_onContinueClick = () => {
this.setState({phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM});
this.props.verifier.verify().then(() => {
this.setState({phase: PHASE_VERIFIED});
}).catch((e) => {
console.log("Verification failed", e);
});
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
sas: e.sas,
});
}
_onVerifierCancel = (e) => {
this.setState({
phase: PHASE_CANCELLED,
});
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_renderPhaseStart() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<h2>{this.props.verifier.userId}</h2>
<p>{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages."
)}</p>
<p>{_t(
// NB. Below wording adjusted to singular 'device' until we have
// cross-signing
"Verifying this user will mark their device as trusted, and " +
"also mark your device as trusted to them"
)}</p>
<DialogButtons
primaryButton={_t('Continue')}
hasCancel={true}
onPrimaryButtonClick={this._onContinueClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderPhaseShowSas() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Verify this user by confirming the following number appears on their screen"
)}</p>
<p>{_t(
"For maximum security, we reccommend you do this in person or use another " +
"trusted means of communication"
)}</p>
<pre>{this._showSasEvent.sas}</pre>
<DialogButtons onPrimaryButtonClick={this._onSasMatchesClick}
primaryButton={_t("This Matches")}
hasCancel={true}
onCancel={this._onCancelClick}
/>
</div>;
}
_renderPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent("views.elements.Spinner");
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to confirm...")}</p>
</div>
);
}
_renderPhaseVerified() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<p>{_t(
"Verification Complete!"
)}</p>
<DialogButtons
primaryButton={_t('Done')}
hasCancel={false}
onPrimaryButtonClick={this._onVerifiedDoneClick}
/>
</div>
);
}
_renderPhaseCancelled() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<p>{_t(
"The other party cancelled the verification."
)}</p>
<DialogButtons
primaryButton={_t('Cancel')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
/>
</div>
);
}
render() {
console.log("rendering pahse "+this.state.phase);
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderPhaseStart();
break;
case PHASE_SHOW_SAS:
body = this._renderPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Incoming Verification Request")}
onFinished={this._onFinished}
>
{body}
</BaseDialog>
);
}
}

View file

@ -114,6 +114,8 @@
"Failed to invite": "Failed to invite",
"Failed to invite users to the room:": "Failed to invite users to the room:",
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
"Waiting for %(userId)s to accept...": "Waiting for %(userId)s to accept...",
"Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...",
"You need to be logged in.": "You need to be logged in.",
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
"Unable to create widget.": "Unable to create widget.",
@ -264,6 +266,7 @@
"Backup of encryption keys to server": "Backup of encryption keys to server",
"Render simple counters in room header": "Render simple counters in room header",
"Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu",
"Two-way device verification using short text": "Two-way device verification using short text",
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout",
"Hide removed messages": "Hide removed messages",
@ -938,12 +941,22 @@
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)",
"To continue, please enter your password:": "To continue, please enter your password:",
"password": "password",
"Verify device": "Verify device",
"Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)",
"Do clicky clicky button press request verify user send to do.": "Do clicky clicky button press request verify user send to do.",
"Send Verification Request": "Send Verification Request",
"Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen",
"For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication",
"This Matches": "This Matches",
"Verification complete!": "Verification complete!",
"Done": "Done",
"%(userId)s cancelled the verification.": "%(userId)s cancelled the verification.",
"Use two-way text verification": "Use two-way text verification",
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:",
"Device name": "Device name",
"Device key": "Device key",
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.",
"In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.",
"Verify device": "Verify device",
"I verify that the keys match": "I verify that the keys match",
"Back": "Back",
"Send Custom Event": "Send Custom Event",
@ -960,6 +973,12 @@
"Toolbox": "Toolbox",
"Developer Tools": "Developer Tools",
"An error has occurred.": "An error has occurred.",
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.",
"Verifying this user will mark their device as trusted, and also mark your device as trusted to them": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them",
"Waiting for partner to confirm...": "Waiting for partner to confirm...",
"Verification Complete!": "Verification Complete!",
"The other party cancelled the verification.": "The other party cancelled the verification.",
"Incoming Verification Request": "Incoming Verification Request",
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
"Start verification": "Start verification",

View file

@ -114,6 +114,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_sas": {
isFeature: true,
displayName: _td("Two-way device verification using short text"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"MessageComposerInput.dontSuggestEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Disable Emoji suggestions while typing'),