mirror of https://github.com/vector-im/riot-web
Merge pull request #5727 from matrix-org/matthew/rework-cross-signing-login
Rework cross-signing login flowpull/21833/head
commit
6a939c4de8
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_UserInfo {
|
||||
.mx_EncryptionInfo_spinner {
|
||||
.mx_Spinner {
|
||||
margin-top: 25px;
|
||||
|
@ -23,4 +22,3 @@ limitations under the License.
|
|||
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 you’re logged in": "Review where you’re 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 won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
|||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: TOAST_KEY,
|
||||
title: _t("Review where you’re 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"),
|
||||
|
|
|
@ -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);
|
||||
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,
|
||||
|
|
Loading…
Reference in New Issue