diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 1a0c7fefa4..dd77eb1f87 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -16,6 +16,7 @@ limitations under the License. import Modal from './Modal'; import sdk from './index'; +import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; @@ -39,40 +40,49 @@ export const saveCrossSigningKeys = newKeys => Object.assign(crossSigningKeys, n // there. const secretStorageKeys = {}; -// XXX: This flow should maybe be reworked to allow retries in case of typos, -// etc. export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } const [name, info] = keyInfoEntries[0]; + // Check the in-memory cache if (secretStorageKeys[name]) { return [name, secretStorageKeys[name]]; } + + const inputToKey = async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + info.passphrase.salt, + info.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; const AccessSecretStorageDialog = sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", - AccessSecretStorageDialog, { - keyInfo: info, - }, + AccessSecretStorageDialog, + { + keyInfo: info, + checkPrivateKey: async (input) => { + const key = await inputToKey(input); + return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); + }, + }, ); const [input] = await finished; if (!input) { throw new Error("Secret storage access canceled"); } - let key; - if (input.passphrase) { - key = await deriveKey( - input.passphrase, - info.passphrase.salt, - info.passphrase.iterations, - ); - } else { - key = decodeRecoveryKey(input.recoveryKey); - } + const key = await inputToKey(input); + // Save to cache to avoid future prompts in the current session secretStorageKeys[name] = key; + return [name, key]; }; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 05adeb48de..d116ce505f 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -30,6 +30,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent { static propTypes = { // { passphrase, pubkey } keyInfo: PropTypes.object.isRequired, + // Function from one of { passphrase, recoveryKey } -> boolean + checkPrivateKey: PropTypes.func.isRequired, } constructor(props) { @@ -39,6 +41,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { recoveryKeyValid: false, forceRecoveryKey: false, passPhrase: '', + keyMatches: null, }; } @@ -61,25 +64,41 @@ export default class AccessSecretStorageDialog extends React.PureComponent { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), + keyMatches: null, }); } _onPassPhraseNext = async () => { - this.props.onFinished({ passphrase: this.state.passPhrase }); + this.setState({ keyMatches: null }); + const input = { passphrase: this.state.passPhrase }; + const keyMatches = await this.props.checkPrivateKey(input); + if (keyMatches) { + this.props.onFinished(input); + } else { + this.setState({ keyMatches }); + } } _onRecoveryKeyNext = async () => { - this.props.onFinished({ recoveryKey: this.state.recoveryKey }); + this.setState({ keyMatches: null }); + const input = { recoveryKey: this.state.recoveryKey }; + const keyMatches = await this.props.checkPrivateKey(input); + if (keyMatches) { + this.props.onFinished(input); + } else { + this.setState({ keyMatches }); + } } _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, + keyMatches: null, }); } _onPassPhraseKeyPress = (e) => { - if (e.key === Key.ENTER) { + if (e.key === Key.ENTER && this.state.passPhrase.length > 0) { this._onPassPhraseNext(); } } @@ -106,6 +125,19 @@ export default class AccessSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); title = _t("Enter secret storage passphrase"); + + let keyStatus; + if (this.state.keyMatches === false) { + keyStatus =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct passphrase.", + )} +
; + } else { + keyStatus =
; + } + content =

{_t( "Warning: You should only access secret storage " + @@ -125,11 +157,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent { value={this.state.passPhrase} autoFocus={true} /> + {keyStatus}

{_t( @@ -163,6 +197,13 @@ export default class AccessSecretStorageDialog extends React.PureComponent { keyStatus =
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
; + } else if (this.state.keyMatches === false) { + keyStatus =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct recovery key.", + )} +
; } else { keyStatus =
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ab26d677a3..f96756d59f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1518,11 +1518,13 @@ "Allow": "Allow", "Deny": "Deny", "Enter secret storage passphrase": "Enter secret storage passphrase", + "Unable to access secret storage. Please verify that you entered the correct passphrase.": "Unable to access secret storage. Please verify that you entered the correct passphrase.", "Warning: You should only access secret storage from a trusted computer.": "Warning: You should only access secret storage from a trusted computer.", "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.", "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.": "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.", "Enter secret storage recovery key": "Enter secret storage recovery key", "This looks like a valid recovery key!": "This looks like a valid recovery key!", + "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.", "Not a valid recovery key": "Not a valid recovery key", "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.", "If you've forgotten your recovery key you can .": "If you've forgotten your recovery key you can .",