Merge pull request #3897 from matrix-org/dbkr/bootstrap_from_key_backup_ui
Implement some parts of new cross signing bootstrap UIpull/21833/head
						commit
						442b8be459
					
				|  | @ -338,6 +338,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { | |||
|     margin-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| .mx_Dialog_titleImage { | ||||
|     vertical-align: middle; | ||||
|     width: 25px; | ||||
|     height: 25px; | ||||
|     margin-left: -2px; | ||||
|     margin-right: 4px; | ||||
| } | ||||
| 
 | ||||
| .mx_Dialog_title { | ||||
|     font-size: 22px; | ||||
|     line-height: 36px; | ||||
|  |  | |||
|  | @ -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 && this.state.accountPassword) { | ||||
|             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 <div> | ||||
|  | @ -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 <div> | ||||
|         const Field = sdk.getComponent('views.elements.Field'); | ||||
| 
 | ||||
|         let authPrompt; | ||||
|         if (this.state.canUploadKeysWithPasswordOnly) { | ||||
|             authPrompt = <div> | ||||
|                 <div>{_t("Enter your account password to confirm the upgrade:")}</div> | ||||
|                 <div><Field type="password" | ||||
|                     id="mx_CreateSecretStorage_accountPassword" | ||||
|                     label={_t("Password")} | ||||
|                     value={this.state.accountPassword} | ||||
|                     onChange={this._onAccountPasswordChange} | ||||
|                     flagInvalid={this.state.accountPasswordCorrect === false} | ||||
|                     autoFocus={true} | ||||
|                 /></div> | ||||
|             </div>; | ||||
|         } else { | ||||
|             authPrompt = <p> | ||||
|                 {_t("You'll need to authenticate with the server to confirm the upgrade.")} | ||||
|             </p>; | ||||
|         } | ||||
| 
 | ||||
|         return <form onSubmit={this._onMigrateFormSubmit}> | ||||
|             <p>{_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.", | ||||
|             )}</p> | ||||
|             <div>{authPrompt}</div> | ||||
|             <DialogButtons primaryButton={_t('Next')} | ||||
|                 onPrimaryButtonClick={this._onMigrateNextClick} | ||||
|                 primaryIsSubmit={true} | ||||
|                 hasCancel={true} | ||||
|                 onCancel={this._onCancel} | ||||
|                 primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} | ||||
|             /> | ||||
|         </div>; | ||||
|         </form>; | ||||
|     } | ||||
| 
 | ||||
|     _renderPhasePassPhrase() { | ||||
|  | @ -533,7 +613,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { | |||
|         const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); | ||||
|         return <div> | ||||
|             <p>{_t( | ||||
|                 "Your access to encrypted messages is now protected.", | ||||
|                 "This device can now verify other devices, granting them access " + | ||||
|                 "to encrypted messages and marking them as trusted for other users.", | ||||
|             )}</p> | ||||
|             <p>{_t( | ||||
|                 "Verify other users in their profile.", | ||||
|             )}</p> | ||||
|             <DialogButtons primaryButton={_t('OK')} | ||||
|                 onPrimaryButtonClick={this._onDone} | ||||
|  | @ -564,7 +648,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: | ||||
|  | @ -578,9 +662,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { | |||
|             case PHASE_STORING: | ||||
|                 return _t('Storing secrets...'); | ||||
|             case PHASE_DONE: | ||||
|                 return _t('Success!'); | ||||
|                 return _t('Encryption upgraded'); | ||||
|             default: | ||||
|                 return null; | ||||
|                 return ''; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -635,11 +719,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let headerImage; | ||||
|         if (this._titleForPhase(this.state.phase)) { | ||||
|             headerImage = require("../../../../../res/img/e2e/normal.svg"); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <BaseDialog className='mx_CreateSecretStorageDialog' | ||||
|                 onFinished={this.props.onFinished} | ||||
|                 title={this._titleForPhase(this.state.phase)} | ||||
|                 hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} | ||||
|                 headerImage={headerImage} | ||||
|                 hasCancel={[PHASE_PASSPHRASE].includes(this.state.phase)} | ||||
|             > | ||||
|             <div> | ||||
|                 {content} | ||||
|  |  | |||
|  | @ -65,6 +65,9 @@ export default createReactClass({ | |||
|         // Title for the dialog.
 | ||||
|         title: PropTypes.node.isRequired, | ||||
| 
 | ||||
|         // Path to an icon to put in the header
 | ||||
|         headerImage: PropTypes.string, | ||||
| 
 | ||||
|         // children should be the content of the dialog
 | ||||
|         children: PropTypes.node, | ||||
| 
 | ||||
|  | @ -110,6 +113,13 @@ export default createReactClass({ | |||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         let headerImage; | ||||
|         if (this.props.headerImage) { | ||||
|             headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} | ||||
|                 alt="" | ||||
|             />; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <MatrixClientContext.Provider value={this._matrixClient}> | ||||
|                 <FocusLock | ||||
|  | @ -135,6 +145,7 @@ export default createReactClass({ | |||
|                         'mx_Dialog_headerWithButton': !!this.props.headerButton, | ||||
|                     })}> | ||||
|                         <div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'> | ||||
|                             {headerImage} | ||||
|                             { this.props.title } | ||||
|                         </div> | ||||
|                         { this.props.headerButton } | ||||
|  |  | |||
|  | @ -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 = <button onClick={this._onCancelClick} disabled={this.props.disabled}> | ||||
|             cancelButton = <button | ||||
|                 // important: the default type is 'submit' and this button comes before the
 | ||||
|                 // primary in the DOM so will get form submissions unless we make it not a submit.
 | ||||
|                 type="button" | ||||
|                 onClick={this._onCancelClick} | ||||
|                 disabled={this.props.disabled} | ||||
|             > | ||||
|                 { this.props.cancelButton || _t("Cancel") } | ||||
|             </button>; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div className="mx_Dialog_buttons"> | ||||
|                 { cancelButton } | ||||
|                 { this.props.children } | ||||
|                 <button className={primaryButtonClassName} | ||||
|                 <button type={this.props.primaryIsSubmit ? 'submit' : 'button'} | ||||
|                     className={primaryButtonClassName} | ||||
|                     onClick={this.props.onPrimaryButtonClick} | ||||
|                     autoFocus={this.props.focus} | ||||
|                     disabled={this.props.disabled || this.props.primaryDisabled} | ||||
|  |  | |||
|  | @ -1976,7 +1976,9 @@ | |||
|     "Import": "Import", | ||||
|     "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.", | ||||
|     "Restore": "Restore", | ||||
|     "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.", | ||||
|     "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", | ||||
|     "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", | ||||
|     "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.", | ||||
|     "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", | ||||
|     "<b>Warning</b>: You should only set up secret storage from a trusted computer.": "<b>Warning</b>: 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.", | ||||
|  | @ -2000,17 +2002,18 @@ | |||
|     "<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe", | ||||
|     "<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive", | ||||
|     "<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage", | ||||
|     "Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.", | ||||
|     "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.", | ||||
|     "Verify other users in their profile.": "Verify other users in their profile.", | ||||
|     "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", | ||||
|     "Restore your Key Backup": "Restore your Key Backup", | ||||
|     "Migrate from Key Backup": "Migrate from Key Backup", | ||||
|     "Upgrade your encryption": "Upgrade your encryption", | ||||
|     "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!", | ||||
|     "Encryption upgraded": "Encryption upgraded", | ||||
|     "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.", | ||||
|  | @ -2022,6 +2025,7 @@ | |||
|     "Set up Secure Message Recovery": "Set up Secure Message Recovery", | ||||
|     "Secure your backup with a passphrase": "Secure your backup with a passphrase", | ||||
|     "Starting backup...": "Starting backup...", | ||||
|     "Success!": "Success!", | ||||
|     "Create Key Backup": "Create Key Backup", | ||||
|     "Unable to create key backup": "Unable to create key backup", | ||||
|     "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.", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker