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 @@ -24,3 +24,8 @@ +deviceID: string, +commServicesAccessToken: string, }; + +export interface IdentityServiceClient { + +deleteUser: () => Promise, + +getKeyserverKeys: (string) => Promise, +} 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,76 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { + IdentityServiceClient, + OutboundKeyInfoResponse, +} from 'lib/types/identity-service-types.js'; + +import { commRustModule } from '../native-modules.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { getContentSigningKey } from '../utils/crypto-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, setDeviceID] = React.useState(); + + React.useEffect(() => { + void (async () => { + const contentSigningKey = await getContentSigningKey(); + setDeviceID(contentSigningKey); + })(); + }, []); + + const client = React.useMemo(() => { + if (!userID || !deviceID || !accessToken) { + return null; + } + return { + deleteUser: () => + commRustModule.deleteUser(userID, deviceID, accessToken), + getKeyserverKeys: async (keyserverID: string) => { + 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 + ) { + return null; + } + return resultObject; + }, + }; + }, [accessToken, deviceID, userID]); + + const value = React.useMemo( + () => ({ + identityClient: client, + }), + [client], + ); + + return ( + + {children} + + ); +} + +export default IdentityServiceContextProvider; 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 ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; +import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; @@ -295,58 +296,60 @@ 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'; @@ -12,23 +13,30 @@ import * as IdentityClient from '../protobufs/identity-client.cjs'; import { Empty } from '../protobufs/identity-structs.cjs'; -class IdentityServiceClientWrapper { +class IdentityServiceClientWrapper implements IdentityServiceClient { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; - unauthorizedClient: ?IdentityClient.IdentityClientServicePromiseClient; + unauthorizedClient: IdentityClient.IdentityClientServicePromiseClient; - constructor() { - this.authClient = null; - this.unauthorizedClient = null; + constructor(authLayer: ?IdentityServiceAuthLayer) { + if (authLayer) { + this.authClient = + IdentityServiceClientWrapper.createAuthClient(authLayer); + } + this.unauthorizedClient = + IdentityServiceClientWrapper.createUnauthorizedClient(); } - 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,15 +49,16 @@ unaryInterceptors: [versionInterceptor, authInterceptor], }; - this.authClient = new IdentityAuthClient.IdentityClientServicePromiseClient( + return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } - async initUnauthorizedClient(): Promise { - const identitySocketAddr = this.determineSocketAddr(); + static createUnauthorizedClient(): IdentityClient.IdentityClientServicePromiseClient { + const identitySocketAddr = + IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); @@ -57,83 +66,55 @@ unaryInterceptors: [versionInterceptor], }; - this.unauthorizedClient = - new IdentityClient.IdentityClientServicePromiseClient( - identitySocketAddr, - null, - unauthorizedClientOpts, - ); + return new IdentityClient.IdentityClientServicePromiseClient( + identitySocketAddr, + null, + unauthorizedClientOpts, + ); } - async deleteUser( - 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'); } - } - - async getKeyserverKeys( - userID: string, - deviceID: string, - accessToken: string, - keyserverID: string, - ): Promise { - if (!this.authClient) { - const authLayer: IdentityServiceAuthLayer = { - userID, - deviceID, - commServicesAccessToken: accessToken, + await this.authClient.deleteUser(new Empty()); + }; + + 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 @@ - - - - - - - - + + + + + + + + + +