diff --git a/lib/components/keyserver-connection-handler.js b/lib/components/keyserver-connection-handler.js index 1115a2011..81b891566 100644 --- a/lib/components/keyserver-connection-handler.js +++ b/lib/components/keyserver-connection-handler.js @@ -1,354 +1,354 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { keyserverAuthActionTypes, logOutActionTypes, useKeyserverAuth, useLogOut, } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { CANCELLED_ERROR, type CallKeyserverEndpoint, } from '../keyserver-conn/keyserver-conn-types.js'; import { useKeyserverRecoveryLogIn } from '../keyserver-conn/recovery-utils.js'; import { filterThreadIDsInFilterList } from '../reducers/calendar-filters-reducer.js'; import { connectionSelector, cookieSelector, deviceTokenSelector, } from '../selectors/keyserver-selectors.js'; import { isLoggedInToKeyserver } from '../selectors/user-selectors.js'; import { useInitialNotificationsEncryptedMessage } from '../shared/crypto-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; -import { OlmSessionCreatorContext } from '../shared/olm-session-creator-context.js'; import type { BaseSocketProps } from '../socket/socket.react.js'; import { logInActionSources, type RecoveryActionSource, } from '../types/account-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import type { CallSingleKeyserverEndpoint } from '../utils/call-single-keyserver-endpoint.js'; import { getConfig } from '../utils/config.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken, relyingOnAuthoritativeKeyserver, } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; type Props = { ...BaseSocketProps, +socketComponent: React.ComponentType, }; const AUTH_RETRY_DELAY_MS = 60000; function KeyserverConnectionHandler(props: Props) { const { socketComponent: Socket, ...socketProps } = props; const { keyserverID } = props; const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const keyserverAuth = useKeyserverAuth(); const hasConnectionIssue = useSelector( state => !!connectionSelector(keyserverID)(state)?.connectionIssue, ); const cookie = useSelector(cookieSelector(keyserverID)); const dataLoaded = useSelector(state => state.dataLoaded); const keyserverDeviceToken = useSelector(deviceTokenSelector(keyserverID)); // We have an assumption that we should be always connected to Ashoat's // keyserver. It is possible that a token which it has is correct, so we can // try to use it. In worst case it is invalid and our push-handler will try // to fix it. const ashoatKeyserverDeviceToken = useSelector( deviceTokenSelector(authoritativeKeyserverID()), ); const deviceToken = keyserverDeviceToken ?? ashoatKeyserverDeviceToken; const navInfo = useSelector(state => state.navInfo); const calendarFilters = useSelector(state => state.calendarFilters); const calendarQuery = React.useMemo(() => { const filters = filterThreadIDsInFilterList( calendarFilters, (threadID: string) => extractKeyserverIDFromID(threadID) === keyserverID, ); return { startDate: navInfo.startDate, endDate: navInfo.endDate, filters, }; }, [calendarFilters, keyserverID, navInfo.endDate, navInfo.startDate]); React.useEffect(() => { if ( hasConnectionIssue && keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { void dispatchActionPromise(logOutActionTypes, callLogOut()); } }, [callLogOut, hasConnectionIssue, dispatchActionPromise, keyserverID]); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; - const olmSessionCreator = React.useContext(OlmSessionCreatorContext); - invariant(olmSessionCreator, 'Olm session creator should be set'); + const { olmAPI } = getConfig(); const [authInProgress, setAuthInProgress] = React.useState(false); const performAuth = React.useCallback(() => { setAuthInProgress(true); let cancelled = false; const cancel = () => { cancelled = true; setAuthInProgress(false); }; const promise = (async () => { try { - const keyserverKeys = - await identityClient.getKeyserverKeys(keyserverID); + const [keyserverKeys] = await Promise.all([ + identityClient.getKeyserverKeys(keyserverID), + olmAPI.initializeCryptoAccount(), + ]); if (cancelled) { throw new Error(CANCELLED_ERROR); } const [notifsSession, contentSession, { userID, deviceID }] = await Promise.all([ - olmSessionCreator.notificationsSessionCreator( + olmAPI.notificationsSessionCreator( cookie, keyserverKeys.identityKeysBlob.notificationIdentityPublicKeys, keyserverKeys.notifInitializationInfo, keyserverID, ), - olmSessionCreator.contentSessionCreator( + olmAPI.contentOutboundSessionCreator( keyserverKeys.identityKeysBlob.primaryIdentityPublicKeys, keyserverKeys.contentInitializationInfo, ), getAuthMetadata(), ]); invariant(userID, 'userID should be set'); invariant(deviceID, 'deviceID should be set'); const deviceTokenUpdateInput = deviceToken ? { [keyserverID]: { deviceToken } } : {}; if (cancelled) { throw new Error(CANCELLED_ERROR); } await dispatchActionPromise( keyserverAuthActionTypes, keyserverAuth({ userID, deviceID, doNotRegister: false, calendarQuery, deviceTokenUpdateInput, authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, keyserverData: { [keyserverID]: { initialContentEncryptedMessage: contentSession, initialNotificationsEncryptedMessage: notifsSession, }, }, }), ); } catch (e) { if (cancelled) { return; } console.log( `Error while authenticating to keyserver with id ${keyserverID}`, e, ); if ( !dataLoaded && keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { await dispatchActionPromise(logOutActionTypes, callLogOut()); } } finally { if (!cancelled) { await sleep(AUTH_RETRY_DELAY_MS); setAuthInProgress(false); } } })(); return [promise, cancel]; }, [ calendarQuery, callLogOut, cookie, dataLoaded, deviceToken, dispatchActionPromise, getAuthMetadata, identityClient, keyserverAuth, keyserverID, - olmSessionCreator, + olmAPI, ]); const activeSessionRecovery = useSelector( state => state.keyserverStore.keyserverInfos[keyserverID]?.connection .activeSessionRecovery, ); // This async function asks the keyserver for its keys, whereas performAuth // above gets the keyserver's keys from the identity service const getInitialNotificationsEncryptedMessageForRecovery = useInitialNotificationsEncryptedMessage(keyserverID); const { resolveKeyserverSessionInvalidationUsingNativeCredentials } = getConfig(); const innerPerformRecovery = React.useCallback( ( recoveryActionSource: RecoveryActionSource, hasBeenCancelled: () => boolean, ) => async ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => { if (!resolveKeyserverSessionInvalidationUsingNativeCredentials) { return; } await resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint, callKeyserverEndpoint, dispatchActionPromise, recoveryActionSource, keyserverID, getInitialNotificationsEncryptedMessageForRecovery, hasBeenCancelled, ); }, [ resolveKeyserverSessionInvalidationUsingNativeCredentials, dispatchActionPromise, keyserverID, getInitialNotificationsEncryptedMessageForRecovery, ], ); const keyserverRecoveryLogIn = useKeyserverRecoveryLogIn(keyserverID); const performRecovery = React.useCallback( (recoveryActionSource: RecoveryActionSource) => { setAuthInProgress(true); let cancelled = false; const cancel = () => { cancelled = true; setAuthInProgress(false); }; const hasBeenCancelled = () => cancelled; const promise = (async () => { try { await keyserverRecoveryLogIn( recoveryActionSource, innerPerformRecovery(recoveryActionSource, hasBeenCancelled), hasBeenCancelled, ); } finally { if (!cancelled) { setAuthInProgress(false); } } })(); return [promise, cancel]; }, [keyserverRecoveryLogIn, innerPerformRecovery], ); const cancelPendingAuth = React.useRef void>(null); const prevPerformAuth = React.useRef(performAuth); const isUserAuthenticated = useSelector(isLoggedInToKeyserver(keyserverID)); const hasAccessToken = useSelector(state => !!state.commServicesAccessToken); const cancelPendingRecovery = React.useRef void>(null); const prevPerformRecovery = React.useRef(performRecovery); React.useEffect(() => { if (activeSessionRecovery && isUserAuthenticated) { cancelPendingAuth.current?.(); cancelPendingAuth.current = null; if (prevPerformRecovery.current !== performRecovery) { cancelPendingRecovery.current?.(); cancelPendingRecovery.current = null; prevPerformRecovery.current = performRecovery; } if (!authInProgress) { const [, cancel] = performRecovery(activeSessionRecovery); cancelPendingRecovery.current = cancel; } return; } cancelPendingRecovery.current?.(); cancelPendingRecovery.current = null; if (!hasAccessToken) { cancelPendingAuth.current?.(); cancelPendingAuth.current = null; } if ( !usingCommServicesAccessToken || isUserAuthenticated || !hasAccessToken ) { return; } if (prevPerformAuth.current !== performAuth) { cancelPendingAuth.current?.(); cancelPendingAuth.current = null; } prevPerformAuth.current = performAuth; if (authInProgress) { return; } const [, cancel] = performAuth(); cancelPendingAuth.current = cancel; }, [ activeSessionRecovery, authInProgress, performRecovery, hasAccessToken, isUserAuthenticated, performAuth, ]); return ; } const Handler: React.ComponentType = React.memo( KeyserverConnectionHandler, ); export default Handler; diff --git a/lib/shared/crypto-utils.js b/lib/shared/crypto-utils.js index e70d4974e..c257b045d 100644 --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -1,114 +1,114 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; import { getOlmSessionInitializationData, getOlmSessionInitializationDataActionTypes, } from '../actions/user-actions.js'; import { cookieSelector } from '../selectors/keyserver-selectors.js'; -import { OlmSessionCreatorContext } from '../shared/olm-session-creator-context.js'; import type { OLMOneTimeKeys, OLMPrekey } from '../types/crypto-types.js'; import { useLegacyAshoatKeyserverCall } from '../utils/action-utils.js'; import type { CallSingleKeyserverEndpointOptions, CallSingleKeyserverEndpoint, } from '../utils/call-single-keyserver-endpoint.js'; +import { getConfig } from '../utils/config.js'; import { values } from '../utils/objects.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; export type InitialNotifMessageOptions = { +callSingleKeyserverEndpoint?: ?CallSingleKeyserverEndpoint, +callSingleKeyserverEndpointOptions?: ?CallSingleKeyserverEndpointOptions, }; const initialEncryptedMessageContent = { type: 'init', }; function useInitialNotificationsEncryptedMessage( keyserverID: string, ): (options?: ?InitialNotifMessageOptions) => Promise { const callGetOlmSessionInitializationData = useLegacyAshoatKeyserverCall( getOlmSessionInitializationData, ); const dispatchActionPromise = useDispatchActionPromise(); const cookie = useSelector(cookieSelector(keyserverID)); - const olmSessionCreator = React.useContext(OlmSessionCreatorContext); - invariant(olmSessionCreator, 'Olm session creator should be set'); - const { notificationsSessionCreator } = olmSessionCreator; + const { olmAPI } = getConfig(); return React.useCallback( async options => { const callSingleKeyserverEndpoint = options?.callSingleKeyserverEndpoint; const callSingleKeyserverEndpointOptions = options?.callSingleKeyserverEndpointOptions; const initDataAction = callSingleKeyserverEndpoint ? getOlmSessionInitializationData(callSingleKeyserverEndpoint) : callGetOlmSessionInitializationData; const olmSessionDataPromise = initDataAction( callSingleKeyserverEndpointOptions, ); void dispatchActionPromise( getOlmSessionInitializationDataActionTypes, olmSessionDataPromise, ); - const { signedIdentityKeysBlob, notifInitializationInfo } = - await olmSessionDataPromise; + const [{ signedIdentityKeysBlob, notifInitializationInfo }] = + await Promise.all([ + olmSessionDataPromise, + olmAPI.initializeCryptoAccount(), + ]); const { notificationIdentityPublicKeys } = JSON.parse( signedIdentityKeysBlob.payload, ); - return await notificationsSessionCreator( + return await olmAPI.notificationsSessionCreator( cookie, notificationIdentityPublicKeys, notifInitializationInfo, keyserverID, ); }, [ callGetOlmSessionInitializationData, dispatchActionPromise, - notificationsSessionCreator, + olmAPI, cookie, keyserverID, ], ); } function getOneTimeKeyValues( oneTimeKeys: OLMOneTimeKeys, ): $ReadOnlyArray { return values(oneTimeKeys.curve25519); } function getPrekeyValue(prekey: OLMPrekey): string { const [prekeyValue] = values(prekey.curve25519); return prekeyValue; } function getOneTimeKeyValuesFromBlob(keyBlob: string): $ReadOnlyArray { const oneTimeKeys: OLMOneTimeKeys = JSON.parse(keyBlob); return getOneTimeKeyValues(oneTimeKeys); } function getPrekeyValueFromBlob(prekeyBlob: string): string { const prekey: OLMPrekey = JSON.parse(prekeyBlob); return getPrekeyValue(prekey); } export { getOneTimeKeyValues, getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, initialEncryptedMessageContent, useInitialNotificationsEncryptedMessage, }; diff --git a/lib/shared/olm-session-creator-context.js b/lib/shared/olm-session-creator-context.js deleted file mode 100644 index 97058e07d..000000000 --- a/lib/shared/olm-session-creator-context.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow - -import * as React from 'react'; - -import type { OLMIdentityKeys } from '../types/crypto-types.js'; -import type { OlmSessionInitializationInfo } from '../types/request-types.js'; - -export type OlmSessionCreatorContextType = { - +notificationsSessionCreator: ( - cookie: ?string, - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, - ) => Promise, - +contentSessionCreator: ( - contentIdentityKeys: OLMIdentityKeys, - contentInitializationInfo: OlmSessionInitializationInfo, - ) => Promise, -}; - -const OlmSessionCreatorContext: React.Context = - React.createContext(null); - -export { OlmSessionCreatorContext }; diff --git a/native/account/account-hooks.js b/native/account/account-hooks.js deleted file mode 100644 index 2ba3fad49..000000000 --- a/native/account/account-hooks.js +++ /dev/null @@ -1,56 +0,0 @@ -// @flow - -import * as React from 'react'; - -import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; -import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; -import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; - -import { - nativeNotificationsSessionCreator, - nativeOutboundContentSessionCreator, -} from '../utils/crypto-utils.js'; - -type Props = { - +children: React.Node, -}; - -function notificationsSessionCreator( - cookie: ?string, - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, -) { - return nativeNotificationsSessionCreator( - notificationsIdentityKeys, - notificationsInitializationInfo, - keyserverID, - ); -} - -function contentSessionCreator( - contentIdentityKeys: OLMIdentityKeys, - contentInitializationInfo: OlmSessionInitializationInfo, -) { - return nativeOutboundContentSessionCreator( - contentIdentityKeys, - contentInitializationInfo, - contentIdentityKeys.ed25519, - ); -} - -const contextValue = { - notificationsSessionCreator, - contentSessionCreator, -}; - -function OlmSessionCreatorProvider(props: Props): React.Node { - const { children } = props; - return ( - - {children} - - ); -} - -export { OlmSessionCreatorProvider }; diff --git a/native/root.react.js b/native/root.react.js index b249450ca..0d73f1715 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,396 +1,391 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { PossiblyStaleNavigationState, UnsafeContainerActionEvent, GenericNavigationAction, } from '@react-navigation/core'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, UIManager, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import { FarcasterDataHandler } from 'lib/components/farcaster-data-handler.react.js'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js'; import PrekeysHandler from 'lib/components/prekeys-handler.react.js'; import { StaffContextProvider } from 'lib/components/staff-provider.react.js'; import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js'; import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { actionLogger } from 'lib/utils/action-logger.js'; -import { OlmSessionCreatorProvider } from './account/account-hooks.js'; import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js'; import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import BackupHandler from './backup/backup-handler.js'; import { BottomSheetProvider } from './bottom-sheet/bottom-sheet-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import AccessTokenHandler from './components/access-token-handler.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import ReportHandler from './components/report-handler.react.js'; import VersionSupportedChecker from './components/version-supported.react.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { filesystemMediaCache } from './media/media-cache.js'; import { DeepLinksContextProvider } from './navigation/deep-links-context-provider.react.js'; import { defaultNavigationState } from './navigation/default-state.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import KeyserverReachabilityHandler from './navigation/keyserver-reachability-handler.js'; import { NavContext, type NavContextType, } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import { MessageSearchProvider } from './search/search-provider.react.js'; import Socket from './socket.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { provider } from './utils/ethers-utils.js'; import { neynarKey } from './utils/neynar-utils.js'; import { useTunnelbrokerInitMessage } from './utils/tunnelbroker-utils.js'; // Add custom items to expo-dev-menu import './dev-menu.js'; import './types/message-types-validator.js'; if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef(); const navDispatchRef = React.useRef GenericNavigationAction), ) => void>(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); void (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef>([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } void (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef>(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return undefined; } return navContainer.addListener( '__unsafe_action__', (event: { +data: UnsafeContainerActionEvent, ... }) => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }, ); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const tunnelbrokerInitMessage = useTunnelbrokerInitMessage(); const gated: React.Node = ( <> ); let navigation; if (initialState) { navigation = ( ); } return ( - - - - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - - - - {navigation} - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + + + + {navigation} + + + + + + + + + + + + + + + + + + ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( ); } export default AppRoot; diff --git a/native/utils/crypto-utils.js b/native/utils/crypto-utils.js deleted file mode 100644 index aa8dec937..000000000 --- a/native/utils/crypto-utils.js +++ /dev/null @@ -1,47 +0,0 @@ -// @flow - -import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; -import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; - -import { commCoreModule } from '../native-modules.js'; - -function nativeNotificationsSessionCreator( - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, -): Promise { - const { prekey, prekeySignature, oneTimeKey } = - notificationsInitializationInfo; - return commCoreModule.initializeNotificationsSession( - JSON.stringify(notificationsIdentityKeys), - prekey, - prekeySignature, - oneTimeKey, - keyserverID, - ); -} - -function nativeOutboundContentSessionCreator( - contentIdentityKeys: OLMIdentityKeys, - contentInitializationInfo: OlmSessionInitializationInfo, - deviceID: string, -): Promise { - const { prekey, prekeySignature, oneTimeKey } = contentInitializationInfo; - const identityKeys = JSON.stringify({ - curve25519: contentIdentityKeys.curve25519, - ed25519: contentIdentityKeys.ed25519, - }); - - return commCoreModule.initializeContentOutboundSession( - identityKeys, - prekey, - prekeySignature, - oneTimeKey, - deviceID, - ); -} - -export { - nativeNotificationsSessionCreator, - nativeOutboundContentSessionCreator, -}; diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js index e04b49c11..eb5277201 100644 --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,515 +1,306 @@ // @flow import olm from '@commapp/olm'; import invariant from 'invariant'; -import localforage from 'localforage'; import * as React from 'react'; import uuid from 'uuid'; -import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; -import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; -import { - hasMinCodeVersion, - NEXT_CODE_VERSION, -} from 'lib/shared/version-utils.js'; import type { SignedIdentityKeysBlob, CryptoStore, IdentityKeysBlob, CryptoStoreContextType, - OLMIdentityKeys, - NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; import { type IdentityNewDeviceKeyUpload, type IdentityExistingDeviceKeyUpload, } from 'lib/types/identity-service-types.js'; -import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; -import { getConfig } from 'lib/utils/config.js'; import { retrieveIdentityKeysAndPrekeys, getAccountOneTimeKeys, } from 'lib/utils/olm-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; -import { - generateCryptoKey, - encryptData, - exportKeyToJWK, -} from '../crypto/aes-gcm-crypto-utils.js'; import { initOlm } from '../olm/olm-utils.js'; -import { - getOlmDataContentKeyForCookie, - getOlmEncryptionKeyDBLabelForCookie, -} from '../push-notif/notif-crypto-utils.js'; import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; -import { isDesktopSafari } from '../shared-worker/utils/db-utils.js'; const CryptoStoreContext: React.Context = React.createContext(null); type Props = { +children: React.Node, }; function GetOrCreateCryptoStoreProvider(props: Props): React.Node { const dispatch = useDispatch(); const createCryptoStore = React.useCallback(async () => { await initOlm(); const identityAccount = new olm.Account(); identityAccount.create(); const { ed25519: identityED25519, curve25519: identityCurve25519 } = JSON.parse(identityAccount.identity_keys()); const identityAccountPicklingKey = uuid.v4(); const pickledIdentityAccount = identityAccount.pickle( identityAccountPicklingKey, ); const notificationAccount = new olm.Account(); notificationAccount.create(); const { ed25519: notificationED25519, curve25519: notificationCurve25519 } = JSON.parse(notificationAccount.identity_keys()); const notificationAccountPicklingKey = uuid.v4(); const pickledNotificationAccount = notificationAccount.pickle( notificationAccountPicklingKey, ); const newCryptoStore = { primaryAccount: { picklingKey: identityAccountPicklingKey, pickledAccount: pickledIdentityAccount, }, primaryIdentityKeys: { ed25519: identityED25519, curve25519: identityCurve25519, }, notificationAccount: { picklingKey: notificationAccountPicklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: { ed25519: notificationED25519, curve25519: notificationCurve25519, }, }; dispatch({ type: setCryptoStore, payload: newCryptoStore }); return newCryptoStore; }, [dispatch]); const currentCryptoStore = useSelector(state => state.cryptoStore); const createCryptoStorePromiseRef = React.useRef>(null); const getCryptoStorePromise = React.useCallback(() => { if (currentCryptoStore) { return Promise.resolve(currentCryptoStore); } const currentCreateCryptoStorePromiseRef = createCryptoStorePromiseRef.current; if (currentCreateCryptoStorePromiseRef) { return currentCreateCryptoStorePromiseRef; } const newCreateCryptoStorePromise = (async () => { try { return await createCryptoStore(); } catch (e) { createCryptoStorePromiseRef.current = undefined; throw e; } })(); createCryptoStorePromiseRef.current = newCreateCryptoStorePromise; return newCreateCryptoStorePromise; }, [createCryptoStore, currentCryptoStore]); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { createCryptoStorePromiseRef.current = undefined; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ getInitializedCryptoStore: getCryptoStorePromise, }), [getCryptoStorePromise], ); return ( {props.children} ); } function useGetOrCreateCryptoStore(): () => Promise { const context = React.useContext(CryptoStoreContext); invariant(context, 'CryptoStoreContext not found'); return context.getInitializedCryptoStore; } function useGetSignedIdentityKeysBlob(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); return React.useCallback(async () => { const [{ primaryAccount, primaryIdentityKeys, notificationIdentityKeys }] = await Promise.all([getOrCreateCryptoStore(), 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]); } function useGetNewDeviceKeyUpload(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); // `getExistingDeviceKeyUpload()` will initialize OLM, so no need to do it // again const getExistingDeviceKeyUpload = useGetExistingDeviceKeyUpload(); const dispatch = useDispatch(); return React.useCallback(async () => { const [ { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, }, cryptoStore, ] = await Promise.all([ getExistingDeviceKeyUpload(), getOrCreateCryptoStore(), ]); const primaryOLMAccount = new olm.Account(); const notificationOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( cryptoStore.primaryAccount.picklingKey, cryptoStore.primaryAccount.pickledAccount, ); notificationOLMAccount.unpickle( cryptoStore.notificationAccount.picklingKey, cryptoStore.notificationAccount.pickledAccount, ); const contentOneTimeKeys = getAccountOneTimeKeys(primaryOLMAccount); const notifOneTimeKeys = getAccountOneTimeKeys(notificationOLMAccount); const pickledPrimaryAccount = primaryOLMAccount.pickle( cryptoStore.primaryAccount.picklingKey, ); const pickledNotificationAccount = notificationOLMAccount.pickle( cryptoStore.notificationAccount.picklingKey, ); const updatedCryptoStore = { primaryAccount: { picklingKey: cryptoStore.primaryAccount.picklingKey, pickledAccount: pickledPrimaryAccount, }, primaryIdentityKeys: cryptoStore.primaryIdentityKeys, notificationAccount: { picklingKey: cryptoStore.notificationAccount.picklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: cryptoStore.notificationIdentityKeys, }; dispatch({ type: setCryptoStore, payload: updatedCryptoStore }); return { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, contentOneTimeKeys, notifOneTimeKeys, }; }, [dispatch, getOrCreateCryptoStore, getExistingDeviceKeyUpload]); } function useGetExistingDeviceKeyUpload(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); // `getSignedIdentityKeysBlob()` will initialize OLM, so no need to do it // again const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const dispatch = useDispatch(); return React.useCallback(async () => { const [ { payload: keyPayload, signature: keyPayloadSignature }, cryptoStore, ] = await Promise.all([ getSignedIdentityKeysBlob(), getOrCreateCryptoStore(), ]); const primaryOLMAccount = new olm.Account(); const notificationOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( cryptoStore.primaryAccount.picklingKey, cryptoStore.primaryAccount.pickledAccount, ); notificationOLMAccount.unpickle( cryptoStore.notificationAccount.picklingKey, cryptoStore.notificationAccount.pickledAccount, ); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = retrieveIdentityKeysAndPrekeys(primaryOLMAccount); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = retrieveIdentityKeysAndPrekeys(notificationOLMAccount); const pickledPrimaryAccount = primaryOLMAccount.pickle( cryptoStore.primaryAccount.picklingKey, ); const pickledNotificationAccount = notificationOLMAccount.pickle( cryptoStore.notificationAccount.picklingKey, ); const updatedCryptoStore = { primaryAccount: { picklingKey: cryptoStore.primaryAccount.picklingKey, pickledAccount: pickledPrimaryAccount, }, primaryIdentityKeys: cryptoStore.primaryIdentityKeys, notificationAccount: { picklingKey: cryptoStore.notificationAccount.picklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: cryptoStore.notificationIdentityKeys, }; dispatch({ type: setCryptoStore, payload: updatedCryptoStore }); return { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, }; }, [dispatch, getOrCreateCryptoStore, getSignedIdentityKeysBlob]); } -function OlmSessionCreatorProvider(props: Props): React.Node { - const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); - const currentCryptoStore = useSelector(state => state.cryptoStore); - const platformDetails = getConfig().platformDetails; - - const createNewNotificationsSession = React.useCallback( - async ( - cookie: ?string, - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, - ) => { - const [{ notificationAccount }, encryptionKey] = await Promise.all([ - getOrCreateCryptoStore(), - generateCryptoKey({ extractable: isDesktopSafari }), - initOlm(), - ]); - - const account = new olm.Account(); - const { picklingKey, pickledAccount } = notificationAccount; - account.unpickle(picklingKey, pickledAccount); - - const notificationsPrekey = notificationsInitializationInfo.prekey; - - const session = new olm.Session(); - session.create_outbound( - account, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - notificationsInitializationInfo.oneTimeKey, - ); - const { body: initialNotificationsEncryptedMessage } = session.encrypt( - JSON.stringify(initialEncryptedMessageContent), - ); - - const mainSession = session.pickle(picklingKey); - const notificationsOlmData: NotificationsOlmDataType = { - mainSession, - pendingSessionUpdate: mainSession, - updateCreationTimestamp: Date.now(), - picklingKey, - }; - const encryptedOlmData = await encryptData( - new TextEncoder().encode(JSON.stringify(notificationsOlmData)), - encryptionKey, - ); - - let notifsOlmDataContentKey; - let notifsOlmDataEncryptionKeyDBLabel; - - if ( - hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) - ) { - notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie( - cookie, - keyserverID, - ); - notifsOlmDataContentKey = getOlmDataContentKeyForCookie( - cookie, - keyserverID, - ); - } else { - notifsOlmDataEncryptionKeyDBLabel = - getOlmEncryptionKeyDBLabelForCookie(cookie); - notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); - } - - const persistEncryptionKeyPromise = (async () => { - let cryptoKeyPersistentForm; - if (isDesktopSafari) { - // Safari doesn't support structured clone algorithm in service - // worker context so we have to store CryptoKey as JSON - cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); - } else { - cryptoKeyPersistentForm = encryptionKey; - } - - await localforage.setItem( - notifsOlmDataEncryptionKeyDBLabel, - cryptoKeyPersistentForm, - ); - })(); - - await Promise.all([ - localforage.setItem(notifsOlmDataContentKey, encryptedOlmData), - persistEncryptionKeyPromise, - ]); - - return initialNotificationsEncryptedMessage; - }, - [getOrCreateCryptoStore, platformDetails], - ); - - const createNewContentSession = React.useCallback( - async ( - contentIdentityKeys: OLMIdentityKeys, - contentInitializationInfo: OlmSessionInitializationInfo, - ) => { - const [{ primaryAccount }] = await Promise.all([ - getOrCreateCryptoStore(), - initOlm(), - ]); - - const account = new olm.Account(); - const { picklingKey, pickledAccount } = primaryAccount; - account.unpickle(picklingKey, pickledAccount); - - const contentPrekey = contentInitializationInfo.prekey; - - const session = new olm.Session(); - session.create_outbound( - account, - contentIdentityKeys.curve25519, - contentIdentityKeys.ed25519, - contentPrekey, - contentInitializationInfo.prekeySignature, - contentInitializationInfo.oneTimeKey, - ); - const { body: initialContentEncryptedMessage } = session.encrypt( - JSON.stringify(initialEncryptedMessageContent), - ); - return initialContentEncryptedMessage; - }, - [getOrCreateCryptoStore], - ); - - const perKeyserverNotificationsSessionPromises = React.useRef<{ - [keyserverID: string]: ?Promise, - }>({}); - - const createNotificationsSession = React.useCallback( - async ( - cookie: ?string, - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, - ) => { - if (perKeyserverNotificationsSessionPromises.current[keyserverID]) { - return perKeyserverNotificationsSessionPromises.current[keyserverID]; - } - - const newNotificationsSessionPromise = (async () => { - try { - return await createNewNotificationsSession( - cookie, - notificationsIdentityKeys, - notificationsInitializationInfo, - keyserverID, - ); - } catch (e) { - perKeyserverNotificationsSessionPromises.current[keyserverID] = - undefined; - throw e; - } - })(); - - perKeyserverNotificationsSessionPromises.current[keyserverID] = - newNotificationsSessionPromise; - return newNotificationsSessionPromise; - }, - [createNewNotificationsSession], - ); - - const isCryptoStoreSet = !!currentCryptoStore; - React.useEffect(() => { - if (!isCryptoStoreSet) { - perKeyserverNotificationsSessionPromises.current = {}; - } - }, [isCryptoStoreSet]); - - const contextValue = React.useMemo( - () => ({ - notificationsSessionCreator: createNotificationsSession, - contentSessionCreator: createNewContentSession, - }), - [createNewContentSession, createNotificationsSession], - ); - - return ( - - {props.children} - - ); -} - export { useGetSignedIdentityKeysBlob, useGetOrCreateCryptoStore, - OlmSessionCreatorProvider, GetOrCreateCryptoStoreProvider, useGetNewDeviceKeyUpload, useGetExistingDeviceKeyUpload, }; diff --git a/web/root.js b/web/root.js index edc060a3f..039f33d21 100644 --- a/web/root.js +++ b/web/root.js @@ -1,77 +1,69 @@ // @flow import localforage from 'localforage'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import { createStore, applyMiddleware, type Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js'; import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import PrekeysHandler from 'lib/components/prekeys-handler.react.js'; import ReportHandler from 'lib/components/report-handler.react.js'; import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; -import { - GetOrCreateCryptoStoreProvider, - OlmSessionCreatorProvider, -} from './account/account-hooks.js'; import App from './app.react.js'; import ErrorBoundary from './error-boundary.react.js'; import IdentityServiceContextProvider from './grpc/identity-service-context-provider.react.js'; import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; import { synchronizeStoreWithOtherTabs, tabSynchronizationMiddleware, } from './redux/tab-synchronization.js'; import history from './router-history.js'; import { SQLiteDataHandler } from './shared-worker/sqlite-data-handler.js'; import { localforageConfig } from './shared-worker/utils/constants.js'; import Socket from './socket.react.js'; localforage.config(localforageConfig); const persistedReducer = persistReducer(persistConfig, reducer); const store: Store = createStore( persistedReducer, defaultWebState, composeWithDevTools({})( applyMiddleware(thunk, reduxLoggerMiddleware, tabSynchronizationMiddleware), ), ); synchronizeStoreWithOtherTabs(store); const persistor = persistStore(store); const RootProvider = (): React.Node => ( - - - - - - - - - - - - - - + + + + + + + + + + ); export default RootProvider;