Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3720 from matrix-org/jryans/4s-new-key-backup
Browse files Browse the repository at this point in the history
Create new key backups using secret storage
  • Loading branch information
jryans authored Dec 12, 2019
2 parents afac882 + 3cbb3c1 commit b7fe067
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 75 deletions.
93 changes: 92 additions & 1 deletion src/CrossSigningManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {};
}
}
4 changes: 2 additions & 2 deletions src/MatrixClientPeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
111 changes: 60 additions & 51 deletions src/components/views/settings/CrossSigningPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -36,13 +36,17 @@ 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() {
this._unmounted = true;
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) => {
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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>
);
}
Expand Down
Loading

0 comments on commit b7fe067

Please sign in to comment.