diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -8,6 +8,7 @@ sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import { preRequestUserStateSelector } from '../selectors/account-selectors.js'; +import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, @@ -29,7 +30,6 @@ UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; -import type { IdentityServiceClient } from '../types/identity-service-types'; import type { RawMessageInfo, MessageTruncationStatuses, @@ -169,23 +169,15 @@ failed: 'DELETE_IDENTITY_ACCOUNT_FAILED', }); -function useDeleteIdentityAccount(): ( - client: IdentityServiceClient, - deviceID: ?string, -) => Promise { - const userID = useSelector(state => state.currentUserInfo?.id); - const accessToken = useSelector(state => state.commServicesAccessToken); - const deleteIdentityAccount = React.useCallback( - async (client: IdentityServiceClient, deviceID: ?string) => { - if (!userID || !accessToken || !deviceID) { - throw new Error('missing identity service auth metadata'); - } - await client.deleteUser(userID, deviceID, accessToken); - }, - [userID, accessToken], - ); - - return deleteIdentityAccount; +function useDeleteIdentityAccount(): () => Promise { + const client = React.useContext(IdentityClientContext); + const identityClient = client?.identityClient; + return React.useCallback(() => { + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + return identityClient.deleteUser(); + }, [identityClient]); } const registerActionTypes = Object.freeze({ diff --git a/lib/shared/identity-client-context.js b/lib/shared/identity-client-context.js new file mode 100644 --- /dev/null +++ b/lib/shared/identity-client-context.js @@ -0,0 +1,14 @@ +// @flow + +import * as React from 'react'; + +import type { IdentityServiceClient } from '../types/identity-service-types.js'; + +export type IdentityClientContextType = { + +identityClient: ?IdentityServiceClient, +}; + +const IdentityClientContext: React.Context = + React.createContext(); + +export { IdentityClientContext }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -20,11 +20,8 @@ }; export interface IdentityServiceClient { - +deleteUser: ( - userID: string, - deviceID: string, - accessToken: string, - ) => Promise; + +deleteUser: () => Promise; + +getKeyserverKeys: string => Promise; } export type IdentityServiceAuthLayer = { diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js new file mode 100644 --- /dev/null +++ b/native/identity-service/identity-service-context-provider.react.js @@ -0,0 +1,107 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { + IdentityServiceClient, + OutboundKeyInfoResponse, + UserLoginResponse, +} from 'lib/types/identity-service-types.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(() => { + return { + deleteUser: async () => { + const { deviceID, userID, accessToken } = await getAuthMetadata(); + return commRustModule.deleteUser(userID, deviceID, accessToken); + }, + getKeyserverKeys: async (keyserverID: string) => { + const { deviceID, userID, accessToken } = await getAuthMetadata(); + const result = await commRustModule.getKeyserverKeys( + userID, + deviceID, + accessToken, + keyserverID, + ); + const resultObject: OutboundKeyInfoResponse = JSON.parse(result); + if ( + !resultObject.payload || + !resultObject.payloadSignature || + !resultObject.contentPrekey || + !resultObject.contentPrekeySignature || + !resultObject.notifPrekey || + !resultObject.notifPrekeySignature + ) { + throw new Error('Invalid response from Identity service'); + } + return resultObject; + }, + }; + }, [getAuthMetadata]); + + const value = React.useMemo( + () => ({ + identityClient: client, + }), + [client], + ); + + return ( + + {children} + + ); +} + +export default IdentityServiceContextProvider; diff --git a/native/profile/delete-account.react.js b/native/profile/delete-account.react.js --- a/native/profile/delete-account.react.js +++ b/native/profile/delete-account.react.js @@ -21,12 +21,10 @@ import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import Button from '../components/button.react.js'; -import { commRustModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; const keyserverLoadingStatusSelector = createLoadingStatusSelector( deleteKeyserverAccountActionTypes, @@ -87,8 +85,7 @@ const deleteIdentityAction = React.useCallback(async () => { try { - const deviceID = await getContentSigningKey(); - return await callDeleteIdentityAccount(commRustModule, deviceID); + return await callDeleteIdentityAccount(); } catch (e) { Alert.alert( 'Unknown error deleting account', diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -45,6 +45,7 @@ import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import { peerToPeerMessageHandler } from './handlers/peer-to-peer-message-handler.js'; +import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; @@ -296,60 +297,62 @@ return ( - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - {navigation} - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + {navigation} + + + + + + + + + + + + + + + + + ); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -3,6 +3,7 @@ import identityServiceConfig from 'lib/facts/identity-service.js'; import type { IdentityServiceAuthLayer, + IdentityServiceClient, OutboundKeyInfoResponse, } from 'lib/types/identity-service-types.js'; @@ -10,25 +11,31 @@ import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs'; import * as IdentityAuthStructs from '../protobufs/identity-auth-structs.cjs'; import { Empty } from '../protobufs/identity-unauth-structs.cjs'; -import * as IdentityClient from '../protobufs/identity-unauth.cjs'; +import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; -class IdentityServiceClientWrapper { +class IdentityServiceClientWrapper implements IdentityServiceClient { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; - unauthorizedClient: ?IdentityClient.IdentityClientServicePromiseClient; + unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; - constructor() { - this.authClient = null; - this.unauthorizedClient = null; + constructor(authLayer: ?IdentityServiceAuthLayer) { + if (authLayer) { + this.authClient = + IdentityServiceClientWrapper.createAuthClient(authLayer); + } + this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(); } - determineSocketAddr(): string { + static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } - async initAuthClient(authLayer: IdentityServiceAuthLayer): Promise { + static createAuthClient( + authLayer: IdentityServiceAuthLayer, + ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; - const identitySocketAddr = this.determineSocketAddr(); + const identitySocketAddr = + IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const authInterceptor = new AuthInterceptor( @@ -41,103 +48,72 @@ unaryInterceptors: [versionInterceptor, authInterceptor], }; - this.authClient = new IdentityAuthClient.IdentityClientServicePromiseClient( + return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } - async initUnauthorizedClient(): Promise { - const identitySocketAddr = this.determineSocketAddr(); + static createUnauthClient(): IdentityUnauthClient.IdentityClientServicePromiseClient { + const identitySocketAddr = + IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); - const unauthorizedClientOpts = { + const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; - this.unauthorizedClient = - new IdentityClient.IdentityClientServicePromiseClient( - identitySocketAddr, - null, - unauthorizedClientOpts, - ); + return new IdentityUnauthClient.IdentityClientServicePromiseClient( + identitySocketAddr, + null, + unauthClientOpts, + ); } - deleteUser: ( - userID: string, - deviceID: string, - accessToken: string, - ) => Promise = async ( - userID: string, - deviceID: string, - accessToken: string, - ): Promise => { + deleteUser: () => Promise = async () => { if (!this.authClient) { - const authLayer: IdentityServiceAuthLayer = { - userID, - deviceID, - commServicesAccessToken: accessToken, - }; - await this.initAuthClient(authLayer); - } - - if (this.authClient) { - await this.authClient.deleteUser(new Empty()); - } else { throw new Error('Identity service client is not initialized'); } + await this.authClient.deleteUser(new Empty()); }; - async getKeyserverKeys( - userID: string, - deviceID: string, - accessToken: string, - keyserverID: string, - ): Promise { - if (!this.authClient) { - const authLayer: IdentityServiceAuthLayer = { - userID, - deviceID, - commServicesAccessToken: accessToken, + getKeyserverKeys: (keyserverID: string) => Promise = + async (keyserverID: string) => { + const client = this.authClient; + if (!client) { + throw new Error('Identity service client is not initialized'); + } + + const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); + request.setUserId(keyserverID); + const response = await client.getKeyserverKeys(request); + const keyserverInfo = response.getKeyserverInfo(); + if (!response.hasKeyserverInfo() || !keyserverInfo) { + return null; + } + + const identityInfo = keyserverInfo.getIdentityInfo(); + const contentPreKey = keyserverInfo.getContentPrekey(); + const notifPreKey = keyserverInfo.getNotifPrekey(); + + if (!identityInfo || !contentPreKey || !notifPreKey) { + return null; + } + + return { + payload: identityInfo.getPayload(), + payloadSignature: identityInfo.getPayloadSignature(), + socialProof: identityInfo.getSocialProof(), + contentPrekey: contentPreKey.getPrekey(), + contentPrekeySignature: contentPreKey.getPrekeySignature(), + notifPrekey: notifPreKey.getPrekey(), + notifPrekeySignature: notifPreKey.getPrekeySignature(), + oneTimeContentPrekey: keyserverInfo.getOneTimeContentPrekey(), + oneTimeNotifPrekey: keyserverInfo.getOneTimeNotifPrekey(), }; - await this.initAuthClient(authLayer); - } - - const client = this.authClient; - if (!client) { - throw new Error('Identity service client is not initialized'); - } - - const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); - request.setUserId(keyserverID); - const response = await client.getKeyserverKeys(request); - const keyserverInfo = response.getKeyserverInfo(); - if (!response.hasKeyserverInfo() || !keyserverInfo) { - return null; - } - - const identityInfo = keyserverInfo.getIdentityInfo(); - const contentPreKey = keyserverInfo.getContentPrekey(); - const notifPreKey = keyserverInfo.getNotifPrekey(); - - if (!identityInfo || !contentPreKey || !notifPreKey) { - return null; - } - - return { - payload: identityInfo.getPayload(), - payloadSignature: identityInfo.getPayloadSignature(), - socialProof: identityInfo.getSocialProof(), - contentPrekey: contentPreKey.getPrekey(), - contentPrekeySignature: contentPreKey.getPrekeySignature(), - notifPrekey: notifPreKey.getPrekey(), - notifPrekeySignature: notifPreKey.getPrekeySignature(), - oneTimeContentPrekey: keyserverInfo.getOneTimeContentPrekey(), - oneTimeNotifPrekey: keyserverInfo.getOneTimeNotifPrekey(), }; - } } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js new file mode 100644 --- /dev/null +++ b/web/grpc/identity-service-context-provider.react.js @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { IdentityServiceClient } from 'lib/types/identity-service-types.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 [client, setClient] = React.useState(); + + const userID = useSelector(state => state.currentUserInfo?.id); + const accessToken = useSelector(state => state.commServicesAccessToken); + const deviceID = useSelector( + state => state.cryptoStore?.primaryIdentityKeys.ed25519, + ); + + React.useEffect(() => { + let authLayer = null; + if (userID && deviceID && accessToken) { + authLayer = { + userID, + deviceID, + commServicesAccessToken: accessToken, + }; + } + setClient(new IdentityServiceClientWrapper(authLayer)); + }, [accessToken, deviceID, userID]); + + const value = React.useMemo( + () => ({ + identityClient: client, + }), + [client], + ); + + return ( + + {children} + + ); +} + +export default IdentityServiceContextProvider; diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -21,6 +21,7 @@ import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; import ErrorBoundary from './error-boundary.react.js'; +import IdentityServiceContextProvider from './grpc/identity-service-context-provider.react.js'; import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; @@ -43,14 +44,16 @@ - - - - - - - - + + + + + + + + + + diff --git a/web/settings/account-delete-modal.react.js b/web/settings/account-delete-modal.react.js --- a/web/settings/account-delete-modal.react.js +++ b/web/settings/account-delete-modal.react.js @@ -17,7 +17,6 @@ import css from './account-delete-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; -import { IdentityServiceClientWrapper } from '../grpc/identity-service-client-wrapper.js'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -40,17 +39,6 @@ const inputDisabled = isDeleteKeyserverAccountLoading || isDeleteIdentityAccountLoading; - const deviceID = useSelector( - state => state.cryptoStore?.primaryIdentityKeys.ed25519, - ); - - const identityServiceClientWrapperRef = - React.useRef(null); - if (!identityServiceClientWrapperRef.current) { - identityServiceClientWrapperRef.current = - new IdentityServiceClientWrapper(); - } - const identityServiceClient = identityServiceClientWrapperRef.current; const callDeleteIdentityAccount = useDeleteIdentityAccount(); const callDeleteKeyserverAccount = useDeleteKeyserverAccount(); @@ -98,10 +86,7 @@ const deleteIdentityAction = React.useCallback(async () => { try { setIdentityErrorMessage(''); - const response = await callDeleteIdentityAccount( - identityServiceClient, - deviceID, - ); + const response = await callDeleteIdentityAccount(); popModal(); return response; } catch (e) { @@ -110,7 +95,7 @@ ); throw e; } - }, [callDeleteIdentityAccount, deviceID, identityServiceClient, popModal]); + }, [callDeleteIdentityAccount, popModal]); const onDelete = React.useCallback( (event: SyntheticEvent) => {