Merge pull request #5727 from matrix-org/matthew/rework-cross-signing-login

Rework cross-signing login flow
pull/21833/head
J. Ryan Stinnett 2021-03-11 12:53:55 +00:00 committed by GitHub
commit 6a939c4de8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 125 additions and 79 deletions

View File

@ -26,7 +26,9 @@ limitations under the License.
padding: 7px 18px;
text-align: center;
border-radius: 8px;
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: $font-14px;
}

View File

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UserInfo {
.mx_EncryptionInfo_spinner {
.mx_Spinner {
margin-top: 25px;
margin-bottom: 15px;
}
text-align: center;
.mx_EncryptionInfo_spinner {
.mx_Spinner {
margin-top: 25px;
margin-bottom: 15px;
}
text-align: center;
}

View File

@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
@ -58,7 +59,9 @@ export default class CompleteSecurity extends React.Component {
let icon;
let title;
if (phase === PHASE_INTRO) {
if (phase === PHASE_LOADING) {
return null;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === PHASE_DONE) {

View File

@ -17,11 +17,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
@ -81,6 +83,22 @@ export default class SetupEncryptionBody extends React.Component {
store.usePassPhrase();
}
_onVerifyClick = () => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}
onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
@ -132,32 +150,22 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>;
}
const brand = SdkConfig.get().brand;
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") }
</AccessibleButton>;
}
return (
<div>
<p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " +
"granting it access to encrypted messages.",
"Verify this login to access your encrypted messages and " +
"prove to others that this login is really you.",
)}</p>
<p>{_t(
"This requires the latest %(brand)s on your other devices:",
{ brand },
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>{_t("%(brand)s Web", { brand })}</div>
<div>{_t("%(brand)s Desktop", { brand })}</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>{_t("%(brand)s iOS", { brand })}</div>
<div>{_t("%(brand)s Android", { brand })}</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>
<div className="mx_CompleteSecurity_actionRow">
{verifyButton}
{useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")}
@ -215,7 +223,7 @@ export default class SetupEncryptionBody extends React.Component {
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {

View File

@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}

View File

@ -25,11 +25,11 @@ export default class VerificationRequestDialog extends React.Component {
verificationRequest: PropTypes.object,
verificationRequestPromise: PropTypes.object,
onFinished: PropTypes.func.isRequired,
member: PropTypes.string,
};
constructor(...args) {
super(...args);
this.onFinished = this.onFinished.bind(this);
this.state = {};
if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest;
@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
contentId="mx_Dialog_content"
title={title}
hasCancel={true}
@ -64,13 +64,4 @@ export default class VerificationRequestDialog extends React.Component {
/>
</BaseDialog>;
}
async onFinished() {
this.props.onFinished();
let request = this.props.verificationRequest;
if (!request && this.props.verificationRequestPromise) {
request = await this.props.verificationRequestPromise;
}
request.cancel();
}
}

View File

@ -46,6 +46,7 @@ import EncryptionPanel from "./EncryptionPanel";
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
import {Action} from "../../../dispatcher/actions";
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import {E2EStatus} from "../../../utils/ShieldUtils";
@ -1367,6 +1368,20 @@ const BasicUserInfo: React.FC<{
}
}
let editDevices;
if (member.userId == cli.getUserId()) {
editDevices = (<p>
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_SECURITY_TAB,
});
}}>
{ _t("Edit devices") }
</AccessibleButton>
</p>)
}
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
@ -1376,6 +1391,7 @@ const BasicUserInfo: React.FC<{
loading={showDeviceListSpinner}
devices={devices}
userId={member.userId} /> }
{ editDevices }
</div>
);

View File

@ -38,6 +38,7 @@ interface IProps {
interface IState {
counter: number;
device?: DeviceInfo;
ip?: string;
}
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
@ -66,9 +67,15 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
// a toast hanging around after logging in if you did a verification as part of login).
this._checkRequestIsPending();
if (request.isSelfVerification) {
const cli = MatrixClientPeg.get();
this.setState({device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
const device = await cli.getDevice(request.channel.deviceId);
const ip = device.last_seen_ip;
this.setState({
device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId),
ip,
});
}
}
@ -118,6 +125,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
verificationRequest: request,
onFinished: () => {
request.cancel();
},
}, null, /* priority = */ false, /* static = */ true);
}
await request.accept();
@ -131,9 +141,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
let nameLabel;
if (request.isSelfVerification) {
if (this.state.device) {
nameLabel = _t("From %(deviceName)s (%(deviceId)s)", {
nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", {
deviceName: this.state.device.getDisplayName(),
deviceId: this.state.device.deviceId,
ip: this.state.ip,
});
}
} else {

View File

@ -728,7 +728,7 @@
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
"Yes": "Yes",
"No": "No",
"Review where youre logged in": "Review where youre logged in",
"You have unverified logins": "You have unverified logins",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Review": "Review",
"Later": "Later",
@ -753,7 +753,8 @@
"Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
"Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s",
"Check your devices": "Check your devices",
"What's new?": "What's new?",
"What's New": "What's New",
"Update": "Update",
@ -980,7 +981,7 @@
"Folder": "Folder",
"Pin": "Pin",
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "From %(deviceName)s (%(deviceId)s) at %(ip)s",
"Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Delete": "Delete",
@ -1756,6 +1757,7 @@
"Failed to deactivate user": "Failed to deactivate user",
"Role": "Role",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Edit devices": "Edit devices",
"Security": "Security",
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
"Verify by scanning": "Verify by scanning",
@ -2720,13 +2722,8 @@
"Decide where your account is hosted": "Decide where your account is hosted",
"Use Security Key or Phrase": "Use Security Key or Phrase",
"Use Security Key": "Use Security Key",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
"This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:",
"%(brand)s Web": "%(brand)s Web",
"%(brand)s Desktop": "%(brand)s Desktop",
"%(brand)s iOS": "%(brand)s iOS",
"%(brand)s Android": "%(brand)s Android",
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
"Verify with another session": "Verify with another session",
"Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",

View File

@ -19,11 +19,12 @@ import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export const PHASE_INTRO = 0;
export const PHASE_BUSY = 1;
export const PHASE_DONE = 2; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 3;
export const PHASE_FINISHED = 4; //UX can be closed
export const PHASE_LOADING = 0;
export const PHASE_INTRO = 1;
export const PHASE_BUSY = 2;
export const PHASE_DONE = 3; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 4;
export const PHASE_FINISHED = 5; //UX can be closed
export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() {
@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter {
return;
}
this._started = true;
this.phase = PHASE_BUSY;
this.phase = PHASE_LOADING;
this.verificationRequest = null;
this.backupInfo = null;
@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter {
}
async fetchKeyInfo() {
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
const cli = MatrixClientPeg.get();
const keys = await cli.isSecretStored('m.cross_signing.master', false);
if (keys === null || Object.keys(keys).length === 0) {
this.keyId = null;
this.keyInfo = null;
@ -85,7 +87,20 @@ export class SetupEncryptionStore extends EventEmitter {
this.keyInfo = keys[this.keyId];
}
this.phase = PHASE_INTRO;
// do we have any other devices which are E2EE which we can verify against?
const dehydratedDevice = await cli.getDehydratedDevice();
this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
device =>
device.getIdentityKey() &&
(!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)),
);
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
// skip before we can even render anything.
this.phase = PHASE_FINISHED;
} else {
this.phase = PHASE_INTRO;
}
this.emit("update");
}

View File

@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Review where youre logged in"),
title: _t("You have unverified logins"),
icon: "verification_warning",
props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"),

View File

@ -15,38 +15,34 @@ limitations under the License.
*/
import { _t } from '../languageHandler';
import dis from "../dispatcher/dispatcher";
import { MatrixClientPeg } from '../MatrixClientPeg';
import Modal from '../Modal';
import DeviceListener from '../DeviceListener';
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
import { Action } from "../dispatcher/actions";
import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
function toastKey(deviceId: string) {
return "unverified_session_" + deviceId;
}
export const showToast = (deviceId: string) => {
export const showToast = async (deviceId: string) => {
const cli = MatrixClientPeg.get();
const onAccept = () => {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_SECURITY_TAB,
});
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
};
const device = cli.getStoredDevice(cli.getUserId(), deviceId);
const device = await cli.getDevice(deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
@ -54,8 +50,13 @@ export const showToast = (deviceId: string) => {
icon: "verification_warning",
props: {
description: _t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
acceptLabel: _t("Verify"),
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", {
name: device.display_name,
deviceID: deviceId,
ip: device.last_seen_ip,
},
),
acceptLabel: _t("Check your devices"),
onAccept,
rejectLabel: _t("Later"),
onReject,