diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 9c26f8f120..0075dcb511 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -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; } diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss index e13b1b6802..b3d4275f60 100644 --- a/res/css/views/right_panel/_EncryptionInfo.scss +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -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; } diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index c73691611d..b18776e0ea 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -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 = ; title = _t("Verify this login"); } else if (phase === PHASE_DONE) { diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 3e7264dfec..32f0f41024 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -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 { ; } - const brand = SdkConfig.get().brand; + let verifyButton; + if (store.hasDevicesToVerifyAgainst) { + verifyButton = + { _t("Verify with another session") } + ; + } return (

{_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.", )}

-

{_t( - "This requires the latest %(brand)s on your other devices:", - { brand }, - )}

- -
-
-
{_t("%(brand)s Web", { brand })}
-
{_t("%(brand)s Desktop", { brand })}
-
-
-
{_t("%(brand)s iOS", { brand })}
-
{_t("%(brand)s Android", { brand })}
-
-

{_t("or another cross-signing capable Matrix client")}

-
+ {verifyButton} {useRecoveryKeyButton} {_t("Skip")} @@ -215,7 +223,7 @@ export default class SetupEncryptionBody extends React.Component {
); - } else if (phase === PHASE_BUSY) { + } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.js index e17501da40..5172f29405 100644 --- a/src/components/views/dialogs/NewSessionReviewDialog.js +++ b/src/components/views/dialogs/NewSessionReviewDialog.js @@ -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(); + }, }); } diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js index 3a6e9a2d10..2a5b7ae699 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.js @@ -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 ; } - - async onFinished() { - this.props.onFinished(); - let request = this.props.verificationRequest; - if (!request && this.props.verificationRequestPromise) { - request = await this.props.verificationRequestPromise; - } - request.cancel(); - } } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index eb47a56269..5fbfd03bd1 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -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 = (

+ { + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_SECURITY_TAB, + }); + }}> + { _t("Edit devices") } + +

) + } + const securitySection = (

{ _t("Security") }

@@ -1376,6 +1391,7 @@ const BasicUserInfo: React.FC<{ loading={showDeviceListSpinner} devices={devices} userId={member.userId} /> } + { editDevices }
); diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 8c8a74b2be..82c6b0b952 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -38,6 +38,7 @@ interface IProps { interface IState { counter: number; device?: DeviceInfo; + ip?: string; } export default class VerificationRequestToast extends React.PureComponent { @@ -66,9 +67,15 @@ export default class VerificationRequestToast extends React.PureComponent { + request.cancel(); + }, }, null, /* priority = */ false, /* static = */ true); } await request.accept(); @@ -131,9 +141,10 @@ export default class VerificationRequestToast extends React.PureComponentanonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.", "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 requests.": "Your server isn't responding to some requests.", - "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 to continue:": "Accept 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.", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 981ce6eca9..3839f27a77 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -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"); } diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 41717e0804..bc129ebd54 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set) => { 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"), diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index 9dedd2b137..e0ea323033 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -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,