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

Add testing flow to bootstrap secret storage
This commit is contained in:
J. Ryan Stinnett 2019-12-11 11:04:36 +00:00 committed by GitHub
commit c2cd97fab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1386 additions and 149 deletions

View file

@ -64,7 +64,6 @@
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss";
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
@ -83,6 +82,8 @@
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss";
@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss";
@import "./views/directory/_NetworkDropdown.scss";
@import "./views/elements/_AccessibleButton.scss";
@import "./views/elements/_AddressSelector.scss";
@ -174,6 +175,7 @@
@import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_CrossSigningPanel.scss";
@import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_IntegrationManager.scss";

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,6 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RestoreKeyBackupDialog_keyStatus {
height: 30px;
}
.mx_RestoreKeyBackupDialog_primaryContainer {
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
padding: 20px;

View file

@ -0,0 +1,34 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AccessSecretStorageDialog_keyStatus {
height: 30px;
}
.mx_AccessSecretStorageDialog_primaryContainer {
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
padding: 20px;
}
.mx_AccessSecretStorageDialog_passPhraseInput,
.mx_AccessSecretStorageDialog_recoveryKeyInput {
width: 300px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
}

View file

@ -0,0 +1,88 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CreateSecretStorageDialog .mx_Dialog_title {
/* TODO: Consider setting this for all dialog titles. */
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_primaryContainer {
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
padding: 20px;
}
.mx_CreateSecretStorageDialog_primaryContainer::after {
content: "";
clear: both;
display: block;
}
.mx_CreateSecretStorageDialog_passPhraseContainer {
display: flex;
align-items: start;
}
.mx_CreateSecretStorageDialog_passPhraseHelp {
flex: 1;
height: 85px;
margin-left: 20px;
font-size: 80%;
}
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
width: 100%;
}
.mx_CreateSecretStorageDialog_passPhraseInput {
flex: none;
width: 250px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_passPhraseMatch {
margin-left: 20px;
}
.mx_CreateSecretStorageDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_recoveryKeyContainer {
display: flex;
}
.mx_CreateSecretStorageDialog_recoveryKey {
width: 262px;
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
margin-right: 12px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons {
flex: 1;
display: flex;
align-items: center;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
flex: 1;
white-space: nowrap;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,6 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RestoreKeyBackupDialog_keyStatus {
height: 30px;
.mx_CrossSigningPanel_statusList {
border-spacing: 0;
td {
padding: 0;
&:first-of-type {
padding-inline-end: 1em;
}
}
}
.mx_CrossSigningPanel_buttonRow {
margin: 1em 0;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -30,3 +31,7 @@ limitations under the License.
.mx_KeyBackupPanel_deviceName {
font-style: italic;
}
.mx_KeyBackupPanel_buttonRow {
margin: 1em 0;
}

View file

@ -0,0 +1,58 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Modal from './Modal';
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';
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];
const inputToKey = async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
passphrase,
info.passphrase.salt,
info.passphrase.iterations,
);
} else {
return decodeRecoveryKey(recoveryKey);
}
};
const AccessSecretStorageDialog =
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
{
keyInfo: info,
checkPrivateKey: async (input) => {
const key = await inputToKey(input);
return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey);
},
},
);
const [input] = await finished;
if (!input) {
throw new Error("Secret storage access canceled");
}
const key = await inputToKey(input);
return [name, key];
};

View file

@ -1,7 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd
Copyright 2017, 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -30,6 +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';
interface MatrixClientCreds {
homeserverUrl: string,
@ -220,14 +222,9 @@ class MatrixClientPeg {
identityServer: new IdentityAuthClient(),
};
opts.cryptoCallbacks = {};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// TODO: Cross-signing keys are temporarily in memory only. A
// separate task in the cross-signing project will build from here.
const keys = [];
opts.cryptoCallbacks = {
getCrossSigningKey: k => keys[k],
saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys),
};
Object.assign(opts.cryptoCallbacks, CrossSigningManager);
}
this.matrixClient = createMatrixClient(opts);

View file

@ -1,5 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
@ -45,13 +45,15 @@ function selectText(target) {
selection.addRange(range);
}
/**
/*
* Walks the user through the process of creating an e2e key backup
* on the server.
*/
export default createReactClass({
getInitialState: function() {
return {
export default class CreateKeyBackupDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
phase: PHASE_PASSPHRASE,
passPhrase: '',
passPhraseConfirm: '',
@ -60,25 +62,25 @@ export default createReactClass({
zxcvbnResult: null,
setPassPhrase: false,
};
},
}
componentWillMount: function() {
componentWillMount() {
this._recoveryKeyNode = null;
this._keyBackupInfo = null;
this._setZxcvbnResultTimeout = null;
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
},
}
_collectRecoveryKeyNode: function(n) {
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
},
}
_onCopyClick: function() {
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) {
@ -87,9 +89,9 @@ export default createReactClass({
phase: PHASE_KEEPITSAFE,
});
}
},
}
_onDownloadClick: function() {
_onDownloadClick = () => {
const blob = new Blob([this._keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
});
@ -99,9 +101,9 @@ export default createReactClass({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
},
}
_createBackup: async function() {
_createBackup = async () => {
this.setState({
phase: PHASE_BACKINGUP,
error: null,
@ -128,38 +130,38 @@ export default createReactClass({
error: e,
});
}
},
}
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
}
_onDone: function() {
_onDone = () => {
this.props.onFinished(true);
},
}
_onOptOutClick: function() {
_onOptOutClick = () => {
this.setState({phase: PHASE_OPTOUT_CONFIRM});
},
}
_onSetUpClick: function() {
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
},
}
_onSkipPassPhraseClick: async function() {
_onSkipPassPhraseClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseNextClick: function() {
_onPassPhraseNextClick = () => {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
},
}
_onPassPhraseKeyPress: async function(e) {
_onPassPhraseKeyPress = async (e) => {
if (e.key === 'Enter') {
// If we're waiting for the timeout before updating the result at this point,
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
@ -177,9 +179,9 @@ export default createReactClass({
this._onPassPhraseNextClick();
}
}
},
}
_onPassPhraseConfirmNextClick: async function() {
_onPassPhraseConfirmNextClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.setState({
setPassPhrase: true,
@ -187,30 +189,30 @@ export default createReactClass({
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseConfirmKeyPress: function(e) {
_onPassPhraseConfirmKeyPress = (e) => {
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
this._onPassPhraseConfirmNextClick();
}
},
}
_onSetAgainClick: function() {
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
});
},
}
_onKeepItSafeBackClick: function() {
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
},
}
_onPassPhraseChange: function(e) {
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
@ -227,19 +229,19 @@ export default createReactClass({
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
},
}
_onPassPhraseConfirmChange: function(e) {
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
},
}
_passPhraseIsValid: function() {
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
},
}
_renderPhasePassPhrase: function() {
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let strengthMeter;
@ -266,7 +268,7 @@ export default createReactClass({
return <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup from a trusted computer.", {},
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
@ -305,9 +307,9 @@ export default createReactClass({
</button></p>
</details>
</div>;
},
}
_renderPhasePassPhraseConfirm: function() {
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText;
@ -361,9 +363,9 @@ export default createReactClass({
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</div>;
},
}
_renderPhaseShowKey: function() {
_renderPhaseShowKey() {
let bodyText;
if (this.state.setPassPhrase) {
bodyText = _t(
@ -380,7 +382,7 @@ export default createReactClass({
"access to your encrypted messages if you forget your passphrase.",
)}</p>
<p>{_t(
"Keep your recovery key somewhere very secure, like a password manager (or a safe)",
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
)}</p>
<p>{bodyText}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
@ -402,18 +404,18 @@ export default createReactClass({
</div>
</div>
</div>;
},
}
_renderPhaseKeepItSafe: function() {
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your Recovery Key is in your <b>Downloads</b> folder.",
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
@ -431,16 +433,16 @@ export default createReactClass({
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
},
}
_renderBusyPhase: function(text) {
_renderBusyPhase(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
</div>;
},
}
_renderPhaseDone: function() {
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
@ -451,9 +453,9 @@ export default createReactClass({
hasCancel={false}
/>
</div>;
},
}
_renderPhaseOptOutConfirm: function() {
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
@ -467,9 +469,9 @@ export default createReactClass({
<button onClick={this._onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
},
}
_titleForPhase: function(phase) {
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
return _t('Secure your backup with a passphrase');
@ -488,9 +490,9 @@ export default createReactClass({
default:
return _t("Create Key Backup");
}
},
}
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content;
@ -543,5 +545,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,564 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
const PHASE_SHOWKEY = 2;
const PHASE_KEEPITSAFE = 3;
const PHASE_STORING = 4;
const PHASE_DONE = 5;
const PHASE_OPTOUT_CONFIRM = 6;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
// XXX: copied from ShareDialog: factor out into utils
function selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
*/
export default class CreateSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
this._keyInfo = null;
this._encodedRecoveryKey = null;
this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null;
this.state = {
phase: PHASE_PASSPHRASE,
passPhrase: '',
passPhraseConfirm: '',
copied: false,
downloaded: false,
zxcvbnResult: null,
setPassPhrase: false,
};
}
componentWillUnmount() {
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
}
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) {
this.setState({
copied: true,
phase: PHASE_KEEPITSAFE,
});
}
}
_onDownloadClick = () => {
const blob = new Blob([this._encodedRecoveryKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
this.setState({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
}
_bootstrapSecretStorage = async () => {
this.setState({
phase: PHASE_STORING,
error: null,
});
const cli = MatrixClientPeg.get();
try {
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");
}
},
createSecretStorageKey: async () => this._keyInfo,
});
this.setState({
phase: PHASE_DONE,
});
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
}
}
_onCancel = () => {
this.props.onFinished(false);
}
_onDone = () => {
this.props.onFinished(true);
}
_onOptOutClick = () => {
this.setState({phase: PHASE_OPTOUT_CONFIRM});
}
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
}
_onSkipPassPhraseClick = async () => {
const [keyInfo, encodedRecoveryKey] =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseNextClick = () => {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
}
_onPassPhraseKeyPress = async (e) => {
if (e.key === 'Enter') {
// If we're waiting for the timeout before updating the result at this point,
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
// even if the user entered a valid passphrase
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
this._setZxcvbnResultTimeout = null;
await new Promise((resolve) => {
this.setState({
zxcvbnResult: scorePassword(this.state.passPhrase),
}, resolve);
});
}
if (this._passPhraseIsValid()) {
this._onPassPhraseNextClick();
}
}
}
_onPassPhraseConfirmNextClick = async () => {
const [keyInfo, encodedRecoveryKey] =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({
setPassPhrase: true,
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseConfirmKeyPress = (e) => {
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
this._onPassPhraseConfirmNextClick();
}
}
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
});
}
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
this._setZxcvbnResultTimeout = setTimeout(() => {
this._setZxcvbnResultTimeout = null;
this.setState({
// precompute this and keep it in state: zxcvbn is fast but
// we use it in a couple of different places so no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
}
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
}
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
}
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let strengthMeter;
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This passphrase looks strong enough.");
} else {
const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
}
const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
helpText = <div>
{this.state.zxcvbnResult.feedback.warning}
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
return <div>
<p>{_t(
"<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"We'll use secret storage to optionally store an encrypted copy of " +
"your cross-signing identity for verifying other devices and message " +
"keys on our server. Protect your access to encrypted messages with a " +
"passphrase to keep it secure.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateSecretStorageDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
autoFocus={true}
/>
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false}
disabled={!this._passPhraseIsValid()}
/>
<details>
<summary>{_t("Advanced")}</summary>
<p><button onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</button></p>
</details>
</div>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
}
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")}
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Please enter your passphrase a second time to confirm.",
)}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
</div>
{passPhraseMatch}
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</div>;
}
_renderPhaseShowKey() {
let bodyText;
if (this.state.setPassPhrase) {
bodyText = _t(
"As a safety net, you can use it to restore your access to encrypted " +
"messages if you forget your passphrase.",
);
} else {
bodyText = _t(
"As a safety net, you can use it to restore your access to encrypted " +
"messages.",
);
}
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your passphrase.",
)}</p>
<p>{_t(
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
)}</p>
<p>{bodyText}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</div>
</div>;
}
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("OK")}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
}
_renderBusyPhase(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
</div>;
}
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Your access to encrypted messages is now protected.",
)}</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
/>
</div>;
}
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
"Without setting up secret storage, you won't be able to restore your " +
"access to encrypted messages or your cross-signing identity for " +
"verifying other devices if you log out or use another device.",
)}
<DialogButtons primaryButton={_t('Set up secret storage')}
onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false}
>
<button onClick={this._onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
}
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
return _t('Secure your encrypted messages with a passphrase');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your passphrase');
case PHASE_OPTOUT_CONFIRM:
return _t('Warning!');
case PHASE_SHOWKEY:
return _t('Recovery key');
case PHASE_KEEPITSAFE:
return _t('Keep it safe');
case PHASE_STORING:
return _t('Storing secrets...');
case PHASE_DONE:
return _t('Success!');
default:
return null;
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content;
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={true}
onCancel={this._onCancel}
/>
</div>
</div>;
} else {
switch (this.state.phase) {
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY:
content = this._renderPhaseShowKey();
break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
break;
case PHASE_OPTOUT_CONFIRM:
content = this._renderPhaseOptOutConfirm();
break;
}
}
return (
<BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import Modal from '../../../../Modal';
@ -28,12 +27,13 @@ import {Key} from "../../../../Keyboard";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
/**
/*
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default createReactClass({
getInitialState: function() {
return {
export default class RestoreKeyBackupDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
backupInfo: null,
loading: false,
loadError: null,
@ -45,27 +45,27 @@ export default createReactClass({
passPhrase: '',
restoreType: null,
};
},
}
componentWillMount: function() {
componentDidMount() {
this._loadBackupStatus();
},
}
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
}
_onDone: function() {
_onDone = () => {
this.props.onFinished(true);
},
}
_onUseRecoveryKeyClick: function() {
_onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
},
}
_onResetRecoveryClick: function() {
_onResetRecoveryClick = () => {
this.props.onFinished(false);
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
@ -75,16 +75,16 @@ export default createReactClass({
},
},
);
},
}
_onRecoveryKeyChange: function(e) {
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
});
},
}
_onPassPhraseNext: async function() {
_onPassPhraseNext = async () => {
this.setState({
loading: true,
restoreError: null,
@ -105,9 +105,9 @@ export default createReactClass({
restoreError: e,
});
}
},
}
_onRecoveryKeyNext: async function() {
_onRecoveryKeyNext = async () => {
this.setState({
loading: true,
restoreError: null,
@ -128,27 +128,27 @@ export default createReactClass({
restoreError: e,
});
}
},
}
_onPassPhraseChange: function(e) {
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
},
}
_onPassPhraseKeyPress: function(e) {
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
},
}
_onRecoveryKeyKeyPress: function(e) {
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
},
}
_loadBackupStatus: async function() {
async _loadBackupStatus() {
this.setState({
loading: true,
loadError: null,
@ -167,9 +167,9 @@ export default createReactClass({
loading: false,
});
}
},
}
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner");
@ -296,7 +296,7 @@ export default createReactClass({
content = <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup " +
"<b>Warning</b>: You should only set up key backup " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
@ -322,7 +322,7 @@ export default createReactClass({
/>
</div>
{_t(
"If you've forgotten your recovery passphrase you can "+
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>"
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
@ -345,5 +345,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,265 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { Key } from "../../../../Keyboard";
/*
* Access Secure Secret Storage by requesting the user's passphrase.
*/
export default class AccessSecretStorageDialog extends React.PureComponent {
static propTypes = {
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
recoveryKey: "",
recoveryKeyValid: false,
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
};
}
_onCancel = () => {
this.props.onFinished(false);
}
_onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
throw new Error("Resetting secret storage unimplemented");
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
keyMatches: null,
});
}
_onPassPhraseNext = async () => {
this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase };
const keyMatches = await this.props.checkPrivateKey(input);
if (keyMatches) {
this.props.onFinished(input);
} else {
this.setState({ keyMatches });
}
}
_onRecoveryKeyNext = async () => {
this.setState({ keyMatches: null });
const input = { recoveryKey: this.state.recoveryKey };
const keyMatches = await this.props.checkPrivateKey(input);
if (keyMatches) {
this.props.onFinished(input);
} else {
this.setState({ keyMatches });
}
}
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
keyMatches: null,
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const hasPassphrase = (
this.props.keyInfo &&
this.props.keyInfo.passphrase &&
this.props.keyInfo.passphrase.salt &&
this.props.keyInfo.passphrase.iterations
);
let content;
let title;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
let keyStatus;
if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct passphrase.",
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
}
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your passphrase.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
/>
</div>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct recovery key.",
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
</div>;
}
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your recovery key.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
return (
<BaseDialog className='mx_AccessSecretStorageDialog'
onFinished={this.props.onFinished}
title={title}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,160 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
super(props);
this._unmounted = false;
this.state = {
error: null,
...this._getUpdatedStatus(),
};
}
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("accountData", this.onAccountData);
}
componentWillUnmount() {
this._unmounted = true;
const cli = MatrixClientPeg.get();
if (!cli) return;
cli.removeListener("accountData", this.onAccountData);
}
onAccountData = (event) => {
const type = event.getType();
if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) {
this.setState(this._getUpdatedStatus());
}
};
_getUpdatedStatus() {
// XXX: Add public accessors if we keep this around in production
const cli = MatrixClientPeg.get();
const crossSigning = cli._crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = crossSigning.isStoredInSecretStorage(secretStorage);
const secretStorageKeyInAccount = secretStorage.hasKey();
return {
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
secretStorageKeyInAccount,
};
}
/**
* 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.
*/
_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");
}
},
});
}
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
}
if (this._unmounted) return;
this.setState(this._getUpdatedStatus());
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
error,
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
secretStorageKeyInAccount,
} = this.state;
let errorSection;
if (error) {
errorSection = <div className="error">{error.toString()}</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>
{errorSection}
</div>
);
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,13 +26,6 @@ export default class KeyBackupPanel extends React.PureComponent {
constructor(props) {
super(props);
this._startNewBackup = this._startNewBackup.bind(this);
this._deleteBackup = this._deleteBackup.bind(this);
this._onKeyBackupSessionsRemaining =
this._onKeyBackupSessionsRemaining.bind(this);
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
this._restoreBackup = this._restoreBackup.bind(this);
this._unmounted = false;
this.state = {
loading: true,
@ -63,13 +57,13 @@ export default class KeyBackupPanel extends React.PureComponent {
}
}
_onKeyBackupSessionsRemaining(sessionsRemaining) {
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
this.setState({
sessionsRemaining,
});
}
_onKeyBackupStatus() {
_onKeyBackupStatus = () => {
// This just loads the current backup status rather than forcing
// a re-check otherwise we risk causing infinite loops
this._loadBackupStatus();
@ -120,7 +114,7 @@ export default class KeyBackupPanel extends React.PureComponent {
}
}
_startNewBackup() {
_startNewBackup = () => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
@ -131,7 +125,7 @@ export default class KeyBackupPanel extends React.PureComponent {
);
}
_deleteBackup() {
_deleteBackup = () => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
title: _t('Delete Backup'),
@ -151,7 +145,7 @@ export default class KeyBackupPanel extends React.PureComponent {
});
}
_restoreBackup() {
_restoreBackup = () => {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
});
@ -295,14 +289,14 @@ export default class KeyBackupPanel extends React.PureComponent {
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</details>
<p>
<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>
</p>
</div>
</div>;
} else {
return <div>
@ -314,9 +308,11 @@ export default class KeyBackupPanel extends React.PureComponent {
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
{ _t("Start using Key Backup") }
</AccessibleButton>
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
{_t("Start using Key Backup")}
</AccessibleButton>
</div>
</div>;
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -252,6 +252,23 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
);
// XXX: There's no such panel in the current cross-signing designs, but
// it's useful to have for testing the feature. If there's no interest
// in having advanced details here once all flows are implemented, we
// can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
let crossSigning;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
@ -263,6 +280,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
</div>
{keyBackup}
{crossSigning}
{this._renderCurrentDeviceInfo()}
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>

View file

@ -495,6 +495,15 @@
"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",
"Cross-signing private keys:": "Cross-signing private keys:",
"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",
@ -696,6 +705,7 @@
"Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Key backup": "Key backup",
"Cross-signing": "Cross-signing",
"Security & Privacy": "Security & Privacy",
"Devices": "Devices",
"A device's public name is visible to people you communicate with": "A device's public name is visible to people you communicate with",
@ -1514,6 +1524,17 @@
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Enter secret storage passphrase": "Enter secret storage passphrase",
"Unable to access secret storage. Please verify that you entered the correct passphrase.": "Unable to access secret storage. Please verify that you entered the correct passphrase.",
"<b>Warning</b>: You should only access secret storage from a trusted computer.": "<b>Warning</b>: You should only access secret storage from a trusted computer.",
"Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.",
"If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.",
"Enter secret storage recovery key": "Enter secret storage recovery key",
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.",
"Not a valid recovery key": "Not a valid recovery key",
"Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "If you've forgotten your recovery key you can <button>set up new recovery options</button>.",
"Unable to load backup status": "Unable to load backup status",
"Recovery Key Mismatch": "Recovery Key Mismatch",
"Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
@ -1529,10 +1550,9 @@
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
"Enter Recovery Key": "Enter Recovery Key",
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Not a valid recovery key": "Not a valid recovery key",
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
"If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>": "If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
"Private Chat": "Private Chat",
"Public Chat": "Public Chat",
"Custom": "Custom",
@ -1884,39 +1904,50 @@
"File to import": "File to import",
"Import": "Import",
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
"<b>Warning</b>: You should only set up secret storage from a trusted computer.": "<b>Warning</b>: You should only set up secret storage from a trusted computer.",
"We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Enter a passphrase...": "Enter a passphrase...",
"Set up with a Recovery Key": "Set up with a Recovery Key",
"Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!",
"That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.",
"Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
"Repeat your passphrase...": "Repeat your passphrase...",
"As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
"As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
"As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.": "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.",
"As a safety net, you can use it to restore your access to encrypted messages.": "As a safety net, you can use it to restore your access to encrypted messages.",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
"Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Keep your recovery key somewhere very secure, like a password manager (or a safe)",
"Keep your recovery key somewhere very secure, like a password manager (or a safe).": "Keep your recovery key somewhere very secure, like a password manager (or a safe).",
"Your Recovery Key": "Your Recovery Key",
"Copy to clipboard": "Copy to clipboard",
"Download": "Download",
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.",
"Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.",
"Set up secret storage": "Set up secret storage",
"Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase",
"Confirm your passphrase": "Confirm your passphrase",
"Recovery key": "Recovery key",
"Keep it safe": "Keep it safe",
"Storing secrets...": "Storing secrets...",
"Success!": "Success!",
"Unable to set up secret storage": "Unable to set up secret storage",
"Retry": "Retry",
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
"Set up with a Recovery Key": "Set up with a Recovery Key",
"As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
"As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a passphrase": "Secure your backup with a passphrase",
"Confirm your passphrase": "Confirm your passphrase",
"Recovery key": "Recovery key",
"Keep it safe": "Keep it safe",
"Starting backup...": "Starting backup...",
"Success!": "Success!",
"Create Key Backup": "Create Key Backup",
"Unable to create key backup": "Unable to create key backup",
"Retry": "Retry",
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
"Set up": "Set up",