Prompt for recovery key on login rather than passphrase
Only show passphrase options at all if the user has a passphrase on their SSSS key.
This commit is contained in:
parent
631184c661
commit
e06ba2003b
9 changed files with 224 additions and 20 deletions
|
@ -70,6 +70,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_recoveryKeyInput {
|
||||
width: 368px;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_heroIcon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
|
|
|
@ -30,6 +30,8 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
|||
// operation ends.
|
||||
let secretStorageKeys = {};
|
||||
let secretStorageBeingAccessed = false;
|
||||
// Stores the 'passphraseOnly' option for the active storage access operation
|
||||
let passphraseOnlyOption = null;
|
||||
|
||||
function isCachingAllowed() {
|
||||
return (
|
||||
|
@ -99,6 +101,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
|||
const key = await inputToKey(input);
|
||||
return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
|
||||
},
|
||||
passphraseOnly: passphraseOnlyOption,
|
||||
},
|
||||
/* className= */ null,
|
||||
/* isPriorityModal= */ false,
|
||||
|
@ -213,19 +216,27 @@ export async function promptForBackupPassphrase() {
|
|||
*
|
||||
* @param {Function} [func] An operation to perform once secret storage has been
|
||||
* bootstrapped. Optional.
|
||||
* @param {bool} [forceReset] Reset secret storage even if it's already set up
|
||||
* @param {object} [opts] Named options
|
||||
* @param {bool} [opts.forceReset] Reset secret storage even if it's already set up
|
||||
* @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client
|
||||
* already has available. If a key is not supplied here, the user will be prompted.
|
||||
* @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys
|
||||
*/
|
||||
export async function accessSecretStorage(func = async () => { }, forceReset = false) {
|
||||
export async function accessSecretStorage(
|
||||
func = async () => { }, opts = {},
|
||||
) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
secretStorageBeingAccessed = true;
|
||||
passphraseOnlyOption = opts.passphraseOnly;
|
||||
secretStorageKeys = Object.assign({}, opts.withKeys || {});
|
||||
try {
|
||||
if (!await cli.hasSecretStorageKey() || forceReset) {
|
||||
if (!await cli.hasSecretStorageKey() || opts.forceReset) {
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
|
||||
{
|
||||
force: forceReset,
|
||||
force: opts.forceReset,
|
||||
},
|
||||
null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
|
@ -263,5 +274,6 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|||
if (!isCachingAllowed()) {
|
||||
secretStorageKeys = {};
|
||||
}
|
||||
passphraseOnlyOption = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import * as sdk from '../../../index';
|
|||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_INTRO,
|
||||
PHASE_RECOVERY_KEY,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
|
@ -61,6 +62,9 @@ export default class CompleteSecurity extends React.Component {
|
|||
if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === PHASE_RECOVERY_KEY) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Recovery Key");
|
||||
} else if (phase === PHASE_DONE) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
|
|
|
@ -19,15 +19,26 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import withValidation from '../../views/elements/Validation';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_INTRO,
|
||||
PHASE_RECOVERY_KEY,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
PHASE_FINISHED,
|
||||
} from '../../../stores/SetupEncryptionStore';
|
||||
|
||||
function keyHasPassphrase(keyInfo) {
|
||||
return (
|
||||
keyInfo.passphrase &&
|
||||
keyInfo.passphrase.salt &&
|
||||
keyInfo.passphrase.iterations
|
||||
);
|
||||
}
|
||||
|
||||
export default class SetupEncryptionBody extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
@ -45,6 +56,11 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
recoveryKey: '',
|
||||
// whether the recovery key is a valid recovery key
|
||||
recoveryKeyValid: null,
|
||||
// whether the recovery key is the correct key or not
|
||||
recoveryKeyCorrect: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -67,9 +83,14 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
store.stop();
|
||||
}
|
||||
|
||||
_onUsePassphraseClick = async () => {
|
||||
_onUseRecoveryKeyClick = async () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.usePassPhrase();
|
||||
store.useRecoveryKey();
|
||||
}
|
||||
|
||||
_onRecoveryKeyCancelClick() {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.cancelUseRecoveryKey();
|
||||
}
|
||||
|
||||
onSkipClick = () => {
|
||||
|
@ -92,6 +113,66 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
store.done();
|
||||
}
|
||||
|
||||
_onUsePassphraseClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.usePassPhrase();
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
this.setState({recoveryKey: e.target.value});
|
||||
}
|
||||
|
||||
_onRecoveryKeyValidate = async (fieldState) => {
|
||||
const result = await this._validateRecoveryKey(fieldState);
|
||||
this.setState({recoveryKeyValid: result.valid});
|
||||
return result;
|
||||
}
|
||||
|
||||
_validateRecoveryKey = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async (state) => {
|
||||
try {
|
||||
const decodedKey = decodeRecoveryKey(state.value);
|
||||
const correct = await MatrixClientPeg.get().checkSecretStorageKey(
|
||||
decodedKey, SetupEncryptionStore.sharedInstance().keyInfo,
|
||||
);
|
||||
this.setState({
|
||||
recoveryKeyValid: true,
|
||||
recoveryKeyCorrect: correct,
|
||||
});
|
||||
return correct;
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
recoveryKeyValid: false,
|
||||
recoveryKeyCorrect: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
invalid: function() {
|
||||
if (this.state.recoveryKeyValid) {
|
||||
return _t("This isn't the recovery key for your account");
|
||||
} else {
|
||||
return _t("This isn't a valid recovery key");
|
||||
}
|
||||
},
|
||||
valid: function() {
|
||||
return _t("Looks good!");
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
_onRecoveryKeyFormSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!this.state.recoveryKeyCorrect) return;
|
||||
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey));
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
|
@ -108,6 +189,13 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
|
||||
} else {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key");
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
|
@ -131,8 +219,8 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
|
||||
{_t("Use Recovery Passphrase or Key")}
|
||||
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}>
|
||||
{recoveryKeyPrompt}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||
{_t("Skip")}
|
||||
|
@ -140,6 +228,47 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_RECOVERY_KEY) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let keyPrompt;
|
||||
if (keyHasPassphrase(store.keyInfo)) {
|
||||
keyPrompt = _t(
|
||||
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.", {},
|
||||
{
|
||||
a: sub => <AccessibleButton
|
||||
element="span"
|
||||
className="mx_linkButton"
|
||||
onClick={this._onUsePassphraseClick}
|
||||
>{sub}</AccessibleButton>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
keyPrompt = _t("Enter your Recovery Key to continue.");
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <form onSubmit={this._onRecoveryKeyFormSubmit}>
|
||||
<p>{keyPrompt}</p>
|
||||
<div className="mx_CompleteSecurity_recoveryKeyEntry">
|
||||
<Field
|
||||
type="text"
|
||||
label={_t('Recovery Key')}
|
||||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
onValidate={this._onRecoveryKeyValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="secondary" onClick={this._onRecoveryKeyCancelClick}>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary"
|
||||
disabled={!this.state.recoveryKeyCorrect}
|
||||
>
|
||||
{_t("Continue")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>;
|
||||
} else if (phase === PHASE_DONE) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
|
|
|
@ -94,7 +94,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
// If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys)
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
accessSecretStorage(() => {}, {forceReset: true});
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
|
|
|
@ -32,6 +32,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
keyInfo: PropTypes.object.isRequired,
|
||||
// Function from one of { passphrase, recoveryKey } -> boolean
|
||||
checkPrivateKey: PropTypes.func.isRequired,
|
||||
// If true, only prompt for a passphrase and do not offer to restore with
|
||||
// a recovery key or reset keys.
|
||||
passphraseOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -58,7 +61,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
_onResetRecoveryClick = () => {
|
||||
// Re-enter the access flow, but resetting storage this time around.
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
accessSecretStorage(() => {}, {forceReset: true});
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
|
@ -164,7 +167,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
primaryDisabled={this.state.passPhrase.length === 0}
|
||||
/>
|
||||
</form>
|
||||
{_t(
|
||||
{this.props.passphraseOnly ? null : _t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>."
|
||||
|
@ -234,7 +237,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
/>
|
||||
</form>
|
||||
{_t(
|
||||
{this.props.passphraseOnly ? null : _t(
|
||||
"If you've forgotten your recovery key you can "+
|
||||
"<button>set up new recovery options</button>."
|
||||
, {}, {
|
||||
|
|
|
@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
_bootstrapSecureSecretStorage = async (forceReset=false) => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => undefined, forceReset);
|
||||
await accessSecretStorage(() => undefined, {forceReset});
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
|
|
|
@ -2083,6 +2083,7 @@
|
|||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Verify this login": "Verify this login",
|
||||
"Recovery Key": "Recovery Key",
|
||||
"Session verified": "Session verified",
|
||||
"Failed to send email": "Failed to send email",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
|
@ -2136,10 +2137,16 @@
|
|||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||
"Registration Successful": "Registration Successful",
|
||||
"Create your account": "Create your account",
|
||||
"This isn't the recovery key for your account": "This isn't the recovery key for your account",
|
||||
"This isn't a valid recovery key": "This isn't a valid recovery key",
|
||||
"Looks good!": "Looks good!",
|
||||
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
|
||||
"Use Recovery Key": "Use Recovery Key",
|
||||
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
|
||||
"This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:",
|
||||
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
|
||||
"Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key",
|
||||
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.",
|
||||
"Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
|
||||
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
|
||||
"Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
|
||||
|
@ -2182,9 +2189,9 @@
|
|||
"Import": "Import",
|
||||
"Confirm encryption setup": "Confirm encryption setup",
|
||||
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
|
||||
"Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
|
||||
"Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption",
|
||||
"Restore": "Restore",
|
||||
"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 session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
|
||||
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.",
|
||||
|
|
|
@ -20,10 +20,11 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage
|
|||
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
export const PHASE_INTRO = 0;
|
||||
export const PHASE_BUSY = 1;
|
||||
export const PHASE_DONE = 2; //final done stage, but still showing UX
|
||||
export const PHASE_CONFIRM_SKIP = 3;
|
||||
export const PHASE_FINISHED = 4; //UX can be closed
|
||||
export const PHASE_RECOVERY_KEY = 1;
|
||||
export const PHASE_BUSY = 2;
|
||||
export const PHASE_DONE = 3; //final done stage, but still showing UX
|
||||
export const PHASE_CONFIRM_SKIP = 4;
|
||||
export const PHASE_FINISHED = 5; //UX can be closed
|
||||
|
||||
export class SetupEncryptionStore extends EventEmitter {
|
||||
static sharedInstance() {
|
||||
|
@ -36,11 +37,19 @@ export class SetupEncryptionStore extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
this._started = true;
|
||||
this.phase = PHASE_INTRO;
|
||||
this.phase = PHASE_BUSY;
|
||||
this.verificationRequest = null;
|
||||
this.backupInfo = null;
|
||||
|
||||
// ID of the key that the secrets we want are encrypted with
|
||||
this.keyId = null;
|
||||
// Descriptor of the key that the secrets we want are encrypted with
|
||||
this.keyInfo = null;
|
||||
|
||||
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
|
||||
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||
|
||||
this.fetchKeyInfo();
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
@ -57,7 +66,40 @@ export class SetupEncryptionStore extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchKeyInfo() {
|
||||
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
|
||||
if (Object.keys(keys).length === 0) {
|
||||
this.keyId = null;
|
||||
this.keyInfo = null;
|
||||
} else {
|
||||
// If the secret is stored under more than one key, we just pick an arbitrary one
|
||||
this.keyId = Object.keys(keys)[0];
|
||||
this.keyInfo = keys[this.keyId];
|
||||
}
|
||||
|
||||
this.phase = PHASE_INTRO;
|
||||
this.emit("update");
|
||||
}
|
||||
|
||||
async useRecoveryKey() {
|
||||
this.phase = PHASE_RECOVERY_KEY;
|
||||
this.emit("update");
|
||||
}
|
||||
|
||||
cancelUseRecoveryKey() {
|
||||
this.phase = PHASE_INTRO;
|
||||
this.emit("update");
|
||||
}
|
||||
|
||||
async setupWithRecoveryKey(recoveryKey) {
|
||||
this.startTrustCheck({[this.keyId]: recoveryKey});
|
||||
}
|
||||
|
||||
async usePassPhrase() {
|
||||
this.startTrustCheck();
|
||||
}
|
||||
|
||||
async startTrustCheck(withKeys) {
|
||||
this.phase = PHASE_BUSY;
|
||||
this.emit("update");
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -84,6 +126,9 @@ export class SetupEncryptionStore extends EventEmitter {
|
|||
// to advance before this.
|
||||
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
}
|
||||
}, {
|
||||
withKeys,
|
||||
passphraseOnly: true,
|
||||
}).catch(reject);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
Loading…
Reference in a new issue