From af2302265af29751f6dd6d6022d4618ca8770756 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 13:36:59 +0000 Subject: [PATCH 01/26] Convert CreateKeyBackupDialog to class --- .../keybackup/CreateKeyBackupDialog.js | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 4953cdff68..c43fdb0626 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +Copyright 2019 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. @@ -15,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; @@ -49,9 +49,11 @@ function selectText(target) { * Walks the user through the process of creating an e2e key backup * on the server. */ -export default createReactClass({ - getInitialState: function() { - return { +export default class CreateKeyBackupDialog extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -60,25 +62,25 @@ export default createReactClass({ zxcvbnResult: null, setPassPhrase: false, }; - }, + } - componentWillMount: function() { + componentWillMount() { this._recoveryKeyNode = null; this._keyBackupInfo = null; this._setZxcvbnResultTimeout = null; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { if (this._setZxcvbnResultTimeout !== null) { clearTimeout(this._setZxcvbnResultTimeout); } - }, + } - _collectRecoveryKeyNode: function(n) { + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; - }, + } - _onCopyClick: function() { + _onCopyClick = () => { selectText(this._recoveryKeyNode); const successful = document.execCommand('copy'); if (successful) { @@ -87,9 +89,9 @@ export default createReactClass({ phase: PHASE_KEEPITSAFE, }); } - }, + } - _onDownloadClick: function() { + _onDownloadClick = () => { const blob = new Blob([this._keyBackupInfo.recovery_key], { type: 'text/plain;charset=us-ascii', }); @@ -99,9 +101,9 @@ export default createReactClass({ downloaded: true, phase: PHASE_KEEPITSAFE, }); - }, + } - _createBackup: async function() { + _createBackup = async () => { this.setState({ phase: PHASE_BACKINGUP, error: null, @@ -128,38 +130,38 @@ export default createReactClass({ error: e, }); } - }, + } - _onCancel: function() { + _onCancel = () => { this.props.onFinished(false); - }, + } - _onDone: function() { + _onDone = () => { this.props.onFinished(true); - }, + } - _onOptOutClick: function() { + _onOptOutClick = () => { this.setState({phase: PHASE_OPTOUT_CONFIRM}); - }, + } - _onSetUpClick: function() { + _onSetUpClick = () => { this.setState({phase: PHASE_PASSPHRASE}); - }, + } - _onSkipPassPhraseClick: async function() { + _onSkipPassPhraseClick = async () => { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, downloaded: false, phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseNextClick: function() { + _onPassPhraseNextClick = () => { this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); - }, + } - _onPassPhraseKeyPress: async function(e) { + _onPassPhraseKeyPress = async (e) => { if (e.key === 'Enter') { // If we're waiting for the timeout before updating the result at this point, // skip ahead and do it now, otherwise we'll deny the attempt to proceed @@ -177,9 +179,9 @@ export default createReactClass({ this._onPassPhraseNextClick(); } } - }, + } - _onPassPhraseConfirmNextClick: async function() { + _onPassPhraseConfirmNextClick = async () => { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ setPassPhrase: true, @@ -187,30 +189,30 @@ export default createReactClass({ downloaded: false, phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseConfirmKeyPress: function(e) { + _onPassPhraseConfirmKeyPress = (e) => { if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { this._onPassPhraseConfirmNextClick(); } - }, + } - _onSetAgainClick: function() { + _onSetAgainClick = () => { this.setState({ passPhrase: '', passPhraseConfirm: '', phase: PHASE_PASSPHRASE, zxcvbnResult: null, }); - }, + } - _onKeepItSafeBackClick: function() { + _onKeepItSafeBackClick = () => { this.setState({ phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseChange: function(e) { + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); @@ -227,19 +229,19 @@ export default createReactClass({ zxcvbnResult: scorePassword(this.state.passPhrase), }); }, PASSPHRASE_FEEDBACK_DELAY); - }, + } - _onPassPhraseConfirmChange: function(e) { + _onPassPhraseConfirmChange = (e) => { this.setState({ passPhraseConfirm: e.target.value, }); - }, + } - _passPhraseIsValid: function() { + _passPhraseIsValid() { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - }, + } - _renderPhasePassPhrase: function() { + _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let strengthMeter; @@ -305,9 +307,9 @@ export default createReactClass({

; - }, + } - _renderPhasePassPhraseConfirm: function() { + _renderPhasePassPhraseConfirm() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let matchText; @@ -361,9 +363,9 @@ export default createReactClass({ disabled={this.state.passPhrase !== this.state.passPhraseConfirm} /> ; - }, + } - _renderPhaseShowKey: function() { + _renderPhaseShowKey() { let bodyText; if (this.state.setPassPhrase) { bodyText = _t( @@ -402,9 +404,9 @@ export default createReactClass({ ; - }, + } - _renderPhaseKeepItSafe: function() { + _renderPhaseKeepItSafe() { let introText; if (this.state.copied) { introText = _t( @@ -431,16 +433,16 @@ export default createReactClass({ ; - }, + } - _renderBusyPhase: function(text) { + _renderBusyPhase(text) { const Spinner = sdk.getComponent('views.elements.Spinner'); return
; - }, + } - _renderPhaseDone: function() { + _renderPhaseDone() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( @@ -451,9 +453,9 @@ export default createReactClass({ hasCancel={false} />

; - }, + } - _renderPhaseOptOutConfirm: function() { + _renderPhaseOptOutConfirm() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
{_t( @@ -467,9 +469,9 @@ export default createReactClass({
; - }, + } - _titleForPhase: function(phase) { + _titleForPhase(phase) { switch (phase) { case PHASE_PASSPHRASE: return _t('Secure your backup with a passphrase'); @@ -488,9 +490,9 @@ export default createReactClass({ default: return _t("Create Key Backup"); } - }, + } - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); let content; @@ -543,5 +545,5 @@ export default createReactClass({ ); - }, -}); + } +} From cf26f14644172c167f80ce21ed1c9af0d1d34f03 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 18 Nov 2019 12:50:54 +0000 Subject: [PATCH 02/26] Switch to function properties to avoid manual binding in KeyBackupPanel --- src/components/views/settings/KeyBackupPanel.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index ec1e52a90c..3d00695e73 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -25,13 +25,6 @@ export default class KeyBackupPanel extends React.PureComponent { constructor(props) { super(props); - this._startNewBackup = this._startNewBackup.bind(this); - this._deleteBackup = this._deleteBackup.bind(this); - this._onKeyBackupSessionsRemaining = - this._onKeyBackupSessionsRemaining.bind(this); - this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this); - this._restoreBackup = this._restoreBackup.bind(this); - this._unmounted = false; this.state = { loading: true, @@ -63,13 +56,13 @@ export default class KeyBackupPanel extends React.PureComponent { } } - _onKeyBackupSessionsRemaining(sessionsRemaining) { + _onKeyBackupSessionsRemaining = (sessionsRemaining) => { this.setState({ sessionsRemaining, }); } - _onKeyBackupStatus() { + _onKeyBackupStatus = () => { // This just loads the current backup status rather than forcing // a re-check otherwise we risk causing infinite loops this._loadBackupStatus(); @@ -120,7 +113,7 @@ export default class KeyBackupPanel extends React.PureComponent { } } - _startNewBackup() { + _startNewBackup = () => { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), { @@ -131,7 +124,7 @@ export default class KeyBackupPanel extends React.PureComponent { ); } - _deleteBackup() { + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { title: _t('Delete Backup'), @@ -151,7 +144,7 @@ export default class KeyBackupPanel extends React.PureComponent { }); } - _restoreBackup() { + _restoreBackup = () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { }); From 9dea84892720c760dfd1d241d633b87a2c87a6ab Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 19 Nov 2019 16:28:49 +0000 Subject: [PATCH 03/26] Use div around buttons to fix React warning --- res/css/views/settings/_KeyBackupPanel.scss | 4 ++++ src/components/views/settings/KeyBackupPanel.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/_KeyBackupPanel.scss b/res/css/views/settings/_KeyBackupPanel.scss index 1bcc0ab10d..4c4190c604 100644 --- a/res/css/views/settings/_KeyBackupPanel.scss +++ b/res/css/views/settings/_KeyBackupPanel.scss @@ -30,3 +30,7 @@ limitations under the License. .mx_KeyBackupPanel_deviceName { font-style: italic; } + +.mx_KeyBackupPanel_buttonRow { + margin: 1em 0; +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 3d00695e73..67d2d32d50 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -288,14 +288,14 @@ export default class KeyBackupPanel extends React.PureComponent {
{backupSigStatuses}
{trustedLocally}
-

+

{restoreButtonCaption}     { _t("Delete Backup") } -

+
; } else { return
From c568c15186ab475d7a310ea5735ec31076f46486 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 17:35:10 +0000 Subject: [PATCH 04/26] In-memory keys need an object --- src/MatrixClientPeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ef0130ec15..30983c452a 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -223,7 +223,7 @@ class MatrixClientPeg { if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { // TODO: Cross-signing keys are temporarily in memory only. A // separate task in the cross-signing project will build from here. - const keys = []; + const keys = {}; opts.cryptoCallbacks = { getCrossSigningKey: k => keys[k], saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), From e6dea37693d1db03d680f469d683aa7c30084016 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 17:56:44 +0000 Subject: [PATCH 05/26] Add hidden button for bootstrapping SSSS This adds an testing button to the key backup panel which bootstraps the Secure Secret Storage system (and also cross-signing keys). Fixes https://github.com/vector-im/riot-web/issues/11212 --- .../views/settings/KeyBackupPanel.js | 46 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 67d2d32d50..f4740ea649 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -20,6 +20,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import SettingsStore from '../../../../lib/settings/SettingsStore'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -124,6 +125,27 @@ export default class KeyBackupPanel extends React.PureComponent { ); } + _bootstrapSecureSecretStorage = async () => { + try { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await MatrixClientPeg.get().bootstrapSecretStorage({ + doInteractiveAuthFlow: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + await finished; + }, + }); + } catch (e) { + console.error(e); + } + } + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { @@ -298,6 +320,21 @@ export default class KeyBackupPanel extends React.PureComponent {
; } else { + // This is a temporary button for testing SSSS. Initialising SSSS + // depends on cross-signing and is part of the same project, so we + // only show this mode when the cross-signing feature is enabled. + // TODO: Clean this up when removing the feature flag. + let bootstrapSecureSecretStorage; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + bootstrapSecureSecretStorage = ( +
+ + {_t("Bootstrap Secure Secret Storage (MSC1946)")} + +
+ ); + } + return

{_t( @@ -307,9 +344,12 @@ export default class KeyBackupPanel extends React.PureComponent {

{encryptedMessageAreEncrypted}

{_t("Back up your keys before signing out to avoid losing them.")}

- - { _t("Start using Key Backup") } - +
+ + {_t("Start using Key Backup")} + +
+ {bootstrapSecureSecretStorage}
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c42a137800..e60007be5e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -511,6 +511,7 @@ "Connecting to integrations server...": "Connecting to integrations server...", "Cannot connect to integrations server": "Cannot connect to integrations server", "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -533,6 +534,7 @@ "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", + "Bootstrap Secure Secret Storage (MSC1946)": "Bootstrap Secure Secret Storage (MSC1946)", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Start using Key Backup": "Start using Key Backup", From 812b0785bf704bb65a6a6b19819860d42a928132 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 10:22:31 -0700 Subject: [PATCH 06/26] Fix i18n post-merge --- src/i18n/strings/en_EN.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9feded09b6..367450656e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,15 +507,10 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integrations server configured": "No integrations server configured", - "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", - "Connecting to integrations server...": "Connecting to integrations server...", - "Cannot connect to integrations server": "Cannot connect to integrations server", - "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", - "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", From b55a1a107788f027ca8cc85afc81edac79fe37e1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:39:04 +0000 Subject: [PATCH 07/26] Appease linter --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c43fdb0626..ba75032ea4 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -45,7 +45,7 @@ function selectText(target) { selection.addRange(range); } -/** +/* * Walks the user through the process of creating an e2e key backup * on the server. */ From c103fe42738161c6a0aaf7b342af64ba29114033 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 28 Nov 2019 16:45:29 +0000 Subject: [PATCH 08/26] Add cross-signing diagnostic panel This is not part of any designs, so it may be short-lived, but it's quite handy for diagnosing issues with cross-signing at least while the feature is in development. --- res/css/_components.scss | 1 + .../views/settings/_CrossSigningPanel.scss | 31 ++++++ res/css/views/settings/_KeyBackupPanel.scss | 1 + .../views/settings/CrossSigningPanel.js | 100 ++++++++++++++++++ .../views/settings/KeyBackupPanel.js | 39 +------ .../tabs/user/SecurityUserSettingsTab.js | 20 +++- src/i18n/strings/en_EN.json | 12 ++- 7 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 res/css/views/settings/_CrossSigningPanel.scss create mode 100644 src/components/views/settings/CrossSigningPanel.js diff --git a/res/css/_components.scss b/res/css/_components.scss index e39003fbec..eb0181aee1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -171,6 +171,7 @@ @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; +@import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationManager.scss"; diff --git a/res/css/views/settings/_CrossSigningPanel.scss b/res/css/views/settings/_CrossSigningPanel.scss new file mode 100644 index 0000000000..fa9f76a963 --- /dev/null +++ b/res/css/views/settings/_CrossSigningPanel.scss @@ -0,0 +1,31 @@ +/* +Copyright 2019 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_CrossSigningPanel_statusList { + border-spacing: 0; + + td { + padding: 0; + + &:first-of-type { + padding-inline-end: 1em; + } + } +} + +.mx_CrossSigningPanel_buttonRow { + margin: 1em 0; +} diff --git a/res/css/views/settings/_KeyBackupPanel.scss b/res/css/views/settings/_KeyBackupPanel.scss index 4c4190c604..872162caad 100644 --- a/res/css/views/settings/_KeyBackupPanel.scss +++ b/res/css/views/settings/_KeyBackupPanel.scss @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 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. diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js new file mode 100644 index 0000000000..c4715648f9 --- /dev/null +++ b/src/components/views/settings/CrossSigningPanel.js @@ -0,0 +1,100 @@ +/* +Copyright 2019 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. +*/ + +import React from 'react'; + +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import Modal from '../../../Modal'; + +export default class CrossSigningPanel extends React.PureComponent { + constructor(props) { + super(props); + this.state = this._getUpdatedStatus(); + } + + _getUpdatedStatus() { + // XXX: Add public accessors if we keep this around in production + const cli = MatrixClientPeg.get(); + const crossSigning = cli._crypto._crossSigningInfo; + const secretStorage = cli._crypto._secretStorage; + const crossSigningPublicKeysOnDevice = crossSigning.getId(); + const crossSigningPrivateKeysInStorage = crossSigning.isStoredInSecretStorage(secretStorage); + const secretStorageKeyInAccount = secretStorage.hasKey(); + + return { + crossSigningPublicKeysOnDevice, + crossSigningPrivateKeysInStorage, + secretStorageKeyInAccount, + }; + } + + _bootstrapSecureSecretStorage = async () => { + try { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await MatrixClientPeg.get().bootstrapSecretStorage({ + doInteractiveAuthFlow: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + await finished; + }, + }); + this.setState(this._getUpdatedStatus()); + } catch (e) { + console.error(e); + } + } + + render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + const { + crossSigningPublicKeysOnDevice, + crossSigningPrivateKeysInStorage, + secretStorageKeyInAccount, + } = this.state; + + return ( +
+ + + + + + + + + + + + + +
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
+ + {_t("Bootstrap Secure Secret Storage")} + +
+
+ ); + } +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index f4740ea649..c2fb3dc9db 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 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. @@ -20,7 +21,6 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; -import SettingsStore from '../../../../lib/settings/SettingsStore'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -125,27 +125,6 @@ export default class KeyBackupPanel extends React.PureComponent { ); } - _bootstrapSecureSecretStorage = async () => { - try { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await MatrixClientPeg.get().bootstrapSecretStorage({ - doInteractiveAuthFlow: async (makeRequest) => { - const { finished } = Modal.createTrackedDialog( - 'Cross-signing keys dialog', '', InteractiveAuthDialog, - { - title: _t("Send cross-signing keys to homeserver"), - matrixClient: MatrixClientPeg.get(), - makeRequest, - }, - ); - await finished; - }, - }); - } catch (e) { - console.error(e); - } - } - _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { @@ -320,21 +299,6 @@ export default class KeyBackupPanel extends React.PureComponent { ; } else { - // This is a temporary button for testing SSSS. Initialising SSSS - // depends on cross-signing and is part of the same project, so we - // only show this mode when the cross-signing feature is enabled. - // TODO: Clean this up when removing the feature flag. - let bootstrapSecureSecretStorage; - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - bootstrapSecureSecretStorage = ( -
- - {_t("Bootstrap Secure Secret Storage (MSC1946)")} - -
- ); - } - return

{_t( @@ -349,7 +313,6 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Start using Key Backup")}

- {bootstrapSecureSecretStorage}
; } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 0732bcf926..98ec18df5a 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../../../languageHandler"; -import {SettingLevel} from "../../../../../settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import * as FormattingUtils from "../../../../../utils/FormattingUtils"; import AccessibleButton from "../../../elements/AccessibleButton"; @@ -252,6 +252,23 @@ export default class SecurityUserSettingsTab extends React.Component { ); + // XXX: There's no such panel in the current cross-signing designs, but + // it's useful to have for testing the feature. If there's no interest + // in having advanced details here once all flows are implemented, we + // can remove this. + const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel'); + let crossSigning; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + crossSigning = ( +
+ {_t("Cross-signing")} +
+ +
+
+ ); + } + return (
{_t("Security & Privacy")}
@@ -263,6 +280,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{keyBackup} + {crossSigning} {this._renderCurrentDeviceInfo()}
{_t("Analytics")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 367450656e..80254ed54e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -496,6 +496,15 @@ "New Password": "New Password", "Confirm password": "Confirm password", "Change Password": "Change Password", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", + "Cross-signing public keys:": "Cross-signing public keys:", + "on device": "on device", + "not found": "not found", + "Cross-signing private keys:": "Cross-signing private keys:", + "in secret storage": "in secret storage", + "Secret storage public key:": "Secret storage public key:", + "in account data": "in account data", + "Bootstrap Secure Secret Storage": "Bootstrap Secure Secret Storage", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", "Authentication": "Authentication", @@ -510,7 +519,6 @@ "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", - "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -533,7 +541,6 @@ "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", - "Bootstrap Secure Secret Storage (MSC1946)": "Bootstrap Secure Secret Storage (MSC1946)", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Start using Key Backup": "Start using Key Backup", @@ -697,6 +704,7 @@ "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Key backup": "Key backup", + "Cross-signing": "Cross-signing", "Security & Privacy": "Security & Privacy", "Devices": "Devices", "A device's public name is visible to people you communicate with": "A device's public name is visible to people you communicate with", From a21285143f74314a6873d27045a0e2226b9936a4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 29 Nov 2019 11:55:36 +0000 Subject: [PATCH 09/26] Add tbody to silence React warning --- src/components/views/settings/CrossSigningPanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index c4715648f9..9c7f04555a 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -75,7 +75,7 @@ export default class CrossSigningPanel extends React.PureComponent { return (
- +
@@ -88,7 +88,7 @@ export default class CrossSigningPanel extends React.PureComponent { -
{_t("Cross-signing public keys:")} {crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}{_t("Secret storage public key:")} {secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
{_t("Bootstrap Secure Secret Storage")} From 92c0fdf085b336c39aa6c7a030c1fbdff738d3b8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 29 Nov 2019 15:57:40 +0000 Subject: [PATCH 10/26] Clarify current state of cross-signing private keys --- src/MatrixClientPeg.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 30983c452a..a65ebbb763 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -221,8 +221,14 @@ class MatrixClientPeg { }; if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - // TODO: Cross-signing keys are temporarily in memory only. A - // separate task in the cross-signing project will build from here. + // This stores the cross-signing private keys in memory for the JS SDK. They + // are also persisted to Secure Secret Storage in account data by + // the JS SDK when created. + // XXX: On desktop platforms, we plan to store only the SSSS default + // key in a secure enclave, while the cross-signing private keys + // will still be retrieved from SSSS, so it's unclear that we + // actually need these cross-signing application callbacks for Riot. + // Should the JS SDK default to in-memory storage of these itself? const keys = {}; opts.cryptoCallbacks = { getCrossSigningKey: k => keys[k], From 6140803b7f74454d2549e36a2cdba752b8afe649 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 29 Nov 2019 17:43:24 +0000 Subject: [PATCH 11/26] Fix key upload auth to test confirmation --- src/components/views/settings/CrossSigningPanel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 9c7f04555a..de4d7bccbf 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -56,7 +56,10 @@ export default class CrossSigningPanel extends React.PureComponent { makeRequest, }, ); - await finished; + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } }, }); this.setState(this._getUpdatedStatus()); From c32c1d201c9ff645240ed065826d9f923a2ae0cd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 29 Nov 2019 17:49:51 +0000 Subject: [PATCH 12/26] Rename device signing auth param --- src/components/views/settings/CrossSigningPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index de4d7bccbf..6c11c4d5c3 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -47,7 +47,7 @@ export default class CrossSigningPanel extends React.PureComponent { try { const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); await MatrixClientPeg.get().bootstrapSecretStorage({ - doInteractiveAuthFlow: async (makeRequest) => { + authUploadDeviceSigningKeys: async (makeRequest) => { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, { From 798d5c8ada8184805c290be094e570188c351889 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 29 Nov 2019 17:53:31 +0000 Subject: [PATCH 13/26] Always update cross-signing status even if error --- src/components/views/settings/CrossSigningPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 6c11c4d5c3..58f452674b 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -62,10 +62,10 @@ export default class CrossSigningPanel extends React.PureComponent { } }, }); - this.setState(this._getUpdatedStatus()); } catch (e) { console.error(e); } + this.setState(this._getUpdatedStatus()); } render() { From c21c0e1150092a461ff330947c9f3b909438d759 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 2 Dec 2019 14:22:47 +0000 Subject: [PATCH 14/26] Add error to debug panel --- src/components/views/settings/CrossSigningPanel.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 58f452674b..027f6bea26 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -24,7 +24,10 @@ import Modal from '../../../Modal'; export default class CrossSigningPanel extends React.PureComponent { constructor(props) { super(props); - this.state = this._getUpdatedStatus(); + this.state = { + error: null, + ...this._getUpdatedStatus(), + }; } _getUpdatedStatus() { @@ -44,6 +47,7 @@ export default class CrossSigningPanel extends React.PureComponent { } _bootstrapSecureSecretStorage = async () => { + this.setState({ error: null }); try { const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); await MatrixClientPeg.get().bootstrapSecretStorage({ @@ -63,6 +67,7 @@ export default class CrossSigningPanel extends React.PureComponent { }, }); } catch (e) { + this.setState({ error: e }); console.error(e); } this.setState(this._getUpdatedStatus()); @@ -71,11 +76,17 @@ export default class CrossSigningPanel extends React.PureComponent { render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const { + error, crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, secretStorageKeyInAccount, } = this.state; + let errorSection; + if (error) { + errorSection =
{error.toString()}
; + } + return (
@@ -97,6 +108,7 @@ export default class CrossSigningPanel extends React.PureComponent { {_t("Bootstrap Secure Secret Storage")} + {errorSection} ); } From 139e19630a6e28019af26f5300eb7fa46cd27aa3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 2 Dec 2019 14:34:32 +0000 Subject: [PATCH 15/26] Watch for account data changes in debug panel --- .../views/settings/CrossSigningPanel.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 027f6bea26..9c7c2ea38a 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -30,6 +30,24 @@ export default class CrossSigningPanel extends React.PureComponent { }; } + componentDidMount() { + const cli = MatrixClientPeg.get(); + cli.on("accountData", this.onAccountData); + } + + componentWillUnmount() { + const cli = MatrixClientPeg.get(); + if (!cli) return; + cli.removeListener("accountData", this.onAccountData); + } + + onAccountData = (event) => { + const type = event.getType(); + if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) { + this.setState(this._getUpdatedStatus()); + } + }; + _getUpdatedStatus() { // XXX: Add public accessors if we keep this around in production const cli = MatrixClientPeg.get(); From a7d94ebcaac062255963e25853e05f790156a373 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 4 Dec 2019 17:23:48 +0000 Subject: [PATCH 16/26] Convert RestoreKeyBackupDialog to modern style --- .../keybackup/RestoreKeyBackupDialog.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 300e6b7f18..9fcb663af9 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; @@ -31,9 +30,10 @@ const RESTORE_TYPE_RECOVERYKEY = 1; /** * Dialog for restoring e2e keys from a backup and the user's recovery key */ -export default createReactClass({ - getInitialState: function() { - return { +export default class RestoreKeyBackupDialog extends React.PureComponent { + constructor(props) { + super(props); + this.state = { backupInfo: null, loading: false, loadError: null, @@ -45,27 +45,27 @@ export default createReactClass({ passPhrase: '', restoreType: null, }; - }, + } - componentWillMount: function() { + componentWillMount() { this._loadBackupStatus(); - }, + } - _onCancel: function() { + _onCancel = () => { this.props.onFinished(false); - }, + } - _onDone: function() { + _onDone = () => { this.props.onFinished(true); - }, + } - _onUseRecoveryKeyClick: function() { + _onUseRecoveryKeyClick = () => { this.setState({ forceRecoveryKey: true, }); - }, + } - _onResetRecoveryClick: function() { + _onResetRecoveryClick = () => { this.props.onFinished(false); Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), @@ -75,16 +75,16 @@ export default createReactClass({ }, }, ); - }, + } - _onRecoveryKeyChange: function(e) { + _onRecoveryKeyChange = (e) => { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), }); - }, + } - _onPassPhraseNext: async function() { + _onPassPhraseNext = async () => { this.setState({ loading: true, restoreError: null, @@ -105,9 +105,9 @@ export default createReactClass({ restoreError: e, }); } - }, + } - _onRecoveryKeyNext: async function() { + _onRecoveryKeyNext = async () => { this.setState({ loading: true, restoreError: null, @@ -128,27 +128,27 @@ export default createReactClass({ restoreError: e, }); } - }, + } - _onPassPhraseChange: function(e) { + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - }, + } - _onPassPhraseKeyPress: function(e) { + _onPassPhraseKeyPress = (e) => { if (e.key === Key.ENTER) { this._onPassPhraseNext(); } - }, + } - _onRecoveryKeyKeyPress: function(e) { + _onRecoveryKeyKeyPress = (e) => { if (e.key === Key.ENTER && this.state.recoveryKeyValid) { this._onRecoveryKeyNext(); } - }, + } - _loadBackupStatus: async function() { + async _loadBackupStatus() { this.setState({ loading: true, loadError: null, @@ -167,9 +167,9 @@ export default createReactClass({ loading: false, }); } - }, + } - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -345,5 +345,5 @@ export default createReactClass({ ); - }, -}); + } +} From 2a8853dd82747e95cacb4505b26652fc89b11acb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 4 Dec 2019 17:24:49 +0000 Subject: [PATCH 17/26] Remove duplicate dialog CSS --- res/css/_components.scss | 1 - .../dialogs/_RestoreKeyBackupDialog.scss | 19 ------------------- .../keybackup/_RestoreKeyBackupDialog.scss | 5 +++++ 3 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 res/css/views/dialogs/_RestoreKeyBackupDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index b174b95598..9796b59213 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -64,7 +64,6 @@ @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; -@import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; diff --git a/res/css/views/dialogs/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/_RestoreKeyBackupDialog.scss deleted file mode 100644 index 69e00c416a..0000000000 --- a/res/css/views/dialogs/_RestoreKeyBackupDialog.scss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2018 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. -*/ - -.mx_RestoreKeyBackupDialog_keyStatus { - height: 30px; -} diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss index 415a2021cc..9cba8e0da9 100644 --- a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 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. @@ -14,6 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_RestoreKeyBackupDialog_keyStatus { + height: 30px; +} + .mx_RestoreKeyBackupDialog_primaryContainer { /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ padding: 20px; From 9f1c2cd3e15065640677af8a6098f7e0a561cb91 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Dec 2019 15:05:28 +0000 Subject: [PATCH 18/26] Add dialogs for creating and accessing secret storage This adds dialogs for creating and accessing secret storage via a passphrase or recovery key. These flows are adapted from the ones used for key backup. --- res/css/_components.scss | 2 + .../_AccessSecretStorageDialog.scss | 34 ++ .../_CreateSecretStorageDialog.scss | 88 +++ src/MatrixClientPeg.js | 40 +- .../keybackup/CreateKeyBackupDialog.js | 8 +- .../CreateSecretStorageDialog.js | 564 ++++++++++++++++++ .../keybackup/RestoreKeyBackupDialog.js | 8 +- .../AccessSecretStorageDialog.js | 224 +++++++ .../views/settings/CrossSigningPanel.js | 63 +- src/i18n/strings/en_EN.json | 49 +- 10 files changed, 1034 insertions(+), 46 deletions(-) create mode 100644 res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss create mode 100644 res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss create mode 100644 src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js create mode 100644 src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 9796b59213..b1fbe30f13 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -81,6 +81,8 @@ @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; +@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss"; +@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @import "./views/elements/_AddressSelector.scss"; diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss new file mode 100644 index 0000000000..db11e91bdb --- /dev/null +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -0,0 +1,34 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2019 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_AccessSecretStorageDialog_keyStatus { + height: 30px; +} + +.mx_AccessSecretStorageDialog_primaryContainer { + /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ + padding: 20px; +} + +.mx_AccessSecretStorageDialog_passPhraseInput, +.mx_AccessSecretStorageDialog_recoveryKeyInput { + width: 300px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; +} + diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss new file mode 100644 index 0000000000..757d8028f0 --- /dev/null +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -0,0 +1,88 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2019 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_CreateSecretStorageDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} + +.mx_CreateSecretStorageDialog_primaryContainer { + /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ + padding: 20px; +} + +.mx_CreateSecretStorageDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateSecretStorageDialog_passPhraseContainer { + display: flex; + align-items: start; +} + +.mx_CreateSecretStorageDialog_passPhraseHelp { + flex: 1; + height: 85px; + margin-left: 20px; + font-size: 80%; +} + +.mx_CreateSecretStorageDialog_passPhraseHelp progress { + width: 100%; +} + +.mx_CreateSecretStorageDialog_passPhraseInput { + flex: none; + width: 250px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; + margin-bottom: 1em; +} + +.mx_CreateSecretStorageDialog_passPhraseMatch { + margin-left: 20px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + +.mx_CreateSecretStorageDialog_recoveryKeyContainer { + display: flex; +} + +.mx_CreateSecretStorageDialog_recoveryKey { + width: 262px; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; + margin-right: 12px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons { + flex: 1; + display: flex; + align-items: center; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons button { + flex: 1; + white-space: nowrap; +} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a65ebbb763..d73931f57b 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,6 +30,8 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; +import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; interface MatrixClientCreds { homeserverUrl: string, @@ -224,13 +226,41 @@ class MatrixClientPeg { // This stores the cross-signing private keys in memory for the JS SDK. They // are also persisted to Secure Secret Storage in account data by // the JS SDK when created. - // XXX: On desktop platforms, we plan to store only the SSSS default - // key in a secure enclave, while the cross-signing private keys - // will still be retrieved from SSSS, so it's unclear that we - // actually need these cross-signing application callbacks for Riot. - // Should the JS SDK default to in-memory storage of these itself? const keys = {}; opts.cryptoCallbacks = { + // XXX: This flow should maybe be reworked to allow retries in + // case of typos, etc. + getSecretStorageKey: async keyInfos => { + const keyInfoEntries = Object.entries(keyInfos); + if (keyInfoEntries.length > 1) { + throw new Error("Multiple storage key requests not implemented"); + } + const [name, info] = keyInfoEntries[0]; + const AccessSecretStorageDialog = + sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, { + keyInfo: info, + }, + ); + const [input] = await finished; + if (!input) { + throw new Error("Secret storage access canceled"); + } + let key; + const { passphrase } = info; + if (passphrase) { + key = await deriveKey(input, passphrase.salt, passphrase.iterations); + } else { + key = decodeRecoveryKey(input); + } + return [name, key]; + }, + // XXX: On desktop platforms, we plan to store only the SSSS default + // key in a secure enclave, while the cross-signing private keys + // will still be retrieved from SSSS, so it's unclear that we + // actually need these cross-signing application callbacks for Riot. + // Should the JS SDK default to in-memory storage of these itself? getCrossSigningKey: k => keys[k], saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), }; diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index ba75032ea4..eae102196f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -268,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { return

{_t( - "Warning: you should only set up key backup from a trusted computer.", {}, + "Warning: You should only set up key backup from a trusted computer.", {}, { b: sub => {sub} }, )}

{_t( @@ -382,7 +382,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { "access to your encrypted messages if you forget your passphrase.", )}

{_t( - "Keep your recovery key somewhere very secure, like a password manager (or a safe)", + "Keep your recovery key somewhere very secure, like a password manager (or a safe).", )}

{bodyText}

@@ -410,12 +410,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent { let introText; if (this.state.copied) { introText = _t( - "Your Recovery Key has been copied to your clipboard, paste it to:", + "Your recovery key has been copied to your clipboard, paste it to:", {}, {b: s => {s}}, ); } else if (this.state.downloaded) { introText = _t( - "Your Recovery Key is in your Downloads folder.", + "Your recovery key is in your Downloads folder.", {}, {b: s => {s}}, ); } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js new file mode 100644 index 0000000000..78ff2a1698 --- /dev/null +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -0,0 +1,564 @@ +/* +Copyright 2018, 2019 New Vector Ltd +Copyright 2019 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. +*/ + +import React from 'react'; +import sdk from '../../../../index'; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import { scorePassword } from '../../../../utils/PasswordScorer'; +import FileSaver from 'file-saver'; +import { _t } from '../../../../languageHandler'; +import Modal from '../../../../Modal'; + +const PHASE_PASSPHRASE = 0; +const PHASE_PASSPHRASE_CONFIRM = 1; +const PHASE_SHOWKEY = 2; +const PHASE_KEEPITSAFE = 3; +const PHASE_STORING = 4; +const PHASE_DONE = 5; +const PHASE_OPTOUT_CONFIRM = 6; + +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. +const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. + +// XXX: copied from ShareDialog: factor out into utils +function selectText(target) { + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + +/* + * Walks the user through the process of creating a passphrase to guard Secure + * Secret Storage in account data. + */ +export default class CreateSecretStorageDialog extends React.PureComponent { + constructor(props) { + super(props); + + this._keyInfo = null; + this._encodedRecoveryKey = null; + this._recoveryKeyNode = null; + this._setZxcvbnResultTimeout = null; + + this.state = { + phase: PHASE_PASSPHRASE, + passPhrase: '', + passPhraseConfirm: '', + copied: false, + downloaded: false, + zxcvbnResult: null, + setPassPhrase: false, + }; + } + + componentWillUnmount() { + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + } + } + + _collectRecoveryKeyNode = (n) => { + this._recoveryKeyNode = n; + } + + _onCopyClick = () => { + selectText(this._recoveryKeyNode); + const successful = document.execCommand('copy'); + if (successful) { + this.setState({ + copied: true, + phase: PHASE_KEEPITSAFE, + }); + } + } + + _onDownloadClick = () => { + const blob = new Blob([this._encodedRecoveryKey], { + type: 'text/plain;charset=us-ascii', + }); + FileSaver.saveAs(blob, 'recovery-key.txt'); + + this.setState({ + downloaded: true, + phase: PHASE_KEEPITSAFE, + }); + } + + _bootstrapSecretStorage = async () => { + this.setState({ + phase: PHASE_STORING, + error: null, + }); + const cli = MatrixClientPeg.get(); + try { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + }, + createSecretStorageKey: async () => this._keyInfo, + }); + this.setState({ + phase: PHASE_DONE, + }); + } catch (e) { + this.setState({ error: e }); + console.error("Error bootstrapping secret storage", e); + } + } + + _onCancel = () => { + this.props.onFinished(false); + } + + _onDone = () => { + this.props.onFinished(true); + } + + _onOptOutClick = () => { + this.setState({phase: PHASE_OPTOUT_CONFIRM}); + } + + _onSetUpClick = () => { + this.setState({phase: PHASE_PASSPHRASE}); + } + + _onSkipPassPhraseClick = async () => { + const [keyInfo, encodedRecoveryKey] = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this._keyInfo = keyInfo; + this._encodedRecoveryKey = encodedRecoveryKey; + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseNextClick = () => { + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + } + + _onPassPhraseKeyPress = async (e) => { + if (e.key === 'Enter') { + // If we're waiting for the timeout before updating the result at this point, + // skip ahead and do it now, otherwise we'll deny the attempt to proceed + // even if the user entered a valid passphrase + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + this._setZxcvbnResultTimeout = null; + await new Promise((resolve) => { + this.setState({ + zxcvbnResult: scorePassword(this.state.passPhrase), + }, resolve); + }); + } + if (this._passPhraseIsValid()) { + this._onPassPhraseNextClick(); + } + } + } + + _onPassPhraseConfirmNextClick = async () => { + const [keyInfo, encodedRecoveryKey] = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); + this._keyInfo = keyInfo; + this._encodedRecoveryKey = encodedRecoveryKey; + this.setState({ + setPassPhrase: true, + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseConfirmKeyPress = (e) => { + if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { + this._onPassPhraseConfirmNextClick(); + } + } + + _onSetAgainClick = () => { + this.setState({ + passPhrase: '', + passPhraseConfirm: '', + phase: PHASE_PASSPHRASE, + zxcvbnResult: null, + }); + } + + _onKeepItSafeBackClick = () => { + this.setState({ + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseChange = (e) => { + this.setState({ + passPhrase: e.target.value, + }); + + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + } + this._setZxcvbnResultTimeout = setTimeout(() => { + this._setZxcvbnResultTimeout = null; + this.setState({ + // precompute this and keep it in state: zxcvbn is fast but + // we use it in a couple of different places so no point recomputing + // it unnecessarily. + zxcvbnResult: scorePassword(this.state.passPhrase), + }); + }, PASSPHRASE_FEEDBACK_DELAY); + } + + _onPassPhraseConfirmChange = (e) => { + this.setState({ + passPhraseConfirm: e.target.value, + }); + } + + _passPhraseIsValid() { + return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; + } + + _renderPhasePassPhrase() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + let strengthMeter; + let helpText; + if (this.state.zxcvbnResult) { + if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { + helpText = _t("Great! This passphrase looks strong enough."); + } else { + const suggestions = []; + for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { + suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); + } + const suggestionBlock =
{suggestions.length > 0 ? suggestions : _t("Keep going...")}
; + + helpText =
+ {this.state.zxcvbnResult.feedback.warning} + {suggestionBlock} +
; + } + strengthMeter =
+ +
; + } + + return
+

{_t( + "Warning: You should only set up secret storage from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_t( + "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.", + )}

+

{_t("For maximum security, this should be different from your account password.")}

+ +
+
+ +
+ {strengthMeter} + {helpText} +
+
+
+ + + +
+ {_t("Advanced")} +

+
+
; + } + + _renderPhasePassPhraseConfirm() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let matchText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { + // only tell them they're wrong if they've actually gone wrong. + // Security concious readers will note that if you left riot-web unattended + // on this screen, this would make it easy for a malicious person to guess + // your passphrase one letter at a time, but they could get this faster by + // just opening the browser's developer tools and reading it. + // Note that not having typed anything at all will not hit this clause and + // fall through so empty box === no hint. + matchText = _t("That doesn't match."); + } + + let passPhraseMatch = null; + if (matchText) { + passPhraseMatch =
+
{matchText}
+
+ + {_t("Go back to set it again.")} + +
+
; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Please enter your passphrase a second time to confirm.", + )}

+
+
+
+ +
+ {passPhraseMatch} +
+
+ +
; + } + + _renderPhaseShowKey() { + let bodyText; + if (this.state.setPassPhrase) { + bodyText = _t( + "As a safety net, you can use it to restore your access to encrypted " + + "messages if you forget your passphrase.", + ); + } else { + bodyText = _t( + "As a safety net, you can use it to restore your access to encrypted " + + "messages.", + ); + } + + return
+

{_t( + "Your recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your passphrase.", + )}

+

{_t( + "Keep your recovery key somewhere very secure, like a password manager (or a safe).", + )}

+

{bodyText}

+
+
+ {_t("Your Recovery Key")} +
+
+
+ {this._encodedRecoveryKey} +
+
+ + +
+
+
+
; + } + + _renderPhaseKeepItSafe() { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {introText} +
    +
  • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
  • +
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • +
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • +
+ + + +
; + } + + _renderBusyPhase(text) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return
+ +
; + } + + _renderPhaseDone() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Your access to encrypted messages is now protected.", + )}

+ +
; + } + + _renderPhaseOptOutConfirm() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {_t( + "Without setting up secret storage, you won't be able to restore your " + + "access to encrypted messages or your cross-signing identity for " + + "verifying other devices if you log out or use another device.", + )} + + + +
; + } + + _titleForPhase(phase) { + switch (phase) { + case PHASE_PASSPHRASE: + return _t('Secure your encrypted messages with a passphrase'); + case PHASE_PASSPHRASE_CONFIRM: + return _t('Confirm your passphrase'); + case PHASE_OPTOUT_CONFIRM: + return _t('Warning!'); + case PHASE_SHOWKEY: + return _t('Recovery key'); + case PHASE_KEEPITSAFE: + return _t('Keep it safe'); + case PHASE_STORING: + return _t('Storing secrets...'); + case PHASE_DONE: + return _t('Success!'); + default: + return null; + } + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + let content; + if (this.state.error) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + content =
+

{_t("Unable to set up secret storage")}

+
+ +
+
; + } else { + switch (this.state.phase) { + case PHASE_PASSPHRASE: + content = this._renderPhasePassPhrase(); + break; + case PHASE_PASSPHRASE_CONFIRM: + content = this._renderPhasePassPhraseConfirm(); + break; + case PHASE_SHOWKEY: + content = this._renderPhaseShowKey(); + break; + case PHASE_KEEPITSAFE: + content = this._renderPhaseKeepItSafe(); + break; + case PHASE_STORING: + content = this._renderBusyPhase(); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; + case PHASE_OPTOUT_CONFIRM: + content = this._renderPhaseOptOutConfirm(); + break; + } + } + + return ( + +
+ {content} +
+
+ ); + } +} diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 9fcb663af9..45168c381e 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -27,7 +27,7 @@ import {Key} from "../../../../Keyboard"; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; -/** +/* * Dialog for restoring e2e keys from a backup and the user's recovery key */ export default class RestoreKeyBackupDialog extends React.PureComponent { @@ -47,7 +47,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { }; } - componentWillMount() { + componentDidMount() { this._loadBackupStatus(); } @@ -296,7 +296,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { content =

{_t( - "Warning: you should only set up key backup " + + "Warning: You should only set up key backup " + "from a trusted computer.", {}, { b: sub => {sub} }, )}

@@ -322,7 +322,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { />
{_t( - "If you've forgotten your recovery passphrase you can "+ + "If you've forgotten your recovery key you can "+ "" , {}, { button: s => { + this.props.onFinished(false); + } + + _onUseRecoveryKeyClick = () => { + this.setState({ + forceRecoveryKey: true, + }); + } + + _onResetRecoveryClick = () => { + this.props.onFinished(false); + throw new Error("Resetting secret storage unimplemented"); + } + + _onRecoveryKeyChange = (e) => { + this.setState({ + recoveryKey: e.target.value, + recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), + }); + } + + _onPassPhraseNext = async () => { + this.props.onFinished(this.state.passPhrase); + } + + _onRecoveryKeyNext = async () => { + this.props.onFinished(this.state.recoveryKey); + } + + _onPassPhraseChange = (e) => { + this.setState({ + passPhrase: e.target.value, + }); + } + + _onPassPhraseKeyPress = (e) => { + if (e.key === Key.ENTER) { + this._onPassPhraseNext(); + } + } + + _onRecoveryKeyKeyPress = (e) => { + if (e.key === Key.ENTER && this.state.recoveryKeyValid) { + this._onRecoveryKeyNext(); + } + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const hasPassphrase = ( + this.props.keyInfo && + this.props.keyInfo.passphrase && + this.props.keyInfo.passphrase.salt && + this.props.keyInfo.passphrase.iterations + ); + + let content; + let title; + if (hasPassphrase && !this.state.forceRecoveryKey) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + title = _t("Enter secret storage passphrase"); + content =
+

{_t( + "Warning: You should only access secret storage " + + "from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_t( + "Access your secure message history and your cross-signing " + + "identity for verifying other devices by entering your passphrase.", + )}

+ +
+ + +
+ {_t( + "If you've forgotten your passphrase you can "+ + "use your recovery key or " + + "set up new recovery options." + , {}, { + button1: s => + {s} + , + button2: s => + {s} + , + })} +
; + } else { + title = _t("Enter secret storage recovery key"); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let keyStatus; + if (this.state.recoveryKey.length === 0) { + keyStatus =
; + } else if (this.state.recoveryKeyValid) { + keyStatus =
+ {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")} +
; + } else { + keyStatus =
+ {"\uD83D\uDC4E "}{_t("Not a valid recovery key")} +
; + } + + content =
+

{_t( + "Warning: You should only access secret storage " + + "from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_t( + "Access your secure message history and your cross-signing " + + "identity for verifying other devices by entering your recovery key.", + )}

+ +
+ + {keyStatus} + +
+ {_t( + "If you've forgotten your recovery key you can "+ + "." + , {}, { + button: s => + {s} + , + })} +
; + } + + return ( + +
+ {content} +
+
+ ); + } +} diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 9c7c2ea38a..fda92ebac9 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -24,6 +24,9 @@ import Modal from '../../../Modal'; export default class CrossSigningPanel extends React.PureComponent { constructor(props) { super(props); + + this._unmounted = false; + this.state = { error: null, ...this._getUpdatedStatus(), @@ -36,6 +39,7 @@ export default class CrossSigningPanel extends React.PureComponent { } componentWillUnmount() { + this._unmounted = true; const cli = MatrixClientPeg.get(); if (!cli) return; cli.removeListener("accountData", this.onAccountData); @@ -64,30 +68,53 @@ export default class CrossSigningPanel extends React.PureComponent { }; } + /** + * Bootstrapping secret storage may take one of these paths: + * 1. Create secret storage from a passphrase and store cross-signing keys + * in secret storage. + * 2. Access existing secret storage by requesting passphrase and accessing + * cross-signing keys as needed. + * 3. All keys are loaded and there's nothing to do. + */ _bootstrapSecureSecretStorage = async () => { this.setState({ error: null }); + const cli = MatrixClientPeg.get(); try { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await MatrixClientPeg.get().bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async (makeRequest) => { - const { finished } = Modal.createTrackedDialog( - 'Cross-signing keys dialog', '', InteractiveAuthDialog, - { - title: _t("Send cross-signing keys to homeserver"), - matrixClient: MatrixClientPeg.get(), - makeRequest, - }, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - }); + if (!cli.hasSecretStorageKey()) { + // This dialog calls bootstrap itself after guiding the user through + // passphrase creation. + const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', + import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), + null, null, /* priority = */ false, /* static = */ true, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Secret storage creation canceled"); + } + } else { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + }, + }); + } } catch (e) { this.setState({ error: e }); - console.error(e); + console.error("Error bootstrapping secret storage", e); } + if (this._unmounted) return; this.setState(this._getUpdatedStatus()); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b60a684e05..ab26d677a3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1517,6 +1517,15 @@ "Remember my selection for this widget": "Remember my selection for this widget", "Allow": "Allow", "Deny": "Deny", + "Enter secret storage passphrase": "Enter secret storage passphrase", + "Warning: You should only access secret storage from a trusted computer.": "Warning: You should only access secret storage from a trusted computer.", + "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.", + "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.": "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.", + "Enter secret storage recovery key": "Enter secret storage recovery key", + "This looks like a valid recovery key!": "This looks like a valid recovery key!", + "Not a valid recovery key": "Not a valid recovery key", + "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.", + "If you've forgotten your recovery key you can .": "If you've forgotten your recovery key you can .", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.", @@ -1532,10 +1541,9 @@ "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "Enter Recovery Key": "Enter Recovery Key", - "This looks like a valid recovery key!": "This looks like a valid recovery key!", - "Not a valid recovery key": "Not a valid recovery key", + "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.", - "If you've forgotten your recovery passphrase you can ": "If you've forgotten your recovery passphrase you can ", + "If you've forgotten your recovery key you can ": "If you've forgotten your recovery key you can ", "Private Chat": "Private Chat", "Public Chat": "Public Chat", "Custom": "Custom", @@ -1885,39 +1893,50 @@ "File to import": "File to import", "Import": "Import", "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", - "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.", + "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.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", "Enter a passphrase...": "Enter a passphrase...", - "Set up with a Recovery Key": "Set up with a Recovery Key", + "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", "Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.", "Repeat your passphrase...": "Repeat your passphrase...", - "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", - "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.", + "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.": "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.", + "As a safety net, you can use it to restore your access to encrypted messages.": "As a safety net, you can use it to restore your access to encrypted messages.", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.", - "Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Keep your recovery key somewhere very secure, like a password manager (or a safe)", + "Keep your recovery key somewhere very secure, like a password manager (or a safe).": "Keep your recovery key somewhere very secure, like a password manager (or a safe).", "Your Recovery Key": "Your Recovery Key", "Copy to clipboard": "Copy to clipboard", "Download": "Download", - "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", - "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", + "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", + "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.", + "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.", + "Set up secret storage": "Set up secret storage", + "Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase", + "Confirm your passphrase": "Confirm your passphrase", + "Recovery key": "Recovery key", + "Keep it safe": "Keep it safe", + "Storing secrets...": "Storing secrets...", + "Success!": "Success!", + "Unable to set up secret storage": "Unable to set up secret storage", + "Retry": "Retry", + "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.", + "Set up with a Recovery Key": "Set up with a Recovery Key", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", + "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a passphrase": "Secure your backup with a passphrase", - "Confirm your passphrase": "Confirm your passphrase", - "Recovery key": "Recovery key", - "Keep it safe": "Keep it safe", "Starting backup...": "Starting backup...", - "Success!": "Success!", "Create Key Backup": "Create Key Backup", "Unable to create key backup": "Unable to create key backup", - "Retry": "Retry", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", "Set up": "Set up", From 7446bcdedb1ab40cb433496b6e0c2aad3e51a914 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Dec 2019 15:20:30 +0000 Subject: [PATCH 19/26] Extract callbacks to a new module --- src/CrossSigningManager.js | 62 ++++++++++++++++++++++++++++++++++++++ src/MatrixClientPeg.js | 49 +++--------------------------- 2 files changed, 67 insertions(+), 44 deletions(-) create mode 100644 src/CrossSigningManager.js diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js new file mode 100644 index 0000000000..56feadd5d7 --- /dev/null +++ b/src/CrossSigningManager.js @@ -0,0 +1,62 @@ +/* +Copyright 2019 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. +*/ + +import Modal from './Modal'; +import sdk from './index'; +import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; + +// This stores the cross-signing private keys in memory for the JS SDK. They are +// also persisted to Secure Secret Storage in account data by the JS SDK when +// created. +const crossSigningKeys = {}; + +// XXX: On desktop platforms, we plan to store only the SSSS default key in a +// secure enclave, while the cross-signing private keys will still be retrieved +// from SSSS, so it's unclear that we actually need these cross-signing +// application callbacks for Riot. Should the JS SDK default to in-memory +// storage of these itself? +export const getCrossSigningKey = k => crossSigningKeys[k]; +export const saveCrossSigningKeys = newKeys => Object.assign(crossSigningKeys, newKeys); + +// XXX: This flow should maybe be reworked to allow retries in case of typos, +// etc. +export const getSecretStorageKey = async keyInfos => { + const keyInfoEntries = Object.entries(keyInfos); + if (keyInfoEntries.length > 1) { + throw new Error("Multiple storage key requests not implemented"); + } + const [name, info] = keyInfoEntries[0]; + const AccessSecretStorageDialog = + sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, { + keyInfo: info, + }, + ); + const [input] = await finished; + if (!input) { + throw new Error("Secret storage access canceled"); + } + let key; + const { passphrase } = info; + if (passphrase) { + key = await deriveKey(input, passphrase.salt, passphrase.iterations); + } else { + key = decodeRecoveryKey(input); + } + return [name, key]; +}; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index d73931f57b..a3a0588bfc 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -1,7 +1,8 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. -Copyright 2017 New Vector Ltd +Copyright 2017, 2018, 2019 New Vector Ltd +Copyright 2019 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. @@ -30,8 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; -import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; +import * as CrossSigningManager from './CrossSigningManager'; interface MatrixClientCreds { homeserverUrl: string, @@ -222,48 +222,9 @@ class MatrixClientPeg { identityServer: new IdentityAuthClient(), }; + opts.cryptoCallbacks = {}; if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - // This stores the cross-signing private keys in memory for the JS SDK. They - // are also persisted to Secure Secret Storage in account data by - // the JS SDK when created. - const keys = {}; - opts.cryptoCallbacks = { - // XXX: This flow should maybe be reworked to allow retries in - // case of typos, etc. - getSecretStorageKey: async keyInfos => { - const keyInfoEntries = Object.entries(keyInfos); - if (keyInfoEntries.length > 1) { - throw new Error("Multiple storage key requests not implemented"); - } - const [name, info] = keyInfoEntries[0]; - const AccessSecretStorageDialog = - sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); - const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", - AccessSecretStorageDialog, { - keyInfo: info, - }, - ); - const [input] = await finished; - if (!input) { - throw new Error("Secret storage access canceled"); - } - let key; - const { passphrase } = info; - if (passphrase) { - key = await deriveKey(input, passphrase.salt, passphrase.iterations); - } else { - key = decodeRecoveryKey(input); - } - return [name, key]; - }, - // XXX: On desktop platforms, we plan to store only the SSSS default - // key in a secure enclave, while the cross-signing private keys - // will still be retrieved from SSSS, so it's unclear that we - // actually need these cross-signing application callbacks for Riot. - // Should the JS SDK default to in-memory storage of these itself? - getCrossSigningKey: k => keys[k], - saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), - }; + Object.assign(opts.cryptoCallbacks, CrossSigningManager); } this.matrixClient = createMatrixClient(opts); From 7601ce93d90b37917d7bc824495db427e192022a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Dec 2019 15:33:10 +0000 Subject: [PATCH 20/26] Add in-memory cache of secret storage keys --- src/CrossSigningManager.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 56feadd5d7..c8738ece88 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -32,6 +32,13 @@ const crossSigningKeys = {}; export const getCrossSigningKey = k => crossSigningKeys[k]; export const saveCrossSigningKeys = newKeys => Object.assign(crossSigningKeys, newKeys); +// This stores the secret storage private keys in memory for the JS SDK. This is +// only meant to act as a cache to avoid prompting the user multiple times +// during the same session. It is considered unsafe to persist this to normal +// web storage. For platforms with a secure enclave, we will store this key +// there. +const secretStorageKeys = {}; + // XXX: This flow should maybe be reworked to allow retries in case of typos, // etc. export const getSecretStorageKey = async keyInfos => { @@ -40,6 +47,10 @@ export const getSecretStorageKey = async keyInfos => { throw new Error("Multiple storage key requests not implemented"); } const [name, info] = keyInfoEntries[0]; + // Check the in-memory cache + if (secretStorageKeys[name]) { + return [name, secretStorageKeys[name]]; + } const AccessSecretStorageDialog = sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", @@ -58,5 +69,7 @@ export const getSecretStorageKey = async keyInfos => { } else { key = decodeRecoveryKey(input); } + // Save to cache to avoid future prompts in the current session + secretStorageKeys[name] = key; return [name, key]; }; From 2bdc16b4bd904a22d7ca888ad8183b99bf8f4bbf Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Dec 2019 16:11:12 +0000 Subject: [PATCH 21/26] Key requests have an object wrapper --- src/CrossSigningManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index c8738ece88..b580fd7f8d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -41,7 +41,7 @@ const secretStorageKeys = {}; // XXX: This flow should maybe be reworked to allow retries in case of typos, // etc. -export const getSecretStorageKey = async keyInfos => { +export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); From d66dbdea61437bacfae2eb20e5035d4e07c32797 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Dec 2019 16:23:00 +0000 Subject: [PATCH 22/26] Indicate which access flow was used --- src/CrossSigningManager.js | 11 +++++++---- .../secretstorage/AccessSecretStorageDialog.js | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b580fd7f8d..1a0c7fefa4 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -63,11 +63,14 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { throw new Error("Secret storage access canceled"); } let key; - const { passphrase } = info; - if (passphrase) { - key = await deriveKey(input, passphrase.salt, passphrase.iterations); + if (input.passphrase) { + key = await deriveKey( + input.passphrase, + info.passphrase.salt, + info.passphrase.iterations, + ); } else { - key = decodeRecoveryKey(input); + key = decodeRecoveryKey(input.recoveryKey); } // Save to cache to avoid future prompts in the current session secretStorageKeys[name] = key; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 8db56e6dfb..f74e96bc2e 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -65,11 +65,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } _onPassPhraseNext = async () => { - this.props.onFinished(this.state.passPhrase); + this.props.onFinished({ passphrase: this.state.passPhrase }); } _onRecoveryKeyNext = async () => { - this.props.onFinished(this.state.recoveryKey); + this.props.onFinished({ recoveryKey: this.state.recoveryKey }); } _onPassPhraseChange = (e) => { From 9b9e074d3020e981d6a726439c1b543bbb1a59b3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Dec 2019 14:15:41 +0000 Subject: [PATCH 23/26] Use consistent import style --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index f74e96bc2e..05adeb48de 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -21,7 +21,7 @@ import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { _t } from '../../../../languageHandler'; -import {Key} from "../../../../Keyboard"; +import { Key } from "../../../../Keyboard"; /* * Access Secure Secret Storage by requesting the user's passphrase. From 24d6e7e4564772fbc235234bd831595ed008c4e2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Dec 2019 17:54:00 +0000 Subject: [PATCH 24/26] Use private key check to provide feedback --- src/CrossSigningManager.js | 40 ++++++++++------ .../AccessSecretStorageDialog.js | 47 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 1a0c7fefa4..dd77eb1f87 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -16,6 +16,7 @@ limitations under the License. import Modal from './Modal'; import sdk from './index'; +import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; @@ -39,40 +40,49 @@ export const saveCrossSigningKeys = newKeys => Object.assign(crossSigningKeys, n // there. const secretStorageKeys = {}; -// XXX: This flow should maybe be reworked to allow retries in case of typos, -// etc. export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } const [name, info] = keyInfoEntries[0]; + // Check the in-memory cache if (secretStorageKeys[name]) { return [name, secretStorageKeys[name]]; } + + const inputToKey = async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + info.passphrase.salt, + info.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; const AccessSecretStorageDialog = sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", - AccessSecretStorageDialog, { - keyInfo: info, - }, + AccessSecretStorageDialog, + { + keyInfo: info, + checkPrivateKey: async (input) => { + const key = await inputToKey(input); + return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); + }, + }, ); const [input] = await finished; if (!input) { throw new Error("Secret storage access canceled"); } - let key; - if (input.passphrase) { - key = await deriveKey( - input.passphrase, - info.passphrase.salt, - info.passphrase.iterations, - ); - } else { - key = decodeRecoveryKey(input.recoveryKey); - } + const key = await inputToKey(input); + // Save to cache to avoid future prompts in the current session secretStorageKeys[name] = key; + return [name, key]; }; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 05adeb48de..d116ce505f 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -30,6 +30,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent { static propTypes = { // { passphrase, pubkey } keyInfo: PropTypes.object.isRequired, + // Function from one of { passphrase, recoveryKey } -> boolean + checkPrivateKey: PropTypes.func.isRequired, } constructor(props) { @@ -39,6 +41,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { recoveryKeyValid: false, forceRecoveryKey: false, passPhrase: '', + keyMatches: null, }; } @@ -61,25 +64,41 @@ export default class AccessSecretStorageDialog extends React.PureComponent { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), + keyMatches: null, }); } _onPassPhraseNext = async () => { - this.props.onFinished({ passphrase: this.state.passPhrase }); + this.setState({ keyMatches: null }); + const input = { passphrase: this.state.passPhrase }; + const keyMatches = await this.props.checkPrivateKey(input); + if (keyMatches) { + this.props.onFinished(input); + } else { + this.setState({ keyMatches }); + } } _onRecoveryKeyNext = async () => { - this.props.onFinished({ recoveryKey: this.state.recoveryKey }); + this.setState({ keyMatches: null }); + const input = { recoveryKey: this.state.recoveryKey }; + const keyMatches = await this.props.checkPrivateKey(input); + if (keyMatches) { + this.props.onFinished(input); + } else { + this.setState({ keyMatches }); + } } _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, + keyMatches: null, }); } _onPassPhraseKeyPress = (e) => { - if (e.key === Key.ENTER) { + if (e.key === Key.ENTER && this.state.passPhrase.length > 0) { this._onPassPhraseNext(); } } @@ -106,6 +125,19 @@ export default class AccessSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); title = _t("Enter secret storage passphrase"); + + let keyStatus; + if (this.state.keyMatches === false) { + keyStatus =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct passphrase.", + )} +
; + } else { + keyStatus =
; + } + content =

{_t( "Warning: You should only access secret storage " + @@ -125,11 +157,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent { value={this.state.passPhrase} autoFocus={true} /> + {keyStatus}

{_t( @@ -163,6 +197,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent { keyStatus =
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
; + } else if (this.state.keyMatches === false) { + keyStatus =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct recovery key.", + )} +
; } else { keyStatus =
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ab26d677a3..f96756d59f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1518,11 +1518,13 @@ "Allow": "Allow", "Deny": "Deny", "Enter secret storage passphrase": "Enter secret storage passphrase", + "Unable to access secret storage. Please verify that you entered the correct passphrase.": "Unable to access secret storage. Please verify that you entered the correct passphrase.", "Warning: You should only access secret storage from a trusted computer.": "Warning: You should only access secret storage from a trusted computer.", "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.", "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.": "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.", "Enter secret storage recovery key": "Enter secret storage recovery key", "This looks like a valid recovery key!": "This looks like a valid recovery key!", + "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.", "Not a valid recovery key": "Not a valid recovery key", "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.", "If you've forgotten your recovery key you can .": "If you've forgotten your recovery key you can .", From 80c120b93b562f16bf1670aaf71ea1b79d260c0f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 10 Dec 2019 16:47:18 +0000 Subject: [PATCH 25/26] Cross-signing storage now handled in JS SDK --- src/CrossSigningManager.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index dd77eb1f87..b158f0dfaf 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,19 +20,6 @@ import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; -// This stores the cross-signing private keys in memory for the JS SDK. They are -// also persisted to Secure Secret Storage in account data by the JS SDK when -// created. -const crossSigningKeys = {}; - -// XXX: On desktop platforms, we plan to store only the SSSS default key in a -// secure enclave, while the cross-signing private keys will still be retrieved -// from SSSS, so it's unclear that we actually need these cross-signing -// application callbacks for Riot. Should the JS SDK default to in-memory -// storage of these itself? -export const getCrossSigningKey = k => crossSigningKeys[k]; -export const saveCrossSigningKeys = newKeys => Object.assign(crossSigningKeys, newKeys); - // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times // during the same session. It is considered unsafe to persist this to normal From 4956e8322815e9927ff5af568b4a7a4dcf9ae2c7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 10 Dec 2019 16:53:15 +0000 Subject: [PATCH 26/26] Remove secret storage key cache for now --- src/CrossSigningManager.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b158f0dfaf..5dc709bd10 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,25 +20,12 @@ import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; -// This stores the secret storage private keys in memory for the JS SDK. This is -// only meant to act as a cache to avoid prompting the user multiple times -// during the same session. It is considered unsafe to persist this to normal -// web storage. For platforms with a secure enclave, we will store this key -// there. -const secretStorageKeys = {}; - export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } const [name, info] = keyInfoEntries[0]; - - // Check the in-memory cache - if (secretStorageKeys[name]) { - return [name, secretStorageKeys[name]]; - } - const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( @@ -67,9 +54,5 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { throw new Error("Secret storage access canceled"); } const key = await inputToKey(input); - - // Save to cache to avoid future prompts in the current session - secretStorageKeys[name] = key; - return [name, key]; };