Merge pull request #3897 from matrix-org/dbkr/bootstrap_from_key_backup_ui

Implement some parts of new cross signing bootstrap UI
This commit is contained in:
David Baker 2020-01-23 11:04:49 +00:00 committed by GitHub
commit 442b8be459
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 36 deletions

View file

@ -338,6 +338,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
margin-bottom: 10px; margin-bottom: 10px;
} }
.mx_Dialog_titleImage {
vertical-align: middle;
width: 25px;
height: 25px;
margin-left: -2px;
margin-right: 4px;
}
.mx_Dialog_title { .mx_Dialog_title {
font-size: 22px; font-size: 22px;
line-height: 36px; line-height: 36px;

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2018, 2019 New Vector Ltd 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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, setPassPhrase: false,
backupInfo: null, backupInfo: null,
backupSigStatus: 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._fetchBackupInfo();
this._queryKeyUploadAuth();
} }
componentWillUnmount() { 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) => { _collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n; this._recoveryKeyNode = n;
} }
_onMigrateNextClick = () => { _onMigrateFormSubmit = (e) => {
e.preventDefault();
this._bootstrapSecretStorage(); 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 () => { _bootstrapSecretStorage = async () => {
this.setState({ this.setState({
phase: PHASE_STORING, phase: PHASE_STORING,
error: null, error: null,
}); });
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: async (makeRequest) => { authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
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, createSecretStorageKey: async () => this._keyInfo,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo,
}); });
@ -157,7 +201,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
phase: PHASE_DONE, phase: PHASE_DONE,
}); });
} catch (e) { } 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); 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; return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
} }
_onAccountPasswordChange = (e) => {
this.setState({
accountPassword: e.target.value,
});
}
_renderPhaseRestoreKeyBackup() { _renderPhaseRestoreKeyBackup() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
@ -309,18 +366,41 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// it automatically. // it automatically.
// https://github.com/vector-im/riot-web/issues/11696 // https://github.com/vector-im/riot-web/issues/11696
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); 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( <p>{_t(
"Secret Storage will be set up using your existing key backup details. " + "Upgrade this device to allow it to verify other devices, " +
"Your secret storage passphrase and recovery key will be the same as " + "granting them access to encrypted messages and marking them " +
"they were for your key backup.", "as trusted for other users.",
)}</p> )}</p>
<div>{authPrompt}</div>
<DialogButtons primaryButton={_t('Next')} <DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onMigrateNextClick} primaryIsSubmit={true}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this._onCancel}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
/> />
</div>; </form>;
} }
_renderPhasePassPhrase() { _renderPhasePassPhrase() {
@ -533,7 +613,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t( <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> )}</p>
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this._onDone}
@ -564,7 +648,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_RESTORE_KEY_BACKUP: case PHASE_RESTORE_KEY_BACKUP:
return _t('Restore your Key Backup'); return _t('Restore your Key Backup');
case PHASE_MIGRATE: case PHASE_MIGRATE:
return _t('Migrate from Key Backup'); return _t('Upgrade your encryption');
case PHASE_PASSPHRASE: case PHASE_PASSPHRASE:
return _t('Secure your encrypted messages with a passphrase'); return _t('Secure your encrypted messages with a passphrase');
case PHASE_PASSPHRASE_CONFIRM: case PHASE_PASSPHRASE_CONFIRM:
@ -578,9 +662,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_STORING: case PHASE_STORING:
return _t('Storing secrets...'); return _t('Storing secrets...');
case PHASE_DONE: case PHASE_DONE:
return _t('Success!'); return _t('Encryption upgraded');
default: 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 ( return (
<BaseDialog className='mx_CreateSecretStorageDialog' <BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)} 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> <div>
{content} {content}

View file

@ -65,6 +65,9 @@ export default createReactClass({
// Title for the dialog. // Title for the dialog.
title: PropTypes.node.isRequired, 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 should be the content of the dialog
children: PropTypes.node, 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 ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>
<FocusLock <FocusLock
@ -135,6 +145,7 @@ export default createReactClass({
'mx_Dialog_headerWithButton': !!this.props.headerButton, 'mx_Dialog_headerWithButton': !!this.props.headerButton,
})}> })}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'> <div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{headerImage}
{ this.props.title } { this.props.title }
</div> </div>
{ this.props.headerButton } { this.props.headerButton }

View file

@ -34,8 +34,11 @@ export default createReactClass({
// A node to insert into the cancel button instead of default "Cancel" // A node to insert into the cancel button instead of default "Cancel"
cancelButton: PropTypes.node, 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. // onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired, onPrimaryButtonClick: PropTypes.func,
// should there be a cancel button? default: true // should there be a cancel button? default: true
hasCancel: PropTypes.bool, hasCancel: PropTypes.bool,
@ -70,15 +73,23 @@ export default createReactClass({
} }
let cancelButton; let cancelButton;
if (this.props.cancelButton || this.props.hasCancel) { 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") } { this.props.cancelButton || _t("Cancel") }
</button>; </button>;
} }
return ( return (
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
{ cancelButton } { cancelButton }
{ this.props.children } { this.props.children }
<button className={primaryButtonClassName} <button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick} onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus} autoFocus={this.props.focus}
disabled={this.props.disabled || this.props.primaryDisabled} disabled={this.props.disabled || this.props.primaryDisabled}

View file

@ -1976,7 +1976,9 @@
"Import": "Import", "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.", "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", "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.", "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.", "<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.", "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>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>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", "<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.", "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", "Set up secret storage": "Set up secret storage",
"Restore your Key Backup": "Restore your Key Backup", "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", "Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase",
"Confirm your passphrase": "Confirm your passphrase", "Confirm your passphrase": "Confirm your passphrase",
"Recovery key": "Recovery key", "Recovery key": "Recovery key",
"Keep it safe": "Keep it safe", "Keep it safe": "Keep it safe",
"Storing secrets...": "Storing secrets...", "Storing secrets...": "Storing secrets...",
"Success!": "Success!", "Encryption upgraded": "Encryption upgraded",
"Unable to set up secret storage": "Unable to set up secret storage", "Unable to set up secret storage": "Unable to set up secret storage",
"Retry": "Retry", "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.", "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", "Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a passphrase": "Secure your backup with a passphrase", "Secure your backup with a passphrase": "Secure your backup with a passphrase",
"Starting backup...": "Starting backup...", "Starting backup...": "Starting backup...",
"Success!": "Success!",
"Create Key Backup": "Create Key Backup", "Create Key Backup": "Create Key Backup",
"Unable to create key backup": "Unable to 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.", "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.",