Merge pull request #3720 from matrix-org/jryans/4s-new-key-backup
Create new key backups using secret storage
This commit is contained in:
commit
b7fe06706d
6 changed files with 287 additions and 75 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue