From 27ee90cad58666a97e3a61993f3a7c2575753aa2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 15 Jan 2020 21:13:56 +0000 Subject: [PATCH 1/4] Add post-login complete security flow This adds a step after login to complete security for your new session. At the moment, the only verification method is entering your SSSS passphrase, but nicer paths will be added soon. This new step only appears when crypto is available and the account has cross-signing enabled in SSSS. Fixes https://github.com/vector-im/riot-web/issues/11214 --- res/css/_components.scss | 1 + .../structures/auth/_CompleteSecurity.scss | 51 ++++++ src/MatrixClientPeg.js | 9 +- .../CreateSecretStorageDialog.js | 2 +- src/components/structures/MatrixChat.js | 90 ++++++--- .../structures/auth/CompleteSecurity.js | 173 ++++++++++++++++++ src/components/structures/auth/SoftLogout.js | 2 +- src/i18n/strings/en_EN.json | 10 +- src/settings/Settings.js | 3 +- 9 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 res/css/structures/auth/_CompleteSecurity.scss create mode 100644 src/components/structures/auth/CompleteSecurity.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 7a9ebfdf26b..a9a114a4cf5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -28,6 +28,7 @@ @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_ViewSource.scss"; +@import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss new file mode 100644 index 00000000000..c258ce4ec7e --- /dev/null +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -0,0 +1,51 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_CompleteSecurity_header { + display: flex; + align-items: center; +} + +.mx_CompleteSecurity_headerIcon { + width: 24px; + height: 24px; + margin: 0 4px; + position: relative; +} + +.mx_CompleteSecurity_heroIcon { + width: 128px; + height: 128px; + position: relative; + margin: 0 auto; +} + +.mx_CompleteSecurity_body { + font-size: 15px; +} + +.mx_CompleteSecurity_actionRow { + display: flex; + justify-content: flex-end; + + .mx_AccessibleButton { + margin-inline-start: 18px; + + &.warning { + color: $warning-color; + } + } +} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 9c939f2fd37..dbc570c8729 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. Copyright 2017, 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -223,9 +223,10 @@ class _MatrixClientPeg { }; opts.cryptoCallbacks = {}; - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - } + // These are always installed regardless of the labs flag so that + // cross-signing features can toggle on without reloading and also be + // accessed immediately after login. + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); this.matrixClient = createMatrixClient(opts); diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 628214a2bbc..01b9c9c7c8d 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -313,7 +313,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

{_t( "Secret Storage will be set up using your existing key backup details. " + "Your secret storage passphrase and recovery key will be the same as " + - " they were for your key backup", + "they were for your key backup.", )}

{ + const actionHandlerRef = dis.register(payload => { + if (payload.action !== "on_logged_in") { + return; + } + dis.unregister(actionHandlerRef); + resolve(); + }); + }); + + const cli = MatrixClientPeg.get(); + // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` + // because the client hasn't been started yet. + if (!isCryptoAvailable()) { + this._onLoggedIn(); + } + + // Test for the master cross-signing key in SSSS as a quick proxy for + // whether cross-signing has been set up on the account. + let masterKeyInStorage = false; + try { + masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master"); + } catch (e) { + if (e.errcode !== "M_NOT_FOUND") throw e; + } + + if (masterKeyInStorage) { + this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); + } else { + this._onLoggedIn(); + } + }, + + onCompleteSecurityFinished() { + this._onLoggedIn(); + }, + render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); let view; - if ( - this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN - ) { + if (this.state.view === VIEWS.LOADING) { const Spinner = sdk.getComponent('elements.Spinner'); view = (
); + } else if (this.state.view === VIEWS.COMPLETE_SECURITY) { + const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity'); + view = ( + + ); } else if (this.state.view === VIEWS.POST_REGISTRATION) { // needs to be before normal PageTypes as you are logged in technically const PostRegistration = sdk.getComponent('structures.auth.PostRegistration'); @@ -1921,7 +1965,7 @@ export default createReactClass({ const Login = sdk.getComponent('structures.auth.Login'); view = ( { + const cli = MatrixClientPeg.get(); + await accessSecretStorage(async () => { + await cli.checkOwnCrossSigningTrust(); + }); + this.setState({ + phase: PHASE_DONE, + }); + } + + onSkipClick = () => { + this.setState({ + phase: PHASE_CONFIRM_SKIP, + }); + } + + onSkipConfirmClick = () => { + this.props.onFinished(); + } + + onSkipBackClick = () => { + this.setState({ + phase: PHASE_INTRO, + }); + } + + onDoneClick = () => { + this.props.onFinished(); + } + + render() { + const AuthPage = sdk.getComponent("auth.AuthPage"); + const AuthHeader = sdk.getComponent("auth.AuthHeader"); + const AuthBody = sdk.getComponent("auth.AuthBody"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const { + phase, + } = this.state; + + let icon; + let title; + let body; + if (phase === PHASE_INTRO) { + icon = ; + title = _t("Complete security"); + body = ( +
+

{_t( + "Verify this session to grant it access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Start")} + +
+
+ ); + } else if (phase === PHASE_DONE) { + icon = ; + title = _t("Session verified"); + body = ( +
+
+

{_t( + "Your new session is now verified. It has access to your " + + "encrypted messages, and other users will see it as trusted.", + )}

+
+ + {_t("Done")} + +
+
+ ); + } else if (phase === PHASE_CONFIRM_SKIP) { + icon = ; + title = _t("Are you sure?"); + body = ( +
+

{_t( + "Without completing security on this device, it won’t have " + + "access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Go Back")} + +
+
+ ); + } else { + throw new Error(`Unknown phase ${phase}`); + } + + return ( + + + +

+ {icon} + {title} +

+
+ {body} +
+
+
+ ); + } +} diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 63f590da2ed..40800ad907f 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -66,7 +66,7 @@ export default class SoftLogout extends React.Component { componentDidMount(): void { // We've ended up here when we don't need to - navigate to login if (!Lifecycle.isSoftLogout()) { - dis.dispatch({action: "on_logged_in"}); + dis.dispatch({action: "start_login"}); return; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 42c87172b82..3756b4c60b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1848,6 +1848,14 @@ "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Could not load user profile": "Could not load user profile", + "Complete security": "Complete security", + "Verify this session to grant it access to encrypted messages.": "Verify this session to grant it access to encrypted messages.", + "Start": "Start", + "Session verified": "Session verified", + "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.", + "Done": "Done", + "Without completing security on this device, it won’t have access to encrypted messages.": "Without completing security on this device, it won’t have access to encrypted messages.", + "Go Back": "Go Back", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", @@ -1952,7 +1960,7 @@ "Import": "Import", "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.", "Restore": "Restore", - "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup", + "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.", "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", "Warning: You should only set up secret storage from a trusted computer.": "Warning: You should only set up secret storage from a trusted computer.", "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 8ee8d898904..2b8c0aef89f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2018, 2019 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -153,7 +153,6 @@ export const SETTINGS = { displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), supportedLevels: LEVELS_FEATURE, default: false, - controller: new ReloadOnChangeController(), }, "feature_event_indexing": { isFeature: true, From 6e027badc0b0f5e7a32ec340b0aa33033d658896 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 15 Jan 2020 22:10:59 +0000 Subject: [PATCH 2/4] Tweak comparison Co-Authored-By: Travis Ralston --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d9aa5e902dd..edc05010864 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1566,7 +1566,7 @@ export default createReactClass({ dis.dispatch({ action: 'view_my_groups', }); - } else if (screen == 'complete_security') { + } else if (screen === 'complete_security') { dis.dispatch({ action: 'start_complete_security', }); From 71fa3222fef38715584c877c7e499e2f98941ded Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 15 Jan 2020 22:11:22 +0000 Subject: [PATCH 3/4] Fix component index import Co-Authored-By: Travis Ralston --- src/components/structures/auth/CompleteSecurity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 0bc41a8fbbf..77f7fe26e4b 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { accessSecretStorage } from '../../../CrossSigningManager'; From 5926e277c485d7833c6b75b52ccdbfc33db6034e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 16 Jan 2020 11:52:02 +0000 Subject: [PATCH 4/4] Avoid logged in event race --- src/components/structures/MatrixChat.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index edc05010864..c59c44ebd8f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1820,12 +1820,9 @@ export default createReactClass({ }, async onUserCompletedLoginFlow(credentials) { - // Create and start the client in the background - Lifecycle.setLoggedIn(credentials); - // Wait for the client to be logged in (but not started) // which is enough to ask the server about account data. - await new Promise(resolve => { + const loggedIn = new Promise(resolve => { const actionHandlerRef = dis.register(payload => { if (payload.action !== "on_logged_in") { return; @@ -1835,6 +1832,10 @@ export default createReactClass({ }); }); + // Create and start the client in the background + Lifecycle.setLoggedIn(credentials); + await loggedIn; + const cli = MatrixClientPeg.get(); // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` // because the client hasn't been started yet.