Merge pull request #3720 from matrix-org/jryans/4s-new-key-backup

Create new key backups using secret storage
pull/21833/head
J. Ryan Stinnett 2019-12-12 17:33:11 +00:00 committed by GitHub
commit b7fe06706d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 287 additions and 75 deletions

View File

@ -19,13 +19,28 @@ 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';
export const getSecretStorageKey = async ({ keys: keyInfos }) => {
// 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 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 = {};
let cachingAllowed = false;
async function getSecretStorageKey({ 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 (cachingAllowed && secretStorageKeys[name]) {
return [name, secretStorageKeys[name]];
}
const inputToKey = async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
@ -54,5 +69,81 @@ 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
if (cachingAllowed) {
secretStorageKeys[name] = key;
}
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();
cachingAllowed = true;
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
cachingAllowed = false;
secretStorageKeys = {};
}
}

View File

@ -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);

View File

@ -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

View File

@ -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) {
@ -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();
@ -78,38 +86,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);
@ -132,28 +110,59 @@ export default class CrossSigningPanel extends React.PureComponent {
errorSection = <div className="error">{error.toString()}</div>;
}
const enabled = (
crossSigningPublicKeysOnDevice &&
crossSigningPrivateKeysInStorage &&
secretStorageKeyInAccount
);
let summarisedStatus;
if (enabled) {
summarisedStatus = <p> {_t(
"Cross-signing and secret storage are enabled.",
)}</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
"Your account has a cross-signing identity in secret storage, but it " +
"is not yet trusted by this device.",
)}</p>;
} else {
summarisedStatus = <p>{_t(
"Cross-signing and secret storage are not yet set up.",
)}</p>;
}
let bootstrapButton;
if (!enabled) {
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
{_t("Bootstrap cross-signing and secret storage")}
</AccessibleButton>
</div>;
}
return (
<div>
<table className="mx_CrossSigningPanel_statusList"><tbody>
<tr>
<td>{_t("Cross-signing public keys:")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
</tr>
</tbody></table>
<div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
{_t("Bootstrap Secure Secret Storage")}
</AccessibleButton>
</div>
{summarisedStatus}
<details>
<summary>{_t("Advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList"><tbody>
<tr>
<td>{_t("Cross-signing public keys:")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
</tr>
</tbody></table>
</details>
{errorSection}
{bootstrapButton}
</div>
);
}

View File

@ -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) {
@ -31,6 +33,8 @@ export default class KeyBackupPanel extends React.PureComponent {
loading: true,
error: null,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
sessionsRemaining: 0,
};
}
@ -72,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,
});
@ -85,6 +91,7 @@ export default class KeyBackupPanel extends React.PureComponent {
error: e,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
loading: false,
});
}
@ -95,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) {
@ -109,6 +118,7 @@ export default class KeyBackupPanel extends React.PureComponent {
error: e,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
loading: false,
});
}
@ -125,6 +135,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, {
@ -145,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() {
@ -193,6 +241,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()) {
@ -223,10 +278,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 <verify>unknown</verify> device with ID %(deviceId)s.",
"Backup has a <validity>valid</validity> signature from this user",
{}, { validity },
);
} else if (!sig.valid && fromThisUser) {
sigStatus = _t(
"Backup has a <validity>invalid</validity> signature from this user",
{}, { validity },
);
} else if (sig.crossSigningId) {
sigStatus = _t(
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s",
{ deviceId: sig.deviceId }, { verify },
);
} else if (!sig.device) {
sigStatus = _t(
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s",
{ deviceId: sig.deviceId }, { verify },
);
} else if (sig.valid && fromThisDevice) {
@ -279,26 +353,54 @@ export default class KeyBackupPanel extends React.PureComponent {
trustedLocally = _t("This backup is trusted because it has been restored on this device");
}
let buttonRow = (
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>
</div>
);
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
buttonRow = <p> {_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.",
)}</p>;
}
return <div>
<div>{clientBackupStatus}</div>
<details>
<summary>{_t("Advanced")}</summary>
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
<div>{_t("Backup key stored: ")}{keyStatus}</div>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</details>
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete Backup") }
</AccessibleButton>
</div>
{buttonRow}
</div>;
} 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 = (
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._startNewBackupWithSecureSecretStorage}>
{_t("Start using Key Backup with Secure Secret Storage")}
</AccessibleButton>
</div>
);
}
return <div>
<div>
<p>{_t(
@ -313,6 +415,7 @@ export default class KeyBackupPanel extends React.PureComponent {
{_t("Start using Key Backup")}
</AccessibleButton>
</div>
{secureSecretStorageKeyBackup}
</div>;
}
}

View File

@ -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",
@ -512,7 +513,10 @@
"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 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",
@ -520,7 +524,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",
@ -544,9 +547,13 @@
"This device is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "This device is <b>not backing up your keys</b>, 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 signature from <verify>unknown</verify> device with ID %(deviceId)s.": "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
"Backup has a <validity>valid</validity> signature from this user": "Backup has a <validity>valid</validity> signature from this user",
"Backup has a <validity>invalid</validity> signature from this user": "Backup has a <validity>invalid</validity> signature from this user",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s",
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s": "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s",
"Backup has a <validity>valid</validity> signature from this device": "Backup has a <validity>valid</validity> signature from this device",
"Backup has an <validity>invalid</validity> signature from this device": "Backup has an <validity>invalid</validity> signature from this device",
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>",
@ -555,8 +562,11 @@
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></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: ",
"Start using Key Backup with Secure Secret Storage": "Start using Key Backup with Secure Secret Storage",
"Your keys are <b>not being backed up from this device</b>.": "Your keys are <b>not being backed up from this device</b>.",
"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",