From d211372740588bdac1ad740111d28183180d5e8b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Jan 2020 10:44:02 +0000 Subject: [PATCH 1/3] UI to bootsrap SSSS from key backup --- .../CreateSecretStorageDialog.js | 130 ++++++++++++++---- .../views/elements/DialogButtons.js | 17 ++- src/i18n/strings/en_EN.json | 6 +- 3 files changed, 123 insertions(+), 30 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 01b9c9c7c8..7d1b82681b 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -1,6 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -70,9 +70,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent { setPassPhrase: false, backupInfo: null, backupSigStatus: null, + // does the server offer a UI auth flow with just m.login.password + // for /keys/device_signing/upload? + canUploadKeysWithPasswordOnly: null, + accountPassword: '', + accountPasswordCorrect: null, }; this._fetchBackupInfo(); + this._queryKeyUploadAuth(); } componentWillUnmount() { @@ -96,11 +102,32 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + async _queryKeyUploadAuth() { + try { + await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); + // We should never get here: the server should always require + // UI auth to upload device signing keys. If we do, we upload + // no keys which would be a no-op. + console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); + } catch (error) { + if (!error.data.flows) { + console.log("uploadDeviceSigningKeys advertised no flows!"); + } + const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { + return f.stages.length === 1 && f.stages[0] === 'm.login.password'; + }); + this.setState({ + canUploadKeysWithPasswordOnly, + }); + } + } + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } - _onMigrateNextClick = () => { + _onMigrateFormSubmit = (e) => { + e.preventDefault(); this._bootstrapSecretStorage(); } @@ -127,29 +154,46 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _doBootstrapUIAuth = async (makeRequest) => { + if (this.state.canUploadKeysWithPasswordOnly) { + await makeRequest({ + type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: MatrixClientPeg.get().getUserId(), + }, + // https://github.com/matrix-org/synapse/issues/5665 + user: MatrixClientPeg.get().getUserId(), + password: this.state.accountPassword, + }); + } else { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + 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"); + } + } + } + _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"); - } - }, + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._keyInfo, keyBackupInfo: this.state.backupInfo, }); @@ -157,7 +201,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { phase: PHASE_DONE, }); } catch (e) { - this.setState({ error: e }); + if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { + this.setState({ + accountPasswordCorrect: false, + phase: PHASE_MIGRATE, + }); + } else { + this.setState({ error: e }); + } console.error("Error bootstrapping secret storage", e); } } @@ -285,6 +336,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; } + _onAccountPasswordChange = (e) => { + this.setState({ + accountPassword: e.target.value, + }); + } + _renderPhaseRestoreKeyBackup() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
@@ -309,18 +366,41 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // it automatically. // https://github.com/vector-im/riot-web/issues/11696 const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
+ const Field = sdk.getComponent('views.elements.Field'); + + let authPrompt; + if (this.state.canUploadKeysWithPasswordOnly) { + authPrompt =
+
{_t("Enter your account password to confirm the upgrade:")}
+
+
; + } else { + authPrompt =

+ {_t("You'll need to authenticate with the server to confirm the upgrade.")} +

; + } + + return

{_t( - "Secret Storage will be set up using your existing key backup details. " + - "Your secret storage passphrase and recovery key will be the same as " + - "they were for your key backup.", + "Upgrade this device to allow it to verify other devices, " + + "granting them access to encrypted messages and marking them " + + "as trusted for other users.", )}

+
{authPrompt}
-
; + ; } _renderPhasePassPhrase() { @@ -564,7 +644,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_RESTORE_KEY_BACKUP: return _t('Restore your Key Backup'); case PHASE_MIGRATE: - return _t('Migrate from Key Backup'); + return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: return _t('Secure your encrypted messages with a passphrase'); case PHASE_PASSPHRASE_CONFIRM: diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js index 4e47e73052..7b37fdb4eb 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.js @@ -34,8 +34,11 @@ export default createReactClass({ // A node to insert into the cancel button instead of default "Cancel" cancelButton: PropTypes.node, + // If true, make the primary button a form submit button (input type="submit") + primaryIsSubmit: PropTypes.bool, + // onClick handler for the primary button. - onPrimaryButtonClick: PropTypes.func.isRequired, + onPrimaryButtonClick: PropTypes.func, // should there be a cancel button? default: true hasCancel: PropTypes.bool, @@ -70,15 +73,23 @@ export default createReactClass({ } let cancelButton; if (this.props.cancelButton || this.props.hasCancel) { - cancelButton = ; } + return (
{ cancelButton } { this.props.children } -