diff --git a/lib/components/keyserver-connection-handler.js b/lib/components/keyserver-connection-handler.js index b351b376b..ef30108e0 100644 --- a/lib/components/keyserver-connection-handler.js +++ b/lib/components/keyserver-connection-handler.js @@ -1,91 +1,90 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { logOutActionTypes, useLogOut } from '../actions/user-actions.js'; import { connectionSelector, cookieSelector, } from '../selectors/keyserver-selectors.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 { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; type Props = { ...BaseSocketProps, +keyserverID: string, +socketComponent: React.ComponentType, }; function KeyserverConnectionHandler(props: Props) { const { socketComponent: Socket, keyserverID, ...rest } = props; const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const hasConnectionIssue = useSelector( state => !!connectionSelector(keyserverID)(state)?.connectionIssue, ); const cookie = useSelector(cookieSelector(keyserverID)); React.useEffect(() => { if (hasConnectionIssue) { void dispatchActionPromise(logOutActionTypes, callLogOut()); } }, [callLogOut, hasConnectionIssue, dispatchActionPromise]); - const identityClient = React.useContext( - IdentityClientContext, - )?.identityClient; - invariant(identityClient, 'Identity client should be set'); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { identityClient } = identityContext; const olmSessionCreator = React.useContext(OlmSessionCreatorContext); invariant(olmSessionCreator, 'Olm session creator should be set'); React.useEffect(() => { if (!usingCommServicesAccessToken) { return; } void (async () => { try { const keyserverKeys = await identityClient.getKeyserverKeys(keyserverID); // eslint-disable-next-line no-unused-vars const [notifsSession, contentSession] = await Promise.all([ olmSessionCreator.notificationsSessionCreator( cookie, keyserverKeys.identityKeysBlob.notificationIdentityPublicKeys, keyserverKeys.notifInitializationInfo, keyserverID, ), olmSessionCreator.contentSessionCreator( keyserverKeys.identityKeysBlob.primaryIdentityPublicKeys, keyserverKeys.contentInitializationInfo, ), ]); } catch (e) { console.log( `Error getting keys for keyserver with id ${keyserverID}`, e, ); } })(); }, [keyserverID, identityClient, olmSessionCreator, cookie]); if (keyserverID !== ashoatKeyserverID) { return null; } return ; } const Handler: React.ComponentType = React.memo( KeyserverConnectionHandler, ); export default Handler; diff --git a/lib/shared/identity-client-context.js b/lib/shared/identity-client-context.js index da710a44d..1ddc2d879 100644 --- a/lib/shared/identity-client-context.js +++ b/lib/shared/identity-client-context.js @@ -1,14 +1,21 @@ // @flow import * as React from 'react'; import type { IdentityServiceClient } from '../types/identity-service-types.js'; +export type AuthMetadata = { + +deviceID: ?string, + +userID: ?string, + +accessToken: ?string, +}; + export type IdentityClientContextType = { +identityClient: IdentityServiceClient, + +getAuthMetadata: () => Promise, }; const IdentityClientContext: React.Context = React.createContext(); export { IdentityClientContext }; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js index 4f07a89ec..72c470fe4 100644 --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -1,242 +1,243 @@ // @flow import * as React from 'react'; import { getOneTimeKeyArray } from 'lib/shared/crypto-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { type IdentityKeysBlob, identityKeysBlobValidator, } from 'lib/types/crypto-types.js'; import { type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type IdentityServiceClient, ONE_TIME_KEYS_NUMBER, type UserDevicesOlmOutboundKeys, type UserLoginResponse, } from 'lib/types/identity-service-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { getCommServicesAuthMetadataEmitter } from '../event-emitters/csa-auth-metadata-emitter.js'; import { commCoreModule, commRustModule } from '../native-modules.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const authMetadataPromiseRef = React.useRef>(); if (!authMetadataPromiseRef.current) { authMetadataPromiseRef.current = (async () => { const { userID, accessToken } = await commCoreModule.getCommServicesAuthMetadata(); return { userID, accessToken }; })(); } React.useEffect(() => { const metadataEmitter = getCommServicesAuthMetadataEmitter(); const subscription = metadataEmitter.addListener( 'commServicesAuthMetadata', (authMetadata: UserLoginResponse) => { authMetadataPromiseRef.current = Promise.resolve({ userID: authMetadata.userId, accessToken: authMetadata.accessToken, }); }, ); return () => subscription.remove(); }, []); const getAuthMetadata = React.useCallback< () => Promise<{ +deviceID: string, +userID: string, +accessToken: string, }>, >(async () => { const deviceID = await getContentSigningKey(); const authMetadata = await authMetadataPromiseRef.current; const userID = authMetadata?.userID; const accessToken = authMetadata?.accessToken; if (!deviceID || !userID || !accessToken) { throw new Error('Identity service client is not initialized'); } return { deviceID, userID, accessToken }; }, []); const client = React.useMemo( () => ({ deleteUser: async () => { const { deviceID, userID, accessToken } = await getAuthMetadata(); return commRustModule.deleteUser(userID, deviceID, accessToken); }, getKeyserverKeys: async ( keyserverID: string, ): Promise => { const { deviceID, userID, accessToken } = await getAuthMetadata(); const result = await commRustModule.getKeyserverKeys( userID, deviceID, accessToken, keyserverID, ); const resultObject = JSON.parse(result); const payload = resultObject?.payload; const keyserverKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: resultObject?.contentPrekey, prekeySignature: resultObject?.contentPrekeySignature, oneTimeKey: resultObject?.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: resultObject?.notifPrekey, prekeySignature: resultObject?.notifPrekeySignature, oneTimeKey: resultObject?.oneTimeNotifPrekey, }, payloadSignature: resultObject?.payloadSignature, socialProof: resultObject?.socialProof, }; if (!keyserverKeys.contentInitializationInfo.oneTimeKey) { throw new Error('Missing content one time key'); } if (!keyserverKeys.notifInitializationInfo.oneTimeKey) { throw new Error('Missing notif one time key'); } return assertWithValidator( keyserverKeys, deviceOlmOutboundKeysValidator, ); }, getOutboundKeysForUser: async ( targetUserID: string, ): Promise => { const { deviceID: authDeviceID, userID, accessToken, } = await getAuthMetadata(); const result = await commRustModule.getOutboundKeysForUser( userID, authDeviceID, accessToken, targetUserID, ); const resultArray = JSON.parse(result); return resultArray .map(outboundKeysInfo => { try { const payload = outboundKeysInfo?.payload; const identityKeysBlob: IdentityKeysBlob = assertWithValidator( payload ? JSON.parse(payload) : null, identityKeysBlobValidator, ); const deviceID = identityKeysBlob.primaryIdentityPublicKeys.ed25519; if ( !outboundKeysInfo.oneTimeContentPrekey || !outboundKeysInfo.oneTimeNotifPrekey ) { console.log(`Missing one time key for device ${deviceID}`); return { deviceID, keys: null, }; } const deviceKeys = { identityKeysBlob, contentInitializationInfo: { prekey: outboundKeysInfo?.contentPrekey, prekeySignature: outboundKeysInfo?.contentPrekeySignature, oneTimeKey: outboundKeysInfo?.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: outboundKeysInfo?.notifPrekey, prekeySignature: outboundKeysInfo?.notifPrekeySignature, oneTimeKey: outboundKeysInfo?.oneTimeNotifPrekey, }, payloadSignature: outboundKeysInfo?.payloadSignature, socialProof: outboundKeysInfo?.socialProof, }; try { const validatedKeys = assertWithValidator( deviceKeys, deviceOlmOutboundKeysValidator, ); return { deviceID, keys: validatedKeys, }; } catch (e) { console.log(e); return { deviceID, keys: null, }; } } catch (e) { console.log(e); return null; } }) .filter(Boolean); }, registerUser: async (username: string, password: string) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature }, notificationsOneTimeKeys, primaryOneTimeKeys, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getNotificationsOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.getPrimaryOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.generateAndGetPrekeys(), ]); const registrationResult = await commRustModule.registerUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyArray(primaryOneTimeKeys), getOneTimeKeyArray(notificationsOneTimeKeys), ); const { userID, accessToken } = JSON.parse(registrationResult); return { accessToken, userID, username }; }, }), [getAuthMetadata], ); const value = React.useMemo( () => ({ identityClient: client, + getAuthMetadata, }), - [client], + [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js index ba3c95e58..7282e6f0a 100644 --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -1,48 +1,61 @@ // @flow import * as React from 'react'; -import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import { + IdentityClientContext, + type AuthMetadata, +} from 'lib/shared/identity-client-context.js'; import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const deviceID = useSelector( state => state.cryptoStore?.primaryIdentityKeys.ed25519, ); const client = React.useMemo(() => { let authLayer = null; if (userID && deviceID && accessToken) { authLayer = { userID, deviceID, commServicesAccessToken: accessToken, }; } return new IdentityServiceClientWrapper(authLayer); }, [accessToken, deviceID, userID]); + const getAuthMetadata = React.useCallback<() => Promise>( + async () => ({ + userID, + deviceID, + accessToken, + }), + [accessToken, deviceID, userID], + ); + const value = React.useMemo( () => ({ identityClient: client, + getAuthMetadata, }), - [client], + [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider;