diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -25,13 +25,15 @@ +pickledAccount: string, }; -export type CryptoStore = { - +primaryAccount: ?PickledOLMAccount, - +primaryIdentityKeys: ?OLMIdentityKeys, - +notificationAccount: ?PickledOLMAccount, - +notificationIdentityKeys: ?OLMIdentityKeys, +export type CompleteCryptoStore = { + +primaryAccount: PickledOLMAccount, + +primaryIdentityKeys: OLMIdentityKeys, + +notificationAccount: PickledOLMAccount, + +notificationIdentityKeys: OLMIdentityKeys, }; +export type CryptoStore = $ObjMap(T) => ?T>; + export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,35 +1,186 @@ // @flow +import olm from '@commapp/olm'; +import invariant from 'invariant'; import * as React from 'react'; +import { useDispatch } from 'react-redux'; +import uuid from 'uuid'; -import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; +import type { + SignedIdentityKeysBlob, + CompleteCryptoStore, + IdentityKeysBlob, +} from 'lib/types/crypto-types.js'; +import { initOlm } from '../olm/olm-utils.js'; +import { + setPrimaryIdentityKeys, + setNotificationIdentityKeys, + setPickledPrimaryAccount, + setPickledNotificationAccount, +} from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getSignedIdentityKeysBlobSelector } from '../selectors/socket-selectors.js'; - -function useSignedIdentityKeysBlob(): ?SignedIdentityKeysBlob { - const getSignedIdentityKeysBlob: ?() => Promise = - useSelector(getSignedIdentityKeysBlobSelector); - - const [signedIdentityKeysBlob, setSignedIdentityKeysBlob] = - React.useState(null); - - React.useEffect(() => { - (async () => { - if ( - getSignedIdentityKeysBlob === null || - getSignedIdentityKeysBlob === undefined - ) { - setSignedIdentityKeysBlob(null); - return; - } - const resolvedSignedIdentityKeysBlob: SignedIdentityKeysBlob = - await getSignedIdentityKeysBlob(); - setSignedIdentityKeysBlob(resolvedSignedIdentityKeysBlob); + +const GetOrCreateCryptoStoreContext: React.Context Promise> = + React.createContext(null); + +type Props = { + +children: React.Node, +}; + +function GetOrCreateCryptoStoreProvider(props: Props): React.Node { + const dispatch = useDispatch(); + const currentCryptoStore = useSelector(state => state.cryptoStore); + const createCryptoStorePromiseRef = + React.useRef>(null); + + const value = React.useCallback(async () => { + if ( + currentCryptoStore.primaryAccount && + currentCryptoStore.primaryIdentityKeys && + currentCryptoStore.notificationAccount && + currentCryptoStore.notificationIdentityKeys + ) { + return { + primaryAccount: currentCryptoStore.primaryAccount, + primaryIdentityKeys: currentCryptoStore.primaryIdentityKeys, + notificationAccount: currentCryptoStore.notificationAccount, + notificationIdentityKeys: currentCryptoStore.notificationIdentityKeys, + }; + } + + const currentCreateCryptoStorePromiseRef = + createCryptoStorePromiseRef.current; + + if (currentCreateCryptoStorePromiseRef) { + return currentCreateCryptoStorePromiseRef; + } + + const newCreateCryptoStorePromise = (async () => { + await initOlm(); + + const identityAccount = new olm.Account(); + identityAccount.create(); + const { ed25519: identityED25519, curve25519: identityCurve25519 } = + JSON.parse(identityAccount.identity_keys()); + + dispatch({ + type: setPrimaryIdentityKeys, + payload: { ed25519: identityED25519, curve25519: identityCurve25519 }, + }); + + const identityAccountPicklingKey = uuid.v4(); + const pickledIdentityAccount = identityAccount.pickle( + identityAccountPicklingKey, + ); + + dispatch({ + type: setPickledPrimaryAccount, + payload: { + picklingKey: identityAccountPicklingKey, + pickledAccount: pickledIdentityAccount, + }, + }); + + const notificationAccount = new olm.Account(); + notificationAccount.create(); + const { + ed25519: notificationED25519, + curve25519: notificationCurve25519, + } = JSON.parse(notificationAccount.identity_keys()); + + dispatch({ + type: setNotificationIdentityKeys, + payload: { + ed25519: notificationED25519, + curve25519: notificationCurve25519, + }, + }); + + const notificationAccountPicklingKey = uuid.v4(); + const pickledNotificationAccount = notificationAccount.pickle( + notificationAccountPicklingKey, + ); + + dispatch({ + type: setPickledNotificationAccount, + payload: { + picklingKey: notificationAccountPicklingKey, + pickledAccount: pickledNotificationAccount, + }, + }); + + createCryptoStorePromiseRef.current = null; + + return { + primaryAccount: { + picklingKey: identityAccountPicklingKey, + pickledAccount: pickledIdentityAccount, + }, + primaryIdentityKeys: { + ed25519: identityED25519, + curve25519: identityCurve25519, + }, + notificationAccount: { + picklingKey: notificationAccountPicklingKey, + pickledAccount: pickledNotificationAccount, + }, + notificationIdentityKeys: { + ed25519: notificationED25519, + curve25519: notificationCurve25519, + }, + }; })(); - }, [getSignedIdentityKeysBlob]); - return signedIdentityKeysBlob; + createCryptoStorePromiseRef.current = newCreateCryptoStorePromise; + return newCreateCryptoStorePromise; + }, [dispatch, currentCryptoStore]); + + return ( + + {props.children} + + ); +} + +function useGetOrCreateCryptoStore(): () => Promise { + const context = React.useContext(GetOrCreateCryptoStoreContext); + invariant(context, 'GetOrCreateCryptoStoreContext not found'); + + return context; +} + +function useGetSignedIdentityKeysBlob(): () => Promise { + const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); + + return React.useCallback(async () => { + const { primaryAccount, primaryIdentityKeys, notificationIdentityKeys } = + await getOrCreateCryptoStore(); + + await initOlm(); + const primaryOLMAccount = new olm.Account(); + primaryOLMAccount.unpickle( + primaryAccount.picklingKey, + primaryAccount.pickledAccount, + ); + + const identityKeysBlob: IdentityKeysBlob = { + primaryIdentityPublicKeys: primaryIdentityKeys, + notificationIdentityPublicKeys: notificationIdentityKeys, + }; + + const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: payloadToBeSigned, + signature: primaryOLMAccount.sign(payloadToBeSigned), + }; + + return signedIdentityKeysBlob; + }, [getOrCreateCryptoStore]); } -export { useSignedIdentityKeysBlob }; +export { + useGetSignedIdentityKeysBlob, + useGetOrCreateCryptoStore, + GetOrCreateCryptoStoreProvider, +}; diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -1,106 +1,33 @@ // @flow -import olm from '@commapp/olm'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import * as React from 'react'; import { useDispatch } from 'react-redux'; -import uuid from 'uuid'; import { useWalletClient } from 'wagmi'; import { isDev } from 'lib/utils/dev-utils.js'; +import { useGetOrCreateCryptoStore } from './account-hooks.js'; import css from './log-in-form.css'; import SIWEButton from './siwe-button.react.js'; import SIWELoginForm from './siwe-login-form.react.js'; import TraditionalLoginForm from './traditional-login-form.react.js'; import Button from '../components/button.react.js'; import OrBreak from '../components/or-break.react.js'; -import { initOlm } from '../olm/olm-utils.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; -import { - setPrimaryIdentityKeys, - setNotificationIdentityKeys, - setPickledPrimaryAccount, - setPickledNotificationAccount, -} from '../redux/crypto-store-reducer.js'; -import { useSelector } from '../redux/redux-utils.js'; function LoginForm(): React.Node { const { openConnectModal } = useConnectModal(); const { data: signer } = useWalletClient(); const dispatch = useDispatch(); - const primaryIdentityPublicKeys = useSelector( - state => state.cryptoStore.primaryIdentityKeys, - ); - const notificationIdentityPublicKeys = useSelector( - state => state.cryptoStore.notificationIdentityKeys, - ); + const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); React.useEffect(() => { (async () => { - if ( - primaryIdentityPublicKeys !== null && - primaryIdentityPublicKeys !== undefined && - notificationIdentityPublicKeys !== null && - notificationIdentityPublicKeys !== undefined - ) { - return; - } - await initOlm(); - - const identityAccount = new olm.Account(); - identityAccount.create(); - const { ed25519: identityED25519, curve25519: identityCurve25519 } = - JSON.parse(identityAccount.identity_keys()); - - dispatch({ - type: setPrimaryIdentityKeys, - payload: { ed25519: identityED25519, curve25519: identityCurve25519 }, - }); - - const identityAccountPicklingKey = uuid.v4(); - const pickledIdentityAccount = identityAccount.pickle( - identityAccountPicklingKey, - ); - - dispatch({ - type: setPickledPrimaryAccount, - payload: { - picklingKey: identityAccountPicklingKey, - pickledAccount: pickledIdentityAccount, - }, - }); - - const notificationAccount = new olm.Account(); - notificationAccount.create(); - const { - ed25519: notificationED25519, - curve25519: notificationCurve25519, - } = JSON.parse(notificationAccount.identity_keys()); - - dispatch({ - type: setNotificationIdentityKeys, - payload: { - ed25519: notificationED25519, - curve25519: notificationCurve25519, - }, - }); - - const notificationAccountPicklingKey = uuid.v4(); - const pickledNotificationAccount = notificationAccount.pickle( - notificationAccountPicklingKey, - ); - - dispatch({ - type: setPickledNotificationAccount, - payload: { - picklingKey: notificationAccountPicklingKey, - pickledAccount: pickledNotificationAccount, - }, - }); + await getOrCreateCryptoStore(); })(); - }, [dispatch, notificationIdentityPublicKeys, primaryIdentityPublicKeys]); + }, [getOrCreateCryptoStore]); const onQRCodeLoginButtonClick = React.useCallback(() => { dispatch({ diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -19,10 +19,7 @@ import stores from 'lib/facts/stores.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; -import type { - OLMIdentityKeys, - SignedIdentityKeysBlob, -} from 'lib/types/crypto-types.js'; +import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; import { useDispatchActionPromise, useServerCall, @@ -34,7 +31,7 @@ siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.js'; -import { useSignedIdentityKeysBlob } from './account-hooks.js'; +import { useGetSignedIdentityKeysBlob } from './account-hooks.js'; import HeaderSeparator from './header-separator.react.js'; import css from './siwe.css'; import Button from '../components/button.react.js'; @@ -88,11 +85,11 @@ state => state.cryptoStore.primaryIdentityKeys, ); - const signedIdentityKeysBlob: ?SignedIdentityKeysBlob = - useSignedIdentityKeysBlob(); + const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const callSIWEAuthEndpoint = React.useCallback( async (message: string, signature: string, extraInfo) => { + const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); invariant( signedIdentityKeysBlob, 'signedIdentityKeysBlob must be set in attemptSIWEAuth', @@ -115,7 +112,7 @@ throw e; } }, - [signedIdentityKeysBlob, siweAuthCall], + [getSignedIdentityKeysBlob, siweAuthCall], ); const attemptSIWEAuth = React.useCallback( @@ -186,8 +183,7 @@ if ( siweAuthLoadingStatus === 'loading' || !siweNonce || - !primaryIdentityPublicKeys || - !signedIdentityKeysBlob + !primaryIdentityPublicKeys ) { return (
diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -15,13 +15,12 @@ LogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; -import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; -import { useSignedIdentityKeysBlob } from './account-hooks.js'; +import { useGetSignedIdentityKeysBlob } from './account-hooks.js'; import HeaderSeparator from './header-separator.react.js'; import css from './log-in-form.css'; import PasswordInput from './password-input.react.js'; @@ -39,8 +38,7 @@ const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); - const signedIdentityKeysBlob: ?SignedIdentityKeysBlob = - useSignedIdentityKeysBlob(); + const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const usernameInputRef = React.useRef(); React.useEffect(() => { @@ -67,6 +65,7 @@ const logInAction = React.useCallback( async (extraInfo: LogInExtraInfo) => { + const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); try { invariant( signedIdentityKeysBlob, @@ -94,7 +93,7 @@ throw e; } }, - [callLogIn, modalContext, password, signedIdentityKeysBlob, username], + [callLogIn, modalContext, password, getSignedIdentityKeysBlob, username], ); const onSubmit = React.useCallback( @@ -174,11 +173,7 @@