diff --git a/lib/keyserver-conn/recovery-utils.js b/lib/keyserver-conn/recovery-utils.js index 9f35f7aec..444b31fc9 100644 --- a/lib/keyserver-conn/recovery-utils.js +++ b/lib/keyserver-conn/recovery-utils.js @@ -1,160 +1,159 @@ // @flow import { type ActionTypes, setNewSession } from './keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from '../shared/crypto-utils.js'; import type { RecoveryActionSource, LogInStartingPayload, LogInResult, } from '../types/account-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange } from '../types/session-types.js'; import callSingleKeyServerEndpoint from '../utils/call-single-keyserver-endpoint.js'; import type { CallSingleKeyserverEndpointOptions } from '../utils/call-single-keyserver-endpoint.js'; import { getConfig } from '../utils/config.js'; import { promiseAll } from '../utils/promises.js'; import { wrapActionPromise } from '../utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; // This function is a shortcut that tells us whether it's worth even trying to // call resolveKeyserverSessionInvalidation function canResolveKeyserverSessionInvalidation(): boolean { if (usingCommServicesAccessToken) { // We can always try to resolve a keyserver session invalidation // automatically using the Olm auth responder return true; } const { resolveKeyserverSessionInvalidationUsingNativeCredentials } = getConfig(); // If we can't use the Olm auth responder, then we can only resolve a // keyserver session invalidation on native, where we have access to the // user's native credentials. Note that we can't do this for ETH users, but we // don't know if the user is an ETH user from this function return !!resolveKeyserverSessionInvalidationUsingNativeCredentials; } // This function attempts to resolve an invalid keyserver session. A session can // become invalid when a keyserver invalidates it, or due to inconsistent client // state. If the client is usingCommServicesAccessToken, then the invalidation // recovery will try to go through the keyserver's Olm auth responder. // Otherwise, it will attempt to use the user's credentials to log in with the // legacy auth responder, which won't work on web and won't work for ETH users. async function resolveKeyserverSessionInvalidation( dispatch: Dispatch, cookie: ?string, urlPrefix: string, recoveryActionSource: RecoveryActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( - keyserverID: string, options?: ?InitialNotifMessageOptions, ) => Promise, ): Promise { const { resolveKeyserverSessionInvalidationUsingNativeCredentials } = getConfig(); if (!resolveKeyserverSessionInvalidationUsingNativeCredentials) { return null; } let newSessionChange = null; let callSingleKeyserverEndpointCallback = null; const boundCallSingleKeyserverEndpoint = async ( endpoint: Endpoint, data: { +[key: string]: mixed }, options?: ?CallSingleKeyserverEndpointOptions, ) => { const innerBoundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => { newSessionChange = sessionChange; setNewSession( dispatch, sessionChange, null, error, recoveryActionSource, keyserverID, ); }; try { const result = await callSingleKeyServerEndpoint( cookie, innerBoundSetNewSession, () => new Promise(r => r(null)), () => new Promise(r => r(null)), urlPrefix, null, false, null, null, endpoint, data, dispatch, options, false, keyserverID, ); if (callSingleKeyserverEndpointCallback) { callSingleKeyserverEndpointCallback(!!newSessionChange); } return result; } catch (e) { if (callSingleKeyserverEndpointCallback) { callSingleKeyserverEndpointCallback(!!newSessionChange); } throw e; } }; const boundCallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallSingleKeyserverEndpointOptions, ) => { if (requests[keyserverID]) { const promises = { [keyserverID]: boundCallSingleKeyserverEndpoint( endpoint, requests[keyserverID], options, ), }; return promiseAll(promises); } return Promise.resolve({}); }; const dispatchRecoveryAttempt = ( actionTypes: ActionTypes< 'LOG_IN_STARTED', 'LOG_IN_SUCCESS', 'LOG_IN_FAILED', >, promise: Promise, inputStartingPayload: LogInStartingPayload, ) => { const startingPayload = { ...inputStartingPayload, authActionSource: recoveryActionSource, }; void dispatch( wrapActionPromise(actionTypes, promise, null, startingPayload), ); return new Promise(r => (callSingleKeyserverEndpointCallback = r)); }; await resolveKeyserverSessionInvalidationUsingNativeCredentials( boundCallSingleKeyserverEndpoint, boundCallKeyserverEndpoint, dispatchRecoveryAttempt, recoveryActionSource, keyserverID, getInitialNotificationsEncryptedMessage, ); return newSessionChange; } export { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, }; diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index a62cdc7be..954fc7afa 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,254 +1,249 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, cookieSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import { createOpenSocketFunction } from '../shared/socket-utils.js'; import type { BoundStateSyncSpec } from '../shared/state-sync/state-sync-spec.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type CalendarQuery } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; import { getConfig } from '../utils/config.js'; import { values } from '../utils/objects.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector( urlPrefixSelector(keyserverID), // We don't actually use the cookie in the socket open function, // but we do use it in the initial message, and when the cookie changes // the socket needs to be reopened. By including the cookie here, // whenever the cookie changes this function will change, // which tells the Socket component to restart the connection. cookieSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }, ); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); // We pass all selectors specified in stateSyncSpecs and get the resulting // BoundStateSyncSpecs in the specs array. We do it so we don't have to // modify the selector when we add a new spec. type BoundStateSyncSpecs = { +specsPerHashKey: { +[string]: BoundStateSyncSpec }, +specPerInnerHashKey: { +[string]: BoundStateSyncSpec }, }; const stateSyncSpecSelectors = values(stateSyncSpecs).map( spec => spec.selector, ); const boundStateSyncSpecsSelector: AppState => BoundStateSyncSpecs = // The FlowFixMe is needed because createSelector types require flow // to know the number of subselectors at compile time. // $FlowFixMe createSelector(stateSyncSpecSelectors, (...specs) => { const boundSpecs = (specs: BoundStateSyncSpec[]); // We create a map from `hashKey` to a given spec for easier lookup later const specsPerHashKey = Object.fromEntries( boundSpecs.map(spec => [spec.hashKey, spec]), ); // We do the same for innerHashKey const specPerInnerHashKey = Object.fromEntries( boundSpecs .filter(spec => spec.innerHashSpec?.hashKey) .map(spec => [spec.innerHashSpec?.hashKey, spec]), ); return { specsPerHashKey, specPerInnerHashKey }; }); const getClientResponsesSelector: ( state: AppState, keyserverID: string, ) => ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, - getInitialNotificationsEncryptedMessage: ?( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( boundStateSyncSpecsSelector, currentCalendarQuery, (state: AppState, keyserverID: string) => keyserverID, ( boundStateSyncSpecs: BoundStateSyncSpecs, calendarQuery: (calendarActive: boolean) => CalendarQuery, keyserverID: string, ) => { return async ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, - getInitialNotificationsEncryptedMessage: ?( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); const { specsPerHashKey, specPerInnerHashKey } = boundStateSyncSpecs; for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { const query = calendarQuery(calendarActive); const hashResults: { [string]: boolean } = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; const [specKey, id] = key.split('|'); if (id) { hashValue = specPerInnerHashKey[specKey]?.getInfoHash( id, keyserverID, ); } else { hashValue = specsPerHashKey[specKey]?.getAllInfosHash( query, keyserverID, ); } // If hashValue values is null then we are still calculating // the hashes in the background. In this case we return true // to skip this state check. Future state checks (after the hash // calculation complete) will be handled normally. if (!hashValue) { hashResults[key] = true; } else { hashResults[key] = expectedHashValue === hashValue; } } const { failUnmentioned } = serverRequest; for (const spec of values(specPerInnerHashKey)) { const innerHashKey = spec.innerHashSpec?.hashKey; if (!failUnmentioned?.[spec.hashKey] || !innerHashKey) { continue; } const ids = spec.getIDs(query, keyserverID); if (!ids) { continue; } for (const id of ids) { const key = `${innerHashKey}|${id}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === - serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && - getInitialNotificationsEncryptedMessage + serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE ) { const initialNotificationsEncryptedMessage = - await getInitialNotificationsEncryptedMessage(keyserverID); + await getInitialNotificationsEncryptedMessage(); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const baseSessionStateFuncSelector: ( keyserverID: string, ) => ( state: AppState, ) => (calendarActive: boolean) => SessionState = keyserverID => createSelector( currentAsOfSelector(keyserverID), updatesCurrentAsOfSelector(keyserverID), currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); const sessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => (calendarActive: boolean) => SessionState = _memoize( baseSessionStateFuncSelector, ); export { openSocketSelector, queuedReports, getClientResponsesSelector, sessionStateFuncSelector, }; diff --git a/lib/shared/crypto-utils.js b/lib/shared/crypto-utils.js index 4a45fee33..4cc8b4408 100644 --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -1,114 +1,115 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import { getOlmSessionInitializationData, getOlmSessionInitializationDataActionTypes, } from '../actions/user-actions.js'; -import type { - OLMIdentityKeys, - OLMOneTimeKeys, - OLMPrekey, -} from '../types/crypto-types'; -import type { OlmSessionInitializationInfo } from '../types/request-types'; +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 { 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( - platformSpecificSessionCreator: ( - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, - ) => Promise, -): ( keyserverID: string, - options?: ?InitialNotifMessageOptions, -) => Promise { +): (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; + return React.useCallback( - async (keyserverID, options) => { + 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 { notificationIdentityPublicKeys } = JSON.parse( signedIdentityKeysBlob.payload, ); - return await platformSpecificSessionCreator( + return await notificationsSessionCreator( + cookie, notificationIdentityPublicKeys, notifInitializationInfo, keyserverID, ); }, [ callGetOlmSessionInitializationData, dispatchActionPromise, - platformSpecificSessionCreator, + notificationsSessionCreator, + 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, getPrekeyValue, getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, initialEncryptedMessageContent, useInitialNotificationsEncryptedMessage, }; diff --git a/lib/utils/config.js b/lib/utils/config.js index 573db6ae3..ff4463421 100644 --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -1,49 +1,48 @@ // @flow import invariant from 'invariant'; import type { CallSingleKeyserverEndpoint } from './call-single-keyserver-endpoint.js'; import type { DispatchRecoveryAttempt, CallKeyserverEndpoint, } from '../keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from '../shared/crypto-utils.js'; import type { RecoveryActionSource } from '../types/account-types.js'; import type { OlmAPI } from '../types/crypto-types.js'; import type { PlatformDetails } from '../types/device-types.js'; export type Config = { +resolveKeyserverSessionInvalidationUsingNativeCredentials: ?( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, recoveryActionSource: RecoveryActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( - keyserverID: string, options?: ?InitialNotifMessageOptions, ) => Promise, ) => Promise, +setSessionIDOnRequest: boolean, +calendarRangeInactivityLimit: ?number, +platformDetails: PlatformDetails, +authoritativeKeyserverID: string, +olmAPI: OlmAPI, }; let registeredConfig: ?Config = null; const registerConfig = (config: Config) => { registeredConfig = { ...registeredConfig, ...config }; }; const getConfig = (): Config => { invariant(registeredConfig, 'config should be set'); return registeredConfig; }; const hasConfig = (): boolean => { return !!registeredConfig; }; export { registerConfig, getConfig, hasConfig }; diff --git a/native/account/legacy-recover-keyserver-session.js b/native/account/legacy-recover-keyserver-session.js index 089a03824..d0a2f1e19 100644 --- a/native/account/legacy-recover-keyserver-session.js +++ b/native/account/legacy-recover-keyserver-session.js @@ -1,54 +1,53 @@ // @flow import { logInActionTypes, logInRawAction } from 'lib/actions/user-actions.js'; import type { DispatchRecoveryAttempt, CallKeyserverEndpoint, } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from 'lib/shared/crypto-utils.js'; import type { RecoveryActionSource } from 'lib/types/account-types.js'; import type { CallSingleKeyserverEndpoint } from 'lib/utils/call-single-keyserver-endpoint.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; import { store } from '../redux/redux-setup.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; async function resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, recoveryActionSource: RecoveryActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( - keyserverID: string, ?InitialNotifMessageOptions, ) => Promise, ) { const keychainCredentials = await fetchNativeKeychainCredentials(); if (!keychainCredentials) { return; } let extraInfo = await nativeLogInExtraInfoSelector(store.getState())(); if (getInitialNotificationsEncryptedMessage) { const initialNotificationsEncryptedMessage = - await getInitialNotificationsEncryptedMessage(keyserverID, { + await getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpoint, }); extraInfo = { ...extraInfo, initialNotificationsEncryptedMessage }; } const { calendarQuery } = extraInfo; await dispatchRecoveryAttempt( logInActionTypes, logInRawAction(callKeyserverEndpoint)({ ...keychainCredentials, ...extraInfo, authActionSource: recoveryActionSource, keyserverIDs: [keyserverID], }), { calendarQuery }, ); } export { resolveKeyserverSessionInvalidationUsingNativeCredentials }; diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index e31b3a826..3cd27f963 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,466 +1,459 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; import Animated from 'react-native-reanimated'; import { logInActionTypes, useLogIn, getOlmSessionInitializationDataActionTypes, useIdentityPasswordLogIn, identityLogInActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { type LogInInfo, type LogInExtraInfo, type LogInResult, type LogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { IdentityAuthResult } from 'lib/types/identity-service-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { TextInput } from './modal-components.react.js'; import { fetchNativeCredentials, setNativeCredentials, } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import PasswordInput from './password-input.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails, UnknownErrorAlertDetails, UserNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; -import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +logInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +legacyLogIn: (logInInfo: LogInInfo) => Promise, +identityPasswordLogIn: ( username: string, password: string, ) => Promise, - +getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + +getInitialNotificationsEncryptedMessage: () => Promise, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; componentDidMount() { void this.attemptToFetchCredentials(); } get usernameInputText(): string { return this.props.logInState.state.usernameInputText || ''; } get passwordInputText(): string { return this.props.logInState.state.passwordInputText || ''; } async attemptToFetchCredentials() { if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } const credentials = await fetchNativeCredentials(); if (!credentials) { return; } if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } this.props.logInState.setState({ usernameInputText: credentials.username, passwordInputText: credentials.password, }); } render(): React.Node { return ( ); } usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => { this.usernameInput = usernameInput; if (Platform.OS === 'ios' && usernameInput) { setTimeout(() => usernameInput.focus()); } }; focusUsernameInput: () => void = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => { this.passwordInput = passwordInput; }; focusPasswordInput: () => void = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; onChangeUsernameInputText: (text: string) => void = text => { this.props.logInState.setState({ usernameInputText: text.trim() }); }; onUsernameKeyPress: (event: KeyPressEvent) => void = event => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.passwordInputText.length === 0 ) { this.focusPasswordInput(); } }; onChangePasswordInputText: (text: string) => void = text => { this.props.logInState.setState({ passwordInputText: text }); }; onSubmit: () => Promise = async () => { this.props.setActiveAlert(true); if (this.usernameInputText.search(validEmailRegex) > -1) { Alert.alert( 'Can’t log in with email', 'You need to log in with your username now', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) { Alert.alert( 'Invalid username', 'Alphanumeric usernames only', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); return; } Keyboard.dismiss(); const extraInfo = await this.props.logInExtraInfo(); const initialNotificationsEncryptedMessage = - await this.props.getInitialNotificationsEncryptedMessage( - authoritativeKeyserverID, - ); + await this.props.getInitialNotificationsEncryptedMessage(); if (usingCommServicesAccessToken) { void this.props.dispatchActionPromise( identityLogInActionTypes, this.identityPasswordLogInAction(), ); } else { void this.props.dispatchActionPromise( logInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); } }; async legacyLogInAction(extraInfo: LogInExtraInfo): Promise { try { const result = await this.props.legacyLogIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, authActionSource: logInActionSources.logInFromNativeForm, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( UserNotFoundAlertDetails.title, UserNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } async identityPasswordLogInAction(): Promise { try { const result = await this.props.identityPasswordLogIn( this.usernameInputText, this.passwordInputText, ); this.props.setActiveAlert(false); await setNativeCredentials({ username: this.usernameInputText, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'user not found') { Alert.alert( UserNotFoundAlertDetails.title, UserNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'Unsupported version') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnsuccessfulLoginAlertAckowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onUsernameAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', }, this.focusUsernameInput, ); }; onPasswordAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { passwordInputText: '', }, this.focusPasswordInput, ); }; onUnknownErrorAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onAppOutOfDateAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); }; } export type InnerLogInPanel = LogInPanel; const styles = StyleSheet.create({ footer: { flexDirection: 'row', justifyContent: 'flex-end', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, row: { marginHorizontal: 24, }, }); const logInLoadingStatusSelector = createLoadingStatusSelector(logInActionTypes); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo(function ConnectedLogInPanel(props: BaseProps) { const logInLoadingStatus = useSelector(logInLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( logInLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyLogIn = useLogIn(); const callIdentityPasswordLogIn = useIdentityPasswordLogIn(); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage( - nativeNotificationsSessionCreator, - ); + useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/register-panel.react.js b/native/account/register-panel.react.js index dfe2fef6b..3eef64ff0 100644 --- a/native/account/register-panel.react.js +++ b/native/account/register-panel.react.js @@ -1,514 +1,507 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { keyserverRegisterActionTypes, keyserverRegister, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { RegisterInfo, LogInExtraInfo, RegisterResult, LogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails, UsernameReservedAlertDetails, UsernameTakenAlertDetails, UnknownErrorAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; -import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type RegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +registerState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +logInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +register: (registerInfo: RegisterInfo) => Promise, - +getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class RegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.registerState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); return ( {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.registerState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.registerState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.registerState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.registerState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.registerState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.passwordInputText !== this.props.registerState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); const extraInfo = await this.props.logInExtraInfo(); const initialNotificationsEncryptedMessage = - await this.props.getInitialNotificationsEncryptedMessage( - authoritativeKeyserverID, - ); + await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( keyserverRegisterActionTypes, this.registerAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async registerAction(extraInfo: LogInExtraInfo): Promise { try { const result = await this.props.register({ ...extraInfo, username: this.props.registerState.state.usernameInputText, password: this.props.registerState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.registerState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const registerLoadingStatusSelector = createLoadingStatusSelector( keyserverRegisterActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedRegisterPanel: React.ComponentType = React.memo(function ConnectedRegisterPanel(props: BaseProps) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useLegacyAshoatKeyserverCall(keyserverRegister); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage( - nativeNotificationsSessionCreator, - ); + useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedRegisterPanel; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js index 1210cb189..0b5bd5f13 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,138 +1,134 @@ // @flow import * as React from 'react'; import { siweAuth, siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { identityLogInActionTypes, useIdentityWalletLogIn, identityRegisterActionTypes, useIdentityWalletRegister, } from 'lib/actions/user-actions.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LogInStartingPayload, LogInExtraInfo, } from 'lib/types/account-types.js'; import type { SIWEResult } from 'lib/types/siwe-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.js'; import type { CallSingleKeyserverEndpointOptions } from 'lib/utils/call-single-keyserver-endpoint.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; -import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; type SIWEServerCallParams = { +message: string, +signature: string, +doNotRegister?: boolean, ... }; function useLegacySIWEServerCall(): ( SIWEServerCallParams, ?CallSingleKeyserverEndpointOptions, ) => Promise { const siweAuthCall = useLegacyAshoatKeyserverCall(siweAuth); const callSIWE = React.useCallback( ( message: string, signature: string, extraInfo: $ReadOnly<{ ...LogInExtraInfo, +doNotRegister?: boolean }>, callSingleKeyserverEndpointOptions: ?CallSingleKeyserverEndpointOptions, ) => siweAuthCall( { message, signature, ...extraInfo, }, callSingleKeyserverEndpointOptions, ), [siweAuthCall], ); const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage(nativeNotificationsSessionCreator); + useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ( { message, signature, doNotRegister }, callSingleKeyserverEndpointOptions, ) => { const extraInfo = await logInExtraInfo(); const initialNotificationsEncryptedMessage = - await getInitialNotificationsEncryptedMessage( - authoritativeKeyserverID, - { - callSingleKeyserverEndpointOptions, - }, - ); + await getInitialNotificationsEncryptedMessage({ + callSingleKeyserverEndpointOptions, + }); const siwePromise = callSIWE( message, signature, { ...extraInfo, initialNotificationsEncryptedMessage, doNotRegister, }, callSingleKeyserverEndpointOptions, ); void dispatchActionPromise( siweAuthActionTypes, siwePromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await siwePromise; }, [ logInExtraInfo, dispatchActionPromise, callSIWE, getInitialNotificationsEncryptedMessage, ], ); } function useIdentityWalletLogInCall(): SIWEResult => Promise { const identityWalletLogIn = useIdentityWalletLogIn(); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ({ address, message, signature }) => { const siwePromise = identityWalletLogIn(address, message, signature); void dispatchActionPromise(identityLogInActionTypes, siwePromise); await siwePromise; }, [dispatchActionPromise, identityWalletLogIn], ); } function useIdentityWalletRegisterCall(): SIWEResult => Promise { const identityWalletRegister = useIdentityWalletRegister(); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ({ address, message, signature }) => { const siwePromise = identityWalletRegister(address, message, signature); void dispatchActionPromise(identityRegisterActionTypes, siwePromise); await siwePromise; }, [dispatchActionPromise, identityWalletRegister], ); } export { useLegacySIWEServerCall, useIdentityWalletLogInCall, useIdentityWalletRegisterCall, }; diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js index b76dba0ed..c21236423 100644 --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -1,245 +1,244 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import { useStaffContext } from 'lib/components/staff-provider.react.js'; import { resolveKeyserverSessionInvalidation } from 'lib/keyserver-conn/recovery-utils.js'; import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import { cookieSelector, urlPrefixSelector, } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { recoveryActionSources, type RecoveryActionSource, } from 'lib/types/account-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import Alert from '../utils/alert.js'; -import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; async function clearSensitiveData() { await commCoreModule.clearSensitiveData(); try { await filesystemMediaCache.clearCache(); } catch { throw new Error('clear_media_cache_failed'); } } function SQLiteDataHandler(): React.Node { const storeLoaded = useSelector(state => state.storeLoaded); const dispatch = useDispatch(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const cookie = useSelector(cookieSelector(authoritativeKeyserverID)); const urlPrefix = useSelector(urlPrefixSelector(authoritativeKeyserverID)); invariant(urlPrefix, "missing urlPrefix for ashoat's keyserver"); const staffCanSee = useStaffCanSee(); const { staffUserHasBeenLoggedIn } = useStaffContext(); const loggedIn = useSelector(isLoggedIn); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const mediaCacheContext = React.useContext(MediaCacheContext); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage(nativeNotificationsSessionCreator); + useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const callFetchNewCookieFromNativeCredentials = React.useCallback( async (source: RecoveryActionSource) => { try { await resolveKeyserverSessionInvalidation( dispatch, cookie, urlPrefix, source, authoritativeKeyserverID, getInitialNotificationsEncryptedMessage, ); dispatch({ type: setStoreLoadedActionType }); } catch (fetchCookieException) { if (staffCanSee) { Alert.alert( `Error fetching new cookie from native credentials: ${ getMessageForException(fetchCookieException) ?? '{no exception message}' }. Please kill the app.`, ); } else { commCoreModule.terminate(); } } }, [ cookie, dispatch, staffCanSee, urlPrefix, getInitialNotificationsEncryptedMessage, ], ); const callClearSensitiveData = React.useCallback( async (triggeredBy: string) => { await clearSensitiveData(); console.log(`SQLite database deletion was triggered by ${triggeredBy}`); }, [], ); const handleSensitiveData = React.useCallback(async () => { try { const databaseCurrentUserInfoID = await commCoreModule.getCurrentUserID(); if ( databaseCurrentUserInfoID && databaseCurrentUserInfoID !== currentLoggedInUserID ) { await callClearSensitiveData('change in logged-in user credentials'); } if (currentLoggedInUserID) { await commCoreModule.setCurrentUserID(currentLoggedInUserID); } } catch (e) { if (isTaskCancelledError(e)) { return; } if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } }, [callClearSensitiveData, currentLoggedInUserID]); React.useEffect(() => { if (!rehydrateConcluded) { return; } const databaseNeedsDeletion = commCoreModule.checkIfDatabaseNeedsDeletion(); if (databaseNeedsDeletion) { void (async () => { try { await callClearSensitiveData('detecting corrupted database'); } catch (e) { if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } await callFetchNewCookieFromNativeCredentials( recoveryActionSources.corruptedDatabaseDeletion, ); })(); return; } const sensitiveDataHandled = handleSensitiveData(); if (storeLoaded) { return; } if (!loggedIn) { dispatch({ type: setStoreLoadedActionType }); return; } void (async () => { await Promise.all([ sensitiveDataHandled, mediaCacheContext?.evictCache(), ]); try { const { threads, messages, drafts, messageStoreThreads, reports, users, keyservers, communities, } = await commCoreModule.getClientDBStore(); const threadInfosFromDB = threadStoreOpsHandlers.translateClientDBData(threads); const reportsFromDB = reportStoreOpsHandlers.translateClientDBData(reports); const usersFromDB = userStoreOpsHandlers.translateClientDBData(users); const keyserverInfosFromDB = keyserverStoreOpsHandlers.translateClientDBData(keyservers); const communityInfosFromDB = communityStoreOpsHandlers.translateClientDBData(communities); dispatch({ type: setClientDBStoreActionType, payload: { drafts, messages, threadStore: { threadInfos: threadInfosFromDB }, currentUserID: currentLoggedInUserID, messageStoreThreads, reports: reportsFromDB, users: usersFromDB, keyserverInfos: keyserverInfosFromDB, communities: communityInfosFromDB, }, }); } catch (setStoreException) { if (isTaskCancelledError(setStoreException)) { dispatch({ type: setStoreLoadedActionType }); return; } if (staffCanSee) { Alert.alert( 'Error setting threadStore or messageStore', getMessageForException(setStoreException) ?? '{no exception message}', ); } await callFetchNewCookieFromNativeCredentials( recoveryActionSources.sqliteLoadFailure, ); } })(); }, [ currentLoggedInUserID, handleSensitiveData, loggedIn, cookie, dispatch, rehydrateConcluded, staffCanSee, storeLoaded, urlPrefix, staffUserHasBeenLoggedIn, callFetchNewCookieFromNativeCredentials, callClearSensitiveData, mediaCacheContext, ]); return null; } export { SQLiteDataHandler, clearSensitiveData }; diff --git a/native/selectors/socket-selectors.js b/native/selectors/socket-selectors.js index 76512a576..152eeed43 100644 --- a/native/selectors/socket-selectors.js +++ b/native/selectors/socket-selectors.js @@ -1,116 +1,110 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; import { commCoreModule } from '../native-modules.js'; import { calendarActiveSelector } from '../navigation/nav-selectors.js'; import type { AppState } from '../redux/state-types.js'; import type { NavPlusRedux } from '../types/selector-types.js'; const baseSessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = keyserverID => createSelector( cookieSelector(keyserverID), (cookie: ?string): SessionIdentification => ({ cookie }), ); const sessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = _memoize( baseSessionIdentificationSelector, ); async function getSignedIdentityKeysBlob(): Promise { await commCoreModule.initializeCryptoAccount(); const { blobPayload, signature } = await commCoreModule.getUserPublicKey(); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: blobPayload, signature, }; return signedIdentityKeysBlob; } type NativeGetClientResponsesSelectorInputType = $ReadOnly<{ ...NavPlusRedux, - getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, - keyserverID: string, + +getInitialNotificationsEncryptedMessage: () => Promise, + +keyserverID: string, }>; const nativeGetClientResponsesSelector: ( input: NativeGetClientResponsesSelectorInputType, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (input: NativeGetClientResponsesSelectorInputType) => getClientResponsesSelector(input.redux, input.keyserverID), (input: NativeGetClientResponsesSelectorInputType) => calendarActiveSelector(input.navContext), (input: NativeGetClientResponsesSelectorInputType) => input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, - getInitialNotificationsEncryptedMessage: ?( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, calendarActive: boolean, - getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, serverRequests, ), ); const baseNativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = keyserverID => createSelector( (input: NavPlusRedux) => sessionStateFuncSelector(keyserverID)(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const nativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = _memoize( baseNativeSessionStateFuncSelector, ); export { sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, }; diff --git a/native/socket.react.js b/native/socket.react.js index 473099d24..3efb21b0b 100644 --- a/native/socket.react.js +++ b/native/socket.react.js @@ -1,154 +1,151 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { canResolveKeyserverSessionInvalidation } from 'lib/keyserver-conn/recovery-utils.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import { openSocketSelector } from 'lib/selectors/socket-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { activeMessageListSelector, nativeCalendarQuery, } from './navigation/nav-selectors.js'; import { NavContext } from './navigation/navigation-context.js'; import { useSelector } from './redux/redux-utils.js'; import { noDataAfterPolicyAcknowledgmentSelector } from './selectors/account-selectors.js'; import { sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import Alert from './utils/alert.js'; -import { nativeNotificationsSessionCreator } from './utils/crypto-utils.js'; import { decompressMessage } from './utils/decompress.js'; const NativeSocket: React.ComponentType = React.memo(function NativeSocket(props: BaseSocketProps) { const navContext = React.useContext(NavContext); const { keyserverID } = props; const cookie = useSelector(cookieSelector(keyserverID)); const connection = useSelector(connectionSelector(keyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const frozen = useSelector(state => state.frozen); const active = useSelector( state => isLoggedIn(state) && state.lifecycleState !== 'background', ); const noDataAfterPolicyAcknowledgment = useSelector( noDataAfterPolicyAcknowledgmentSelector(keyserverID), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const openSocket = useSelector(openSocketSelector(keyserverID)); invariant(openSocket, 'openSocket failed to be created'); const sessionIdentification = useSelector( sessionIdentificationSelector(keyserverID), ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(keyserverID), ); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage( - nativeNotificationsSessionCreator, - ); + useInitialNotificationsEncryptedMessage(keyserverID); const getClientResponses = useSelector(state => nativeGetClientResponsesSelector({ redux: state, navContext, getInitialNotificationsEncryptedMessage, keyserverID, }), ); const sessionStateFunc = useSelector(state => nativeSessionStateFuncSelector(keyserverID)({ redux: state, navContext, }), ); const currentCalendarQuery = useSelector(state => nativeCalendarQuery({ redux: state, navContext, }), ); const activeThread = React.useMemo(() => { if (!active) { return null; } return activeMessageListSelector(navContext); }, [active, navContext]); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(keyserverID), ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const hasPassword = accountHasPassword(currentUserInfo); const showSocketCrashLoopAlert = React.useCallback(() => { if ( canResolveKeyserverSessionInvalidation() && (hasPassword || usingCommServicesAccessToken) ) { // In this case, we expect that the socket crash loop recovery // will be invisible to the user, so we don't show an alert return; } Alert.alert( 'Log in needed', 'After acknowledging the policies, we need you to log in to your ' + 'account again', [{ text: 'OK' }], ); }, [hasPassword]); const activeSessionRecovery = useSelector( state => state.keyserverStore.keyserverInfos[keyserverID]?.connection .activeSessionRecovery, ); return ( ); }); export default NativeSocket; diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js index 15d98c554..72e87a2ce 100644 --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,431 +1,418 @@ // @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, getPrekeyValueFromBlob, } from 'lib/shared/crypto-utils.js'; import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; import type { SignedIdentityKeysBlob, CryptoStore, IdentityKeysBlob, CryptoStoreContextType, OLMIdentityKeys, NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; import { type IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { retrieveAccountKeysSet } 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 { isDesktopSafari } from '../database/utils/db-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'; 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 useGetDeviceKeyUpload(): () => 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 [signedIdentityKeysBlob, 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 primaryAccountKeysSet = retrieveAccountKeysSet(primaryOLMAccount); const notificationAccountKeysSet = retrieveAccountKeysSet( 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: signedIdentityKeysBlob.payload, keyPayloadSignature: signedIdentityKeysBlob.signature, contentPrekey: primaryAccountKeysSet.prekey, contentPrekeySignature: primaryAccountKeysSet.prekeySignature, notifPrekey: notificationAccountKeysSet.prekey, notifPrekeySignature: notificationAccountKeysSet.prekeySignature, contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys, notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys, }; }, [dispatch, getOrCreateCryptoStore, getSignedIdentityKeysBlob]); } function OlmSessionCreatorProvider(props: Props): React.Node { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); const currentCryptoStore = useSelector(state => state.cryptoStore); 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 = getPrekeyValueFromBlob( 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, ); const notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie(cookie); const notifsOlmDataContentKey = getOlmDataContentKeyForCookie( cookie, keyserverID, ); 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], ); 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 = getPrekeyValueFromBlob( 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 notificationsSessionPromise = React.useRef>(null); const createNotificationsSession = React.useCallback( async ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { if (notificationsSessionPromise.current) { return notificationsSessionPromise.current; } const newNotificationsSessionPromise = (async () => { try { return await createNewNotificationsSession( cookie, notificationsIdentityKeys, notificationsInitializationInfo, keyserverID, ); } catch (e) { notificationsSessionPromise.current = undefined; throw e; } })(); notificationsSessionPromise.current = newNotificationsSessionPromise; return newNotificationsSessionPromise; }, [createNewNotificationsSession], ); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { notificationsSessionPromise.current = undefined; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ notificationsSessionCreator: createNotificationsSession, contentSessionCreator: createNewContentSession, }), [createNewContentSession, createNotificationsSession], ); return ( {props.children} ); } -function useWebNotificationsSessionCreator(): ( - cookie: ?string, - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - keyserverID: string, -) => Promise { - const context = React.useContext(OlmSessionCreatorContext); - invariant(context, 'WebNotificationsSessionCreator not found.'); - - return context.notificationsSessionCreator; -} - export { useGetSignedIdentityKeysBlob, useGetOrCreateCryptoStore, OlmSessionCreatorProvider, - useWebNotificationsSessionCreator, GetOrCreateCryptoStoreProvider, useGetDeviceKeyUpload, }; diff --git a/web/selectors/socket-selectors.js b/web/selectors/socket-selectors.js index 86ccb1594..0dafa5bcc 100644 --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -1,114 +1,108 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { sessionIDSelector, cookieSelector, } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; import type { AppState } from '../redux/redux-setup.js'; const baseSessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = keyserverID => createSelector( cookieSelector(keyserverID), sessionIDSelector(keyserverID), (cookie: ?string, sessionID: ?string): SessionIdentification => ({ cookie, sessionID, }), ); const sessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = _memoize( baseSessionIdentificationSelector, ); type WebGetClientResponsesSelectorInputType = { +state: AppState, +getSignedIdentityKeysBlob: () => Promise, - +getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + +getInitialNotificationsEncryptedMessage: () => Promise, +keyserverID: string, }; const webGetClientResponsesSelector: ( input: WebGetClientResponsesSelectorInputType, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (input: WebGetClientResponsesSelectorInputType) => getClientResponsesSelector(input.state, input.keyserverID), (input: WebGetClientResponsesSelectorInputType) => input.getSignedIdentityKeysBlob, (input: WebGetClientResponsesSelectorInputType) => input.state.navInfo.tab === 'calendar', (input: WebGetClientResponsesSelectorInputType) => input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, getSignedIdentityKeysBlob: () => Promise, - getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, getSignedIdentityKeysBlob: () => Promise, calendarActive: boolean, - getInitialNotificationsEncryptedMessage: ( - keyserverID: string, - ) => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, serverRequests, ), ); const baseWebSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = keyserverID => createSelector( sessionStateFuncSelector(keyserverID), (state: AppState) => state.navInfo.tab === 'calendar', ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const webSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = _memoize( baseWebSessionStateFuncSelector, ); export { sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, }; diff --git a/web/socket.react.js b/web/socket.react.js index 7d5f059ee..ed3683de4 100644 --- a/web/socket.react.js +++ b/web/socket.react.js @@ -1,133 +1,114 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import { openSocketSelector } from 'lib/selectors/socket-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; -import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; -import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; -import { - useGetSignedIdentityKeysBlob, - useWebNotificationsSessionCreator, -} from './account/account-hooks.js'; +import { useGetSignedIdentityKeysBlob } from './account/account-hooks.js'; import { useSelector } from './redux/redux-utils.js'; import { activeThreadSelector, webCalendarQuery, } from './selectors/nav-selectors.js'; import { sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import { decompressMessage } from './utils/decompress.js'; const WebSocket: React.ComponentType = React.memo(function WebSocket(props) { const { keyserverID } = props; const cookie = useSelector(cookieSelector(keyserverID)); const connection = useSelector(connectionSelector(keyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const active = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous && state.lifecycleState !== 'background', ); const openSocket = useSelector(openSocketSelector(keyserverID)); invariant(openSocket, 'openSocket failed to be created'); const sessionIdentification = useSelector( sessionIdentificationSelector(keyserverID), ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(keyserverID), ); const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); - const webNotificationsSessionCreator = useWebNotificationsSessionCreator(); - const webNotifsSessionCreatorForCookie = React.useCallback( - async ( - notificationsIdentityKeys: OLMIdentityKeys, - notificationsInitializationInfo: OlmSessionInitializationInfo, - ) => - webNotificationsSessionCreator( - cookie, - notificationsIdentityKeys, - notificationsInitializationInfo, - keyserverID, - ), - [webNotificationsSessionCreator, cookie, keyserverID], - ); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage(webNotifsSessionCreatorForCookie); + useInitialNotificationsEncryptedMessage(keyserverID); const getClientResponses = useSelector(state => webGetClientResponsesSelector({ state, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, keyserverID, }), ); const sessionStateFunc = useSelector( webSessionStateFuncSelector(keyserverID), ); const currentCalendarQuery = useSelector(webCalendarQuery); const reduxActiveThread = useSelector(activeThreadSelector); const windowActive = useSelector(state => state.windowActive); const activeThread = React.useMemo(() => { if (!active || !windowActive) { return null; } return reduxActiveThread; }, [active, windowActive, reduxActiveThread]); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(keyserverID), ); const activeSessionRecovery = useSelector( state => state.keyserverStore.keyserverInfos[keyserverID]?.connection .activeSessionRecovery, ); return ( ); }); export default WebSocket;