From c5099b9b96cabc20ab7f5c17b102cd5a8122ec24 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Dec 2019 14:28:02 +0000 Subject: [PATCH 1/9] Re-add the secret storage key cache --- src/CrossSigningManager.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5dc709bd10..b158f0dfaf 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,12 +20,25 @@ import MatrixClientPeg from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; +// This stores the secret storage private keys in memory for the JS SDK. This is +// only meant to act as a cache to avoid prompting the user multiple times +// during the same session. It is considered unsafe to persist this to normal +// web storage. For platforms with a secure enclave, we will store this key +// there. +const secretStorageKeys = {}; + 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( @@ -54,5 +67,9 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { throw new Error("Secret storage access canceled"); } const key = await inputToKey(input); + + // Save to cache to avoid future prompts in the current session + secretStorageKeys[name] = key; + return [name, key]; }; From 66f760096974a1d46010541d86e40277d9674839 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Dec 2019 15:05:03 +0000 Subject: [PATCH 2/9] Add `accessSecretStorage` helper with common flow setup This moves the details of dialogs that may be needed when accessing secret storage to centralised helper. In addition, this clears the secret storage key cache so that keys are only live for a single operation. --- src/CrossSigningManager.js | 77 ++++++++++++++++++- src/MatrixClientPeg.js | 4 +- .../views/settings/CrossSigningPanel.js | 34 +------- src/i18n/strings/en_EN.json | 2 +- 4 files changed, 78 insertions(+), 39 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b158f0dfaf..addde2215d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -19,13 +19,14 @@ 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'; +import { _t } from './languageHandler'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times -// during the same session. It is considered unsafe to persist this to normal -// web storage. For platforms with a secure enclave, we will store this key -// there. -const secretStorageKeys = {}; +// during the same single operation. Use `accessSecretStorage` below to scope a +// single secret storage operation, as it will clear the cached keys once the +// operation ends. +let secretStorageKeys = {}; export const getSecretStorageKey = async ({ keys: keyInfos }) => { const keyInfoEntries = Object.entries(keyInfos); @@ -73,3 +74,71 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { return [name, key]; }; + +export const crossSigningCallbacks = { + getSecretStorageKey, +}; + +/** + * 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 + * each other in a cycle of sorts) have been bootstrapped before running the + * provided function. + * + * Bootstrapping secret storage may take one of these paths: + * 1. Create secret storage from a passphrase and store cross-signing keys + * in secret storage. + * 2. Access existing secret storage by requesting passphrase and accessing + * cross-signing keys as needed. + * 3. All keys are loaded and there's nothing to do. + * + * Additionally, the secret storage keys are cached during the scope of this function + * to ensure the user is prompted only once for their secret storage + * passphrase. The cache is then + * + * @param {Function} [func] An operation to perform once secret storage has been + * bootstrapped. Optional. + */ +export async function accessSecretStorage(func = async () => { }) { + const cli = MatrixClientPeg.get(); + + try { + if (!cli.hasSecretStorageKey()) { + // 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"), + null, null, /* priority = */ false, /* static = */ true, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Secret storage creation canceled"); + } + } else { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: async (makeRequest) => { + 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"); + } + }, + }); + } + + // `return await` needed here to ensure `finally` block runs after the + // inner operation completes. + return await func(); + } finally { + // Clear secret storage key cache now that work is complete + secretStorageKeys = {}; + } +} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a3a0588bfc..51ac7acb37 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import * as CrossSigningManager from './CrossSigningManager'; +import { crossSigningCallbacks } from './CrossSigningManager'; interface MatrixClientCreds { homeserverUrl: string, @@ -224,7 +224,7 @@ class MatrixClientPeg { opts.cryptoCallbacks = {}; if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - Object.assign(opts.cryptoCallbacks, CrossSigningManager); + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); } this.matrixClient = createMatrixClient(opts); diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index fda92ebac9..f3a7a62e8f 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -19,7 +19,7 @@ import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; -import Modal from '../../../Modal'; +import { accessSecretStorage } from '../../../CrossSigningManager'; export default class CrossSigningPanel extends React.PureComponent { constructor(props) { @@ -78,38 +78,8 @@ export default class CrossSigningPanel extends React.PureComponent { */ _bootstrapSecureSecretStorage = async () => { this.setState({ error: null }); - const cli = MatrixClientPeg.get(); try { - if (!cli.hasSecretStorageKey()) { - // 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"), - null, null, /* priority = */ false, /* static = */ true, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Secret storage creation canceled"); - } - } else { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async (makeRequest) => { - 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"); - } - }, - }); - } + await accessSecretStorage(); } catch (e) { this.setState({ error: e }); console.error("Error bootstrapping secret storage", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7cf0aeaf36..ee973cf485 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -57,6 +57,7 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "The server does not support the room version specified.": "The server does not support the room version specified.", "Failure to create room": "Failure to create room", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Send anyway": "Send anyway", "Send": "Send", "Sun": "Sun", @@ -495,7 +496,6 @@ "New Password": "New Password", "Confirm password": "Confirm password", "Change Password": "Change Password", - "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Cross-signing public keys:": "Cross-signing public keys:", "on device": "on device", "not found": "not found", From 8cbc9baddd171ab647d2da94d06de23e21ca2a6a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Dec 2019 16:32:47 +0000 Subject: [PATCH 3/9] Add testing flow for new key backups with SSSS This adds a path to test key backups with SSSS via an extra button only visible when the cross-signing feature is enabled. --- .../keybackup/CreateKeyBackupDialog.js | 7 ++- .../views/settings/KeyBackupPanel.js | 44 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index eae102196f..3fac00c1b3 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -16,12 +16,11 @@ limitations under the License. */ import React from 'react'; +import FileSaver from 'file-saver'; + import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; - -import FileSaver from 'file-saver'; - import { _t } from '../../../../languageHandler'; const PHASE_PASSPHRASE = 0; @@ -118,7 +117,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { phase: PHASE_DONE, }); } catch (e) { - console.log("Error creating key backup", e); + console.error("Error creating key backup", e); // TODO: If creating a version succeeds, but backup fails, should we // delete the version, disable backup, or do nothing? If we just // disable without deleting, we'll enable on next app reload since diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index c2fb3dc9db..cbbb041228 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,6 +21,8 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import SettingsStore from '../../../../lib/settings/SettingsStore'; +import { accessSecretStorage } from '../../../CrossSigningManager'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -125,6 +127,31 @@ export default class KeyBackupPanel extends React.PureComponent { ); } + _startNewBackupWithSecureSecretStorage = async () => { + const cli = MatrixClientPeg.get(); + let info; + try { + await accessSecretStorage(async () => { + info = await cli.prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await cli.createKeyBackupVersion(info); + }); + await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); + this._loadBackupStatus(); + } catch (e) { + console.error("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info && info.version) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } + } + } + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { @@ -299,6 +326,22 @@ export default class KeyBackupPanel extends React.PureComponent { ; } else { + // This is a temporary button for testing the new path which stores + // the key backup key in SSSS. Initialising SSSS depends on + // cross-signing and is part of the same project, so we only show + // this mode when the cross-signing feature is enabled. + // TODO: Clean this up when removing the feature flag. + let secureSecretStorageKeyBackup; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + secureSecretStorageKeyBackup = ( +
+ + {_t("Start using Key Backup with Secure Secret Storage")} + +
+ ); + } + return

{_t( @@ -313,6 +356,7 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Start using Key Backup")}

+ {secureSecretStorageKeyBackup}
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ee973cf485..2425dfcafa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -540,6 +540,7 @@ "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", + "Start using Key Backup with Secure Secret Storage": "Start using Key Backup with Secure Secret Storage", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Start using Key Backup": "Start using Key Backup", From d2f2d3b3442aff1921d468461a7e51c83dc04dce Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 13:51:45 +0000 Subject: [PATCH 4/9] Handle cross-singing sigs in key backup details --- .../views/settings/KeyBackupPanel.js | 23 +++++++++++++++++-- src/i18n/strings/en_EN.json | 5 +++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index cbbb041228..44244f688b 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -250,10 +250,29 @@ export default class KeyBackupPanel extends React.PureComponent { sig.device && sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key() ); + const fromThisUser = ( + sig.crossSigningId && + sig.deviceId === MatrixClientPeg.get().getCrossSigningId() + ); let sigStatus; - if (!sig.device) { + if (sig.valid && fromThisUser) { sigStatus = _t( - "Backup has a signature from unknown device with ID %(deviceId)s.", + "Backup has a valid signature from this user", + {}, { validity }, + ); + } else if (!sig.valid && fromThisUser) { + sigStatus = _t( + "Backup has a invalid signature from this user", + {}, { validity }, + ); + } else if (sig.crossSigningId) { + sigStatus = _t( + "Backup has a signature from unknown user with ID %(deviceId)s", + { deviceId: sig.deviceId }, { verify }, + ); + } else if (!sig.device) { + sigStatus = _t( + "Backup has a signature from unknown device with ID %(deviceId)s", { deviceId: sig.deviceId }, { verify }, ); } else if (sig.valid && fromThisDevice) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2425dfcafa..f6bc9c7c41 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -529,7 +529,10 @@ "Connect this device to Key Backup": "Connect this device to Key Backup", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", - "Backup has a signature from unknown device with ID %(deviceId)s.": "Backup has a signature from unknown device with ID %(deviceId)s.", + "Backup has a valid signature from this user": "Backup has a valid signature from this user", + "Backup has a invalid signature from this user": "Backup has a invalid signature from this user", + "Backup has a signature from unknown user with ID %(deviceId)s": "Backup has a signature from unknown user with ID %(deviceId)s", + "Backup has a signature from unknown device with ID %(deviceId)s": "Backup has a signature from unknown device with ID %(deviceId)s", "Backup has a valid signature from this device": "Backup has a valid signature from this device", "Backup has an invalid signature from this device": "Backup has an invalid signature from this device", "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", From 4417235d9e859acc5786cbf68d54fc5e69000143 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 14:06:44 +0000 Subject: [PATCH 5/9] Show whether backup key is stored --- src/components/views/settings/KeyBackupPanel.js | 16 ++++++++++++++++ src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 44244f688b..1a28b89354 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -33,6 +33,8 @@ export default class KeyBackupPanel extends React.PureComponent { loading: true, error: null, backupInfo: null, + backupSigStatus: null, + backupKeyStored: null, sessionsRemaining: 0, }; } @@ -74,9 +76,11 @@ export default class KeyBackupPanel extends React.PureComponent { async _checkKeyBackupStatus() { try { const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup(); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); this.setState({ backupInfo, backupSigStatus: trustInfo, + backupKeyStored, error: null, loading: false, }); @@ -87,6 +91,7 @@ export default class KeyBackupPanel extends React.PureComponent { error: e, backupInfo: null, backupSigStatus: null, + backupKeyStored: null, loading: false, }); } @@ -97,11 +102,13 @@ export default class KeyBackupPanel extends React.PureComponent { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); if (this._unmounted) return; this.setState({ error: null, backupInfo, backupSigStatus, + backupKeyStored, loading: false, }); } catch (e) { @@ -111,6 +118,7 @@ export default class KeyBackupPanel extends React.PureComponent { error: e, backupInfo: null, backupSigStatus: null, + backupKeyStored: null, loading: false, }); } @@ -220,6 +228,13 @@ export default class KeyBackupPanel extends React.PureComponent { restoreButtonCaption = _t("Connect this device to Key Backup"); } + let keyStatus; + if (this.state.backupKeyStored === true) { + keyStatus = _t("in secret storage"); + } else { + keyStatus = _t("not stored"); + } + let uploadStatus; const { sessionsRemaining } = this.state; if (!MatrixClientPeg.get().getKeyBackupEnabled()) { @@ -331,6 +346,7 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Advanced")}
{_t("Backup version: ")}{this.state.backupInfo.version}
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}
+
{_t("Backup key stored: ")}{keyStatus}
{uploadStatus}
{backupSigStatuses}
{trustedLocally}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f6bc9c7c41..a12f98ebb5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -527,6 +527,7 @@ "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.", "Connect this device to Key Backup": "Connect this device to Key Backup", + "not stored": "not stored", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", "Backup has a valid signature from this user": "Backup has a valid signature from this user", @@ -543,6 +544,7 @@ "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", + "Backup key stored: ": "Backup key stored: ", "Start using Key Backup with Secure Secret Storage": "Start using Key Backup with Secure Secret Storage", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", From e87ff54eee87a77653af032940bb6e2f9e3ed359 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 14:27:57 +0000 Subject: [PATCH 6/9] Clean up cross-signing debug panel --- .../views/settings/CrossSigningPanel.js | 69 ++++++++++++++----- src/i18n/strings/en_EN.json | 5 +- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index f3a7a62e8f..06c4783c4a 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -102,28 +102,59 @@ export default class CrossSigningPanel extends React.PureComponent { errorSection =
{error.toString()}
; } + const enabled = ( + crossSigningPublicKeysOnDevice && + crossSigningPrivateKeysInStorage && + secretStorageKeyInAccount + ); + + let summarisedStatus; + if (enabled) { + summarisedStatus =

✅ {_t( + "Cross-signing and secret storage are enabled.", + )}

; + } else if (crossSigningPrivateKeysInStorage) { + summarisedStatus =

{_t( + "Your account has a cross-signing identity in secret storage, but it " + + "is not yet trusted by this device.", + )}

; + } else { + summarisedStatus =

{_t( + "Cross-signing and secret storage are not yet set up.", + )}

; + } + + let bootstrapButton; + if (!enabled) { + bootstrapButton =
+ + {_t("Bootstrap cross-signing and secret storage")} + +
; + } + return (
- - - - - - - - - - - - - -
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
-
- - {_t("Bootstrap Secure Secret Storage")} - -
+ {summarisedStatus} +
+ {_t("Advanced")} + + + + + + + + + + + + + +
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
{errorSection} + {bootstrapButton}
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a12f98ebb5..f44d3d7b67 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -496,6 +496,10 @@ "New Password": "New Password", "Confirm password": "Confirm password", "Change Password": "Change Password", + "Cross-signing and secret storage are enabled.": "Cross-signing and secret storage are enabled.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.", + "Cross-signing and secret storage are not yet set up.": "Cross-signing and secret storage are not yet set up.", + "Bootstrap cross-signing and secret storage": "Bootstrap cross-signing and secret storage", "Cross-signing public keys:": "Cross-signing public keys:", "on device": "on device", "not found": "not found", @@ -503,7 +507,6 @@ "in secret storage": "in secret storage", "Secret storage public key:": "Secret storage public key:", "in account data": "in account data", - "Bootstrap Secure Secret Storage": "Bootstrap Secure Secret Storage", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", "Authentication": "Authentication", From 458cc9598d89314077e4aed8be6b9262fe6d4ecd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 15:18:36 +0000 Subject: [PATCH 7/9] Support restoring key backup with stored secret --- .../views/settings/KeyBackupPanel.js | 48 ++++++++++++++----- src/i18n/strings/en_EN.json | 1 + 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 1a28b89354..559b1e0ba1 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -180,10 +180,23 @@ export default class KeyBackupPanel extends React.PureComponent { }); } - _restoreBackup = () => { - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - }); + _restoreBackup = async () => { + // Use legacy path if backup key not stored in secret storage + if (!this.state.backupKeyStored) { + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog); + return; + } + + try { + await accessSecretStorage(async () => { + await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + this.state.backupInfo, + ); + }); + } catch (e) { + console.log("Error restoring backup", e); + } } render() { @@ -340,6 +353,24 @@ export default class KeyBackupPanel extends React.PureComponent { trustedLocally = _t("This backup is trusted because it has been restored on this device"); } + let buttonRow = ( +
+ + {restoreButtonCaption} +     + + {_t("Delete Backup")} + +
+ ); + if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) { + buttonRow =

⚠️ {_t( + "Backup key stored in secret storage, but this feature is not " + + "enabled on this device. Please enable cross-signing in Labs to " + + "modify key backup state.", + )}

; + } + return
{clientBackupStatus}
@@ -351,14 +382,7 @@ export default class KeyBackupPanel extends React.PureComponent {
{backupSigStatuses}
{trustedLocally}
-
- - {restoreButtonCaption} -     - - { _t("Delete Backup") } - -
+ {buttonRow}
; } else { // This is a temporary button for testing the new path which stores diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f44d3d7b67..a89f44b2c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -545,6 +545,7 @@ "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", "Backup is not signed by any of your devices": "Backup is not signed by any of your devices", "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", + "Backup key stored in secret storage, but this feature is not enabled on this device. Please enable cross-signing in Labs to modify key backup state.": "Backup key stored in secret storage, but this feature is not enabled on this device. Please enable cross-signing in Labs to modify key backup state.", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", "Backup key stored: ": "Backup key stored: ", From 6338ee9683bcd8a0e54244b005f036df74d9e973 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 15:34:01 +0000 Subject: [PATCH 8/9] Only allow key caching inside the access helper --- src/CrossSigningManager.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index addde2215d..ab0a22e4d5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -27,8 +27,9 @@ import { _t } from './languageHandler'; // single secret storage operation, as it will clear the cached keys once the // operation ends. let secretStorageKeys = {}; +let cachingAllowed = false; -export const getSecretStorageKey = async ({ keys: keyInfos }) => { +async function getSecretStorageKey({ keys: keyInfos }) { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); @@ -36,7 +37,7 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { const [name, info] = keyInfoEntries[0]; // Check the in-memory cache - if (secretStorageKeys[name]) { + if (cachingAllowed && secretStorageKeys[name]) { return [name, secretStorageKeys[name]]; } @@ -70,10 +71,12 @@ export const getSecretStorageKey = async ({ keys: keyInfos }) => { const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - secretStorageKeys[name] = key; + if (cachingAllowed) { + secretStorageKeys[name] = key; + } return [name, key]; -}; +} export const crossSigningCallbacks = { getSecretStorageKey, @@ -101,6 +104,7 @@ export const crossSigningCallbacks = { */ export async function accessSecretStorage(func = async () => { }) { const cli = MatrixClientPeg.get(); + cachingAllowed = true; try { if (!cli.hasSecretStorageKey()) { @@ -139,6 +143,7 @@ export async function accessSecretStorage(func = async () => { }) { return await func(); } finally { // Clear secret storage key cache now that work is complete + cachingAllowed = false; secretStorageKeys = {}; } } From 3cbb3c12cd1139a1dad9fae27cc42abb23849c4f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 15:42:27 +0000 Subject: [PATCH 9/9] Update cross-signing details on trust changes --- src/components/views/settings/CrossSigningPanel.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 06c4783c4a..ccab886f17 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -36,6 +36,8 @@ export default class CrossSigningPanel extends React.PureComponent { componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("accountData", this.onAccountData); + cli.on("userTrustStatusChanged", this.onStatusChanged); + cli.on("crossSigning.keysChanged", this.onStatusChanged); } componentWillUnmount() { @@ -43,6 +45,8 @@ export default class CrossSigningPanel extends React.PureComponent { const cli = MatrixClientPeg.get(); if (!cli) return; cli.removeListener("accountData", this.onAccountData); + cli.removeListener("userTrustStatusChanged", this.onStatusChanged); + cli.removeListener("crossSigning.keysChanged", this.onStatusChanged); } onAccountData = (event) => { @@ -52,6 +56,10 @@ export default class CrossSigningPanel extends React.PureComponent { } }; + onStatusChanged = () => { + this.setState(this._getUpdatedStatus()); + }; + _getUpdatedStatus() { // XXX: Add public accessors if we keep this around in production const cli = MatrixClientPeg.get();