UI to bootsrap SSSS from key backup

This commit is contained in:
David Baker 2020-01-22 10:44:02 +00:00
parent 8b2adf313f
commit d211372740
3 changed files with 123 additions and 30 deletions

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,16 +154,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}); });
} }
_bootstrapSecretStorage = async () => { _doBootstrapUIAuth = async (makeRequest) => {
this.setState({ if (this.state.canUploadKeysWithPasswordOnly) {
phase: PHASE_STORING, await makeRequest({
error: null, 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,
}); });
const cli = MatrixClientPeg.get(); } else {
try {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog( const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog, 'Cross-signing keys dialog', '', InteractiveAuthDialog,
{ {
@ -149,7 +180,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (!confirmed) { if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled"); throw new Error("Cross-signing key upload auth canceled");
} }
}, }
}
_bootstrapSecretStorage = async () => {
this.setState({
phase: PHASE_STORING,
error: null,
});
const cli = MatrixClientPeg.get();
try {
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
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) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({
accountPasswordCorrect: false,
phase: PHASE_MIGRATE,
});
} else {
this.setState({ error: e }); 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() {
@ -564,7 +644,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:

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

@ -1972,7 +1972,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,7 +2002,7 @@
"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",