mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
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:
commit
442b8be459
5 changed files with 160 additions and 36 deletions
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
Loading…
Reference in a new issue