Merge pull request #3640 from matrix-org/jryans/4s-new-key-backup
Add testing flow to bootstrap secret storagepull/21833/head
						commit
						c2cd97fab3
					
				|  | @ -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"; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
| 
 | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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]; | ||||
| }; | ||||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -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>    | ||||
|                     <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>; | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 J. Ryan Stinnett
						J. Ryan Stinnett