From 4ff847c8a32fcefef52a0e872704e6f4f9caeba3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 15:57:04 +0100 Subject: [PATCH 1/6] put CompleteSecurity state management in store and split off a child component (SetupCrossSigningBody) that can be reused from the "Verify this session" toast. --- .../structures/auth/CompleteSecurity.js | 235 ++---------------- .../structures/auth/SetupEncryptionBody.js | 196 +++++++++++++++ src/stores/SetupEncryptionStore.js | 147 +++++++++++ 3 files changed, 366 insertions(+), 212 deletions(-) create mode 100644 src/components/structures/auth/SetupEncryptionBody.js create mode 100644 src/stores/SetupEncryptionStore.js diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 3154564cd3..06cece0af2 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -18,13 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager'; - -const PHASE_INTRO = 0; -const PHASE_BUSY = 1; -const PHASE_DONE = 2; -const PHASE_CONFIRM_SKIP = 3; +import { + SetupEncryptionStore, + PHASE_INTRO, + PHASE_BUSY, + PHASE_DONE, + PHASE_CONFIRM_SKIP, +} from '../../../stores/SetupEncryptionStore'; +import SetupEncryptionBody from "./SetupEncryptionBody"; export default class CompleteSecurity extends React.Component { static propTypes = { @@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component { constructor() { super(); - - this.state = { - phase: PHASE_INTRO, - // this serves dual purpose as the object for the request logic and - // the presence of it insidicating that we're in 'verify mode'. - // Because of the latter, it lives in the state. - verificationRequest: null, - backupInfo: null, - }; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this._onStoreUpdate); + store.start(); + this.state = {phase: store.phase}; } + _onStoreUpdate = () => { + const store = SetupEncryptionStore.sharedInstance(); + this.setState({phase: store.phase}); + }; + componentWillUnmount() { - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - } - } - - _onUsePassphraseClick = async () => { - this.setState({ - phase: PHASE_BUSY, - }); - const cli = MatrixClientPeg.get(); - try { - const backupInfo = await cli.getKeyBackupVersion(); - this.setState({backupInfo}); - - // The control flow is fairly twisted here... - // For the purposes of completing security, we only wait on getting - // as far as the trust check and then show a green shield. - // We also begin the key backup restore as well, which we're - // awaiting inside `accessSecretStorage` only so that it keeps your - // passphase cached for that work. This dialog itself will only wait - // on the first trust check, and the key backup restore will happen - // in the background. - await new Promise((resolve, reject) => { - try { - accessSecretStorage(async () => { - await cli.checkOwnCrossSigningTrust(); - resolve(); - if (backupInfo) { - // A complete restore can take many minutes for large - // accounts / slow servers, so we allow the dialog - // to advance before this. - await cli.restoreKeyBackupWithSecretStorage(backupInfo); - } - }); - } catch (e) { - console.error(e); - reject(e); - } - }); - - if (cli.getCrossSigningId()) { - this.setState({ - phase: PHASE_DONE, - }); - } - } catch (e) { - if (!(e instanceof AccessCancelledError)) { - console.log(e); - } - // this will throw if the user hits cancel, so ignore - this.setState({ - phase: PHASE_INTRO, - }); - } - } - - onVerificationRequest = async (request) => { - if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; - - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - await request.accept(); - request.on("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: request, - }); - } - - onVerificationRequestChange = () => { - if (this.state.verificationRequest.cancelled) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: null, - }); - } - } - - onSkipClick = () => { - this.setState({ - phase: PHASE_CONFIRM_SKIP, - }); - } - - onSkipConfirmClick = () => { - this.props.onFinished(); - } - - onSkipBackClick = () => { - this.setState({ - phase: PHASE_INTRO, - }); - } - - onDoneClick = () => { - this.props.onFinished(); + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); } render() { const AuthPage = sdk.getComponent("auth.AuthPage"); const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - - const { - phase, - } = this.state; - + const {phase} = this.state; let icon; let title; - let body; - - if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); - body = ; - } else if (phase === PHASE_INTRO) { - const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + if (phase === PHASE_INTRO) { icon = ; title = _t("Complete security"); - body = ( -
-

{_t( - "Open an existing session & use it to verify this one, " + - "granting it access to encrypted messages.", - )}

-

{_t("Waiting…")}

-

{_t( - "If you can’t access one, ", - {}, { - button: sub => - {sub} - , - })}

-
- - {_t("Skip")} - -
-
- ); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); - let message; - if (this.state.backupInfo) { - message =

{_t( - "Your new session is now verified. It has access to your " + - "encrypted messages, and other users will see it as trusted.", - )}

; - } else { - message =

{_t( - "Your new session is now verified. Other users will see it as trusted.", - )}

; - } - body = ( -
-
- {message} -
- - {_t("Done")} - -
-
- ); } else if (phase === PHASE_CONFIRM_SKIP) { icon = ; title = _t("Are you sure?"); - body = ( -
-

{_t( - "Without completing security on this session, it won’t have " + - "access to encrypted messages.", - )}

-
- - {_t("Skip")} - - - {_t("Go Back")} - -
-
- ); } else if (phase === PHASE_BUSY) { - const Spinner = sdk.getComponent('views.elements.Spinner'); icon = ; title = _t("Complete security"); - body = ; } else { throw new Error(`Unknown phase ${phase}`); } @@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component { {title}
- {body} +
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js new file mode 100644 index 0000000000..a59fa08b32 --- /dev/null +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -0,0 +1,196 @@ +/* +Copyright 2020 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 { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; +import { + SetupEncryptionStore, + PHASE_INTRO, + PHASE_BUSY, + PHASE_DONE, + PHASE_CONFIRM_SKIP, + PHASE_FINISHED, +} from '../../../stores/SetupEncryptionStore'; + +export default class SetupEncryptionBody extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this._onStoreUpdate); + store.start(); + this.state = { + phase: store.phase, + // this serves dual purpose as the object for the request logic and + // the presence of it insidicating that we're in 'verify mode'. + // Because of the latter, it lives in the state. + verificationRequest: store.verificationRequest, + backupInfo: store.backupInfo, + }; + } + + _onStoreUpdate = () => { + const store = SetupEncryptionStore.sharedInstance(); + if (store.phase === PHASE_FINISHED) { + this.props.onFinished(); + return; + } + this.setState({ + phase: store.phase, + verificationRequest: store.verificationRequest, + backupInfo: store.backupInfo, + }); + }; + + componentWillUnmount() { + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); + } + + _onUsePassphraseClick = async () => { + const store = SetupEncryptionStore.sharedInstance(); + store.usePassPhrase(); + } + + onSkipClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skip(); + } + + onSkipConfirmClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skipConfirm(); + } + + onSkipBackClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.returnAfterSkip(); + } + + onDoneClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.done(); + } + + render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const { + phase, + } = this.state; + + if (this.state.verificationRequest) { + const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); + return ; + } else if (phase === PHASE_INTRO) { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + return ( +
+

{_t( + "Open an existing session & use it to verify this one, " + + "granting it access to encrypted messages.", + )}

+

{_t("Waiting…")}

+

{_t( + "If you can’t access one, ", + {}, { + button: sub => + {sub} + , + })}

+
+ + {_t("Skip")} + +
+
+ ); + } else if (phase === PHASE_DONE) { + let message; + if (this.state.backupInfo) { + message =

{_t( + "Your new session is now verified. It has access to your " + + "encrypted messages, and other users will see it as trusted.", + )}

; + } else { + message =

{_t( + "Your new session is now verified. Other users will see it as trusted.", + )}

; + } + return ( +
+
+ {message} +
+ + {_t("Done")} + +
+
+ ); + } else if (phase === PHASE_CONFIRM_SKIP) { + return ( +
+

{_t( + "Without completing security on this session, it won’t have " + + "access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Go Back")} + +
+
+ ); + } else if (phase === PHASE_BUSY) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return ; + } else { + throw new Error(`Unknown phase ${phase}`); + } + } +} diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js new file mode 100644 index 0000000000..93c1770b1f --- /dev/null +++ b/src/stores/SetupEncryptionStore.js @@ -0,0 +1,147 @@ +/* +Copyright 2020 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 EventEmitter from 'events'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManager'; + +export const PHASE_INTRO = 0; +export const PHASE_BUSY = 1; +export const PHASE_DONE = 2; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 3; +export const PHASE_FINISHED = 4; //UX can be closed + +/** + * Holds the active "Complete Security" session + */ +export class SetupEncryptionStore extends EventEmitter { + static sharedInstance() { + if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); + return global.mx_SetupEncryptionStore; + } + + start() { + if (this._started) { + return; + } + this._started = true; + this.phase = PHASE_INTRO; + this.verificationRequest = null; + this.backupInfo = null; + MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); + } + + stop() { + if (!this._started) { + return; + } + this._started = false; + if (this.verificationRequest) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + } + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); + } + } + + async usePassPhrase() { + this.phase = PHASE_BUSY; + this.emit("update"); + const cli = MatrixClientPeg.get(); + try { + const backupInfo = await cli.getKeyBackupVersion(); + this.backupInfo = backupInfo; + this.emit("update"); + // The control flow is fairly twisted here... + // For the purposes of completing security, we only wait on getting + // as far as the trust check and then show a green shield. + // We also begin the key backup restore as well, which we're + // awaiting inside `accessSecretStorage` only so that it keeps your + // passphase cached for that work. This dialog itself will only wait + // on the first trust check, and the key backup restore will happen + // in the background. + await new Promise((resolve, reject) => { + try { + accessSecretStorage(async () => { + await cli.checkOwnCrossSigningTrust(); + resolve(); + if (backupInfo) { + // A complete restore can take many minutes for large + // accounts / slow servers, so we allow the dialog + // to advance before this. + await cli.restoreKeyBackupWithSecretStorage(backupInfo); + } + }).catch(reject); + } catch (e) { + console.error(e); + reject(e); + } + }); + + if (cli.getCrossSigningId()) { + this.phase = PHASE_DONE; + this.emit("update"); + } + } catch (e) { + if (!(e instanceof AccessCancelledError)) { + console.log(e); + } + // this will throw if the user hits cancel, so ignore + this.phase = PHASE_INTRO; + this.emit("update"); + } + } + + onVerificationRequest = async (request) => { + if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; + + if (this.verificationRequest) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + } + this.verificationRequest = request; + await request.accept(); + request.on("change", this.onVerificationRequestChange); + this.emit("update"); + } + + onVerificationRequestChange = () => { + if (this.verificationRequest.cancelled) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + this.verificationRequest = null; + this.emit("update"); + } + } + + skip() { + this.phase = PHASE_CONFIRM_SKIP; + this.emit("update"); + } + + skipConfirm() { + this.phase = PHASE_FINISHED; + this.emit("update"); + } + + returnAfterSkip() { + this.phase = PHASE_INTRO; + this.emit("update"); + } + + done() { + this.phase = PHASE_FINISHED; + this.emit("update"); + } +} From 3e59127d12a83e55def0f7003c39c7e82cc33a2f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 16:10:43 +0100 Subject: [PATCH 2/6] use SetupEncryptionBody to show a dialog from "Verify this session" --- .../views/dialogs/SetupEncryptionDialog.js | 29 +++++++++++++++++++ .../views/toasts/SetupEncryptionToast.js | 9 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/components/views/dialogs/SetupEncryptionDialog.js diff --git a/src/components/views/dialogs/SetupEncryptionDialog.js b/src/components/views/dialogs/SetupEncryptionDialog.js new file mode 100644 index 0000000000..f32a289a29 --- /dev/null +++ b/src/components/views/dialogs/SetupEncryptionDialog.js @@ -0,0 +1,29 @@ +/* +Copyright 2020 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 SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; + +export default function SetupEncryptionDialog({onFinished}) { + return + + ; +} diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js index 9016e4c6d7..ad6488a9bb 100644 --- a/src/components/views/toasts/SetupEncryptionToast.js +++ b/src/components/views/toasts/SetupEncryptionToast.js @@ -18,7 +18,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; import DeviceListener from '../../../DeviceListener'; +import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog"; import { accessSecretStorage } from '../../../CrossSigningManager'; export default class SetupEncryptionToast extends React.PureComponent { @@ -32,7 +34,12 @@ export default class SetupEncryptionToast extends React.PureComponent { }; _onSetupClick = async () => { - accessSecretStorage(); + if (this.props.kind === "verify_this_session") { + Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog, + {}, null, /* priority = */ false, /* static = */ true); + } else { + accessSecretStorage(); + } }; getDescription() { From 7cded53cdb118c3fee1bc21e60148d60a88f0c74 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 17:02:36 +0100 Subject: [PATCH 3/6] fix i18n --- src/i18n/strings/en_EN.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 57b39309b0..9dcea47cc8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2006,14 +2006,7 @@ "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Could not load user profile": "Could not load user profile", "Complete security": "Complete security", - "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.", - "Waiting…": "Waiting…", - "If you can’t access one, ": "If you can’t access one, ", "Session verified": "Session verified", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", - "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", - "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", - "Go Back": "Go Back", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", @@ -2063,6 +2056,13 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", + "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.", + "Waiting…": "Waiting…", + "If you can’t access one, ": "If you can’t access one, ", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", + "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", + "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", + "Go Back": "Go Back", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Failed to re-authenticate": "Failed to re-authenticate", "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.", From 7d4e4982575d9bc97f5ba8a2eb47a927f36ecf4f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 17:03:40 +0100 Subject: [PATCH 4/6] fix lint --- src/stores/SetupEncryptionStore.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 93c1770b1f..7b42e1552d 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -24,9 +24,6 @@ export const PHASE_DONE = 2; //final done stage, but still showing UX export const PHASE_CONFIRM_SKIP = 3; export const PHASE_FINISHED = 4; //UX can be closed -/** - * Holds the active "Complete Security" session - */ export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); From 792a7b395394f5bffe7d0009b4d9089509a7d03e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 13:07:07 +0100 Subject: [PATCH 5/6] don't throw here --- src/components/structures/auth/SetupEncryptionBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index a59fa08b32..e8c15bd1af 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -190,7 +190,7 @@ export default class SetupEncryptionBody extends React.Component { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { - throw new Error(`Unknown phase ${phase}`); + console.log(`SetupEncryptionBody: Unknown phase ${phase}`); } } } From 78b167a7ea4f6a055c248c1d3e24c01f71345108 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 13:40:09 +0100 Subject: [PATCH 6/6] fix typo --- src/components/structures/auth/SetupEncryptionBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index e8c15bd1af..c7c73cd616 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -41,7 +41,7 @@ export default class SetupEncryptionBody extends React.Component { this.state = { phase: store.phase, // this serves dual purpose as the object for the request logic and - // the presence of it insidicating that we're in 'verify mode'. + // the presence of it indicating that we're in 'verify mode'. // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo,