diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 2184eaf347..5c254bbd00 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -96,6 +96,9 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { { keyInfo: info, checkPrivateKey: async (input) => { + if (!info.pubkey) { + return true; + } const key = await inputToKey(input); return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); }, @@ -159,6 +162,20 @@ export const crossSigningCallbacks = { onSecretRequested, }; +export async function promptForBackupPassphrase() { + let key; + + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { + showSummary: false, keyCallback: k => key = k, + }, null, /* priority = */ false, /* static = */ true); + + const success = await finished; + if (!success) throw new Error("Key backup prompt cancelled"); + + return key; +} + /** * This helper should be used whenever you need to access secret storage. It * ensures that secret storage (and also cross-signing since they each depend on @@ -215,6 +232,7 @@ export async function accessSecretStorage(func = async () => { }, force = false) throw new Error("Cross-signing key upload auth canceled"); } }, + getBackupPassphrase: promptForBackupPassphrase, }); } diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 3992a587ae..7878a1a670 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -50,6 +50,7 @@ export default class DeviceListener { MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().on('accountData', this._onAccountData); this._recheck(); } @@ -58,6 +59,7 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('accountData', this._onAccountData); } this._dismissed.clear(); } @@ -87,6 +89,13 @@ export default class DeviceListener { this._recheck(); } + _onAccountData = (ev) => { + // User may have migrated SSSS to symmetric, in which case we can dismiss that toast + if (ev.getType().startsWith('m.secret_storage.key.')) { + this._recheck(); + } + } + // The server doesn't tell us when key backup is set up, so we poll // & cache the result async _getKeyBackupInfo() { @@ -150,6 +159,19 @@ export default class DeviceListener { } } return; + } else if (await cli.secretStorageKeyNeedsUpgrade()) { + if (this._dismissedThisDeviceToast) { + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + return; + } + + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Encryption upgrade available"), + icon: "verification_warning", + props: {kind: 'upgrade_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); } else { ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 49b103ecf7..35529fbc5b 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -23,6 +23,7 @@ import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; +import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; const PHASE_LOADING = 0; const PHASE_MIGRATE = 1; @@ -243,6 +244,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { createSecretStorageKey: async () => this._keyInfo, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + getKeyBackupPassphrase: promptForBackupPassphrase, }); } this.setState({ diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b2f4493d5b..52002f0591 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1524,6 +1524,7 @@ export default createReactClass({ icon: "verification", props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), + priority: ToastStore.PRIORITY_REALTIME, }); } }); diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index fed1310469..23a6692b39 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -21,7 +21,6 @@ import * as sdk from '../../../index'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import SettingsStore from "../../../settings/SettingsStore"; export default class LogoutDialog extends React.Component { defaultProps = { @@ -36,8 +35,8 @@ export default class LogoutDialog extends React.Component { this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this); this._onLogoutConfirm = this._onLogoutConfirm.bind(this); - const lowBandwidth = SettingsStore.getValue("lowBandwidth"); - const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled(); + const cli = MatrixClientPeg.get(); + const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled(); this.state = { shouldLoadBackupStatus: shouldLoadBackupStatus, diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 438806bf82..8e4a4e1e60 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -36,6 +36,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { // if false, will close the dialog as soon as the restore completes succesfully // default: true showSummary: PropTypes.bool, + // If specified, gather the key from the user but then call the function with the backup + // key rather than actually (necessarily) restoring the backup. + keyCallback: PropTypes.func, }; static defaultProps = { @@ -103,9 +106,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { restoreType: RESTORE_TYPE_PASSPHRASE, }); try { + // We do still restore the key backup: we must ensure that the key backup key + // is the right one and restoring it is currently the only way we can do this. const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword( this.state.passPhrase, undefined, undefined, this.state.backupInfo, ); + if (this.props.keyCallback) { + const key = await MatrixClientPeg.get().keyBackupKeyFromPassword( + this.state.passPhrase, this.state.backupInfo, + ); + this.props.keyCallback(key); + } + if (!this.props.showSummary) { this.props.onFinished(true); return; @@ -135,6 +147,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey( this.state.recoveryKey, undefined, undefined, this.state.backupInfo, ); + if (this.props.keyCallback) { + const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey); + this.props.keyCallback(key); + } if (!this.props.showSummary) { this.props.onFinished(true); return; diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index 5dd01c9f79..38ee31c8b7 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -72,8 +72,8 @@ export default class VerificationPanel extends React.PureComponent { renderQRPhase(pending) { const {member, request} = this.props; - const showSAS = request.methods.includes(verificationMethods.SAS); - const showQR = this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD); + const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS); + const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const noCommonMethodError = !showSAS && !showQR ? diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 5bf36b1ae8..cf47c797fc 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -33,6 +33,7 @@ export default class CrossSigningPanel extends React.PureComponent { crossSigningPublicKeysOnDevice: false, crossSigningPrivateKeysInStorage: false, secretStorageKeyInAccount: false, + secretStorageKeyNeedsUpgrade: null, }; } @@ -60,6 +61,10 @@ export default class CrossSigningPanel extends React.PureComponent { } }; + _onBootstrapClick = () => { + this._bootstrapSecureSecretStorage(false); + }; + onStatusChanged = () => { this._getUpdatedStatus(); }; @@ -74,6 +79,7 @@ export default class CrossSigningPanel extends React.PureComponent { const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); const crossSigningReady = await cli.isCrossSigningReady(); + const secretStorageKeyNeedsUpgrade = await cli.secretStorageKeyNeedsUpgrade(); this.setState({ crossSigningPublicKeysOnDevice, @@ -81,6 +87,7 @@ export default class CrossSigningPanel extends React.PureComponent { secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, + secretStorageKeyNeedsUpgrade, }); } @@ -126,6 +133,7 @@ export default class CrossSigningPanel extends React.PureComponent { secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, + secretStorageKeyNeedsUpgrade, } = this.state; let errorSection; @@ -180,7 +188,7 @@ export default class CrossSigningPanel extends React.PureComponent { ) { bootstrapButton = (
- + {_t("Bootstrap cross-signing and secret storage")}
@@ -209,6 +217,10 @@ export default class CrossSigningPanel extends React.PureComponent { {_t("Homeserver feature support:")} {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")} + + {_t("Secret Storage key format:")} + {secretStorageKeyNeedsUpgrade ? _t("outdated") : _t("up to date")} + {errorSection} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 22a6a2f222..57b39309b0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -586,6 +586,9 @@ "in account data": "in account data", "Homeserver feature support:": "Homeserver feature support:", "exists": "exists", + "Secret Storage key format:": "Secret Storage key format:", + "outdated": "outdated", + "up to date": "up to date", "Your homeserver does not support session management.": "Your homeserver does not support session management.", "Unable to load session list": "Unable to load session list", "Authentication": "Authentication", diff --git a/src/stores/ToastStore.js b/src/stores/ToastStore.js index 2c4464813b..f17d13bf9e 100644 --- a/src/stores/ToastStore.js +++ b/src/stores/ToastStore.js @@ -20,6 +20,9 @@ import EventEmitter from 'events'; * Holds the active toasts */ export default class ToastStore extends EventEmitter { + static PRIORITY_REALTIME = 1; + static PRIORITY_DEFAULT = 0; + static sharedInstance() { if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore(); return global.mx_ToastStore; @@ -36,9 +39,16 @@ export default class ToastStore extends EventEmitter { } addOrReplaceToast(newToast) { + if (newToast.priority === undefined) newToast.priority = ToastStore.PRIORITY_DEFAULT; + const oldIndex = this._toasts.findIndex(t => t.key === newToast.key); if (oldIndex === -1) { - this._toasts.push(newToast); + // we only have two priorities so just push realtime ones onto the front + if (newToast.priority) { + this._toasts.unshift(newToast); + } else { + this._toasts.push(newToast); + } } else { this._toasts[oldIndex] = newToast; }