diff --git a/res/css/_components.scss b/res/css/_components.scss index 4081e49630..529ce9ac85 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss index 415a2021cc..9cba8e0da9 100644 --- a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss @@ -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; diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss new file mode 100644 index 0000000000..db11e91bdb --- /dev/null +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -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; +} + diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss new file mode 100644 index 0000000000..757d8028f0 --- /dev/null +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -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; +} diff --git a/res/css/views/dialogs/_RestoreKeyBackupDialog.scss b/res/css/views/settings/_CrossSigningPanel.scss similarity index 66% rename from res/css/views/dialogs/_RestoreKeyBackupDialog.scss rename to res/css/views/settings/_CrossSigningPanel.scss index 69e00c416a..fa9f76a963 100644 --- a/res/css/views/dialogs/_RestoreKeyBackupDialog.scss +++ b/res/css/views/settings/_CrossSigningPanel.scss @@ -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; } diff --git a/res/css/views/settings/_KeyBackupPanel.scss b/res/css/views/settings/_KeyBackupPanel.scss index 1bcc0ab10d..872162caad 100644 --- a/res/css/views/settings/_KeyBackupPanel.scss +++ b/res/css/views/settings/_KeyBackupPanel.scss @@ -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; +} diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js new file mode 100644 index 0000000000..5dc709bd10 --- /dev/null +++ b/src/CrossSigningManager.js @@ -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]; +}; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ef0130ec15..a3a0588bfc 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -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); diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 4953cdff68..eae102196f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -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

{_t( - "Warning: you should only set up key backup from a trusted computer.", {}, + "Warning: You should only set up key backup from a trusted computer.", {}, { b: sub => {sub} }, )}

{_t( @@ -305,9 +307,9 @@ export default createReactClass({

; - }, + } - _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} /> ; - }, + } - _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.", )}

{_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).", )}

{bodyText}

@@ -402,18 +404,18 @@ export default createReactClass({
; - }, + } - _renderPhaseKeepItSafe: function() { + _renderPhaseKeepItSafe() { let introText; if (this.state.copied) { introText = _t( - "Your Recovery Key has been copied to your clipboard, paste it to:", + "Your recovery key has been copied to your clipboard, paste it to:", {}, {b: s => {s}}, ); } else if (this.state.downloaded) { introText = _t( - "Your Recovery Key is in your Downloads folder.", + "Your recovery key is in your Downloads folder.", {}, {b: s => {s}}, ); } @@ -431,16 +433,16 @@ export default createReactClass({ ; - }, + } - _renderBusyPhase: function(text) { + _renderBusyPhase(text) { const Spinner = sdk.getComponent('views.elements.Spinner'); return
; - }, + } - _renderPhaseDone: function() { + _renderPhaseDone() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( @@ -451,9 +453,9 @@ export default createReactClass({ hasCancel={false} />

; - }, + } - _renderPhaseOptOutConfirm: function() { + _renderPhaseOptOutConfirm() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
{_t( @@ -467,9 +469,9 @@ export default createReactClass({
; - }, + } - _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({ ); - }, -}); + } +} diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js new file mode 100644 index 0000000000..78ff2a1698 --- /dev/null +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -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(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); + } + const suggestionBlock =
{suggestions.length > 0 ? suggestions : _t("Keep going...")}
; + + helpText =
+ {this.state.zxcvbnResult.feedback.warning} + {suggestionBlock} +
; + } + strengthMeter =
+ +
; + } + + return
+

{_t( + "Warning: You should only set up secret storage from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_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.", + )}

+

{_t("For maximum security, this should be different from your account password.")}

+ +
+
+ +
+ {strengthMeter} + {helpText} +
+
+
+ + + +
+ {_t("Advanced")} +

+
+
; + } + + _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 =
+
{matchText}
+
+ + {_t("Go back to set it again.")} + +
+
; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Please enter your passphrase a second time to confirm.", + )}

+
+
+
+ +
+ {passPhraseMatch} +
+
+ +
; + } + + _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
+

{_t( + "Your recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your passphrase.", + )}

+

{_t( + "Keep your recovery key somewhere very secure, like a password manager (or a safe).", + )}

+

{bodyText}

+
+
+ {_t("Your Recovery Key")} +
+
+
+ {this._encodedRecoveryKey} +
+
+ + +
+
+
+
; + } + + _renderPhaseKeepItSafe() { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {introText} + + + + +
; + } + + _renderBusyPhase(text) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return
+ +
; + } + + _renderPhaseDone() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Your access to encrypted messages is now protected.", + )}

+ +
; + } + + _renderPhaseOptOutConfirm() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {_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.", + )} + + + +
; + } + + _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 =
+

{_t("Unable to set up secret storage")}

+
+ +
+
; + } 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 ( + +
+ {content} +
+
+ ); + } +} diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 300e6b7f18..45168c381e 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -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 =

{_t( - "Warning: you should only set up key backup " + + "Warning: You should only set up key backup " + "from a trusted computer.", {}, { b: sub => {sub} }, )}

@@ -322,7 +322,7 @@ export default createReactClass({ />
{_t( - "If you've forgotten your recovery passphrase you can "+ + "If you've forgotten your recovery key you can "+ "" , {}, { button: s => ); - }, -}); + } +} diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js new file mode 100644 index 0000000000..d116ce505f --- /dev/null +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -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 =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct passphrase.", + )} +
; + } else { + keyStatus =
; + } + + content =
+

{_t( + "Warning: You should only access secret storage " + + "from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_t( + "Access your secure message history and your cross-signing " + + "identity for verifying other devices by entering your passphrase.", + )}

+ +
+ + {keyStatus} + +
+ {_t( + "If you've forgotten your passphrase you can "+ + "use your recovery key or " + + "set up new recovery options." + , {}, { + button1: s => + {s} + , + button2: s => + {s} + , + })} +
; + } 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 =
; + } else if (this.state.recoveryKeyValid) { + keyStatus =
+ {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")} +
; + } else if (this.state.keyMatches === false) { + keyStatus =
+ {"\uD83D\uDC4E "}{_t( + "Unable to access secret storage. Please verify that you " + + "entered the correct recovery key.", + )} +
; + } else { + keyStatus =
+ {"\uD83D\uDC4E "}{_t("Not a valid recovery key")} +
; + } + + content =
+

{_t( + "Warning: You should only access secret storage " + + "from a trusted computer.", {}, + { b: sub => {sub} }, + )}

+

{_t( + "Access your secure message history and your cross-signing " + + "identity for verifying other devices by entering your recovery key.", + )}

+ +
+ + {keyStatus} + +
+ {_t( + "If you've forgotten your recovery key you can "+ + "." + , {}, { + button: s => + {s} + , + })} +
; + } + + return ( + +
+ {content} +
+
+ ); + } +} diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js new file mode 100644 index 0000000000..fda92ebac9 --- /dev/null +++ b/src/components/views/settings/CrossSigningPanel.js @@ -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 =
{error.toString()}
; + } + + return ( +
+ + + + + + + + + + + + + +
{_t("Cross-signing public keys:")}{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}
{_t("Cross-signing private keys:")}{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}
{_t("Secret storage public key:")}{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
+ + {_t("Bootstrap Secure Secret Storage")} + +
+ {errorSection} +
+ ); + } +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index ec1e52a90c..c2fb3dc9db 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -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 {
{backupSigStatuses}
{trustedLocally}
-

+

{restoreButtonCaption}     { _t("Delete Backup") } -

+
; } else { return
@@ -314,9 +308,11 @@ export default class KeyBackupPanel extends React.PureComponent {

{encryptedMessageAreEncrypted}

{_t("Back up your keys before signing out to avoid losing them.")}

- - { _t("Start using Key Backup") } - +
+ + {_t("Start using Key Backup")} + +
; } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 0732bcf926..98ec18df5a 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -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 { ); + // 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 = ( +
+ {_t("Cross-signing")} +
+ +
+
+ ); + } + return (
{_t("Security & Privacy")}
@@ -263,6 +280,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{keyBackup} + {crossSigning} {this._renderCurrentDeviceInfo()}
{_t("Analytics")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d1bd34860d..7cf0aeaf36 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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.", + "Warning: You should only access secret storage from a trusted computer.": "Warning: 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 use your recovery key or set up new recovery options.": "If you've forgotten your passphrase you can use your recovery key or set up new recovery options.", + "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 .": "If you've forgotten your recovery key you can .", "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 use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "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", + "Warning: You should only set up key backup from a trusted computer.": "Warning: 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 ": "If you've forgotten your recovery passphrase you can ", + "If you've forgotten your recovery key you can ": "If you've forgotten your recovery key you can ", "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.", + "Warning: You should only set up secret storage from a trusted computer.": "Warning: 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 copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", - "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", + "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", + "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it 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",