UI to bootsrap SSSS from key backup
This commit is contained in:
parent
8b2adf313f
commit
d211372740
3 changed files with 123 additions and 30 deletions
|
@ -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:
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue