diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 0b50f26fd..b2ad222fc 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,88 +1,98 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tShape } from '../utils/validation-utils.js'; export type OLMIdentityKeys = { +ed25519: string, +curve25519: string, }; +const olmIdentityKeysValidator: TInterface = + tShape({ + ed25519: t.String, + curve25519: t.String, + }); export type OLMPrekey = { +curve25519: { +id: string, +key: string, }, }; export type OLMOneTimeKeys = { +curve25519: { +[string]: string }, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type CryptoStore = { +primaryAccount: PickledOLMAccount, +primaryIdentityKeys: OLMIdentityKeys, +notificationAccount: PickledOLMAccount, +notificationIdentityKeys: OLMIdentityKeys, }; export type CryptoStoreContextType = { +getInitializedCryptoStore: () => Promise, }; export type NotificationsOlmDataType = { +mainSession: string, +picklingKey: string, +pendingSessionUpdate: string, +updateCreationTimestamp: number, }; export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, }; +export const identityKeysBlobValidator: TInterface = + tShape({ + primaryIdentityPublicKeys: olmIdentityKeysValidator, + notificationIdentityPublicKeys: olmIdentityKeysValidator, + }); export type SignedIdentityKeysBlob = { +payload: string, +signature: string, }; export const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); export type UserDetail = { +username: string, +userID: string, }; // This type should not be changed without making equivalent changes to // `Message` in Identity service's `reserved_users` module export type ReservedUsernameMessage = | { +statement: 'Add the following usernames to reserved list', +payload: $ReadOnlyArray, +issuedAt: string, } | { +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, } | { +statement: 'This user is the owner of the following username and user ID', +payload: UserDetail, +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ PREKEY: 0, TEXT: 1, }); diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 6ea2031ee..1573964d6 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,57 +1,85 @@ // @flow +import t, { type TInterface } from 'tcomb'; + +import { + identityKeysBlobValidator, + type IdentityKeysBlob, +} from './crypto-types.js'; +import { + type OlmSessionInitializationInfo, + olmSessionInitializationInfoValidator, +} from './request-types.js'; +import { tShape } from '../utils/validation-utils.js'; + export type UserLoginResponse = { +userId: string, +accessToken: string, }; // This type should not be altered without also updating // OutboundKeyInfoResponse in native/native_rust_library/src/lib.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; +export type KeyserverKeys = { + +identityKeysBlob: IdentityKeysBlob, + +contentInitializationInfo: OlmSessionInitializationInfo, + +notifInitializationInfo: OlmSessionInitializationInfo, + +payloadSignature: string, + +socialProof: ?string, +}; +export const keyserverKeysValidator: TInterface = + tShape({ + identityKeysBlob: identityKeysBlobValidator, + contentInitializationInfo: olmSessionInitializationInfoValidator, + notifInitializationInfo: olmSessionInitializationInfoValidator, + payloadSignature: t.String, + socialProof: t.maybe(t.String), + }); + export interface IdentityServiceClient { +deleteUser: () => Promise; - +getKeyserverKeys: string => Promise; + +getKeyserverKeys: string => Promise; +registerUser?: ( username: string, password: string, ) => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; // This type should not be altered without also updating // InboundKeyInfoResponse in native/native_rust_library/src/lib.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof?: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type IdentityRegisterResult = { +userID: string, +accessToken: string, +username: string, }; export const ONE_TIME_KEYS_NUMBER = 10; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js index 419853887..70353d9ad 100644 --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -1,138 +1,155 @@ // @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 { - IdentityServiceClient, - OutboundKeyInfoResponse, - UserLoginResponse, +import { + type IdentityServiceClient, + type UserLoginResponse, + type KeyserverKeys, + keyserverKeysValidator, } from 'lib/types/identity-service-types.js'; import { ONE_TIME_KEYS_NUMBER } 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) => { + getKeyserverKeys: async (keyserverID: string): Promise => { 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'); + 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'); } - return resultObject; + if (!keyserverKeys.notifInitializationInfo.oneTimeKey) { + throw new Error('Missing notif one time key'); + } + + return assertWithValidator(keyserverKeys, keyserverKeysValidator); }, 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, }), [client], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index ecc6025b5..5dcf01b05 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,119 +1,129 @@ // @flow import identityServiceConfig from 'lib/facts/identity-service.js'; -import type { - IdentityServiceAuthLayer, - IdentityServiceClient, - OutboundKeyInfoResponse, +import { + type IdentityServiceAuthLayer, + type IdentityServiceClient, + type KeyserverKeys, + keyserverKeysValidator, } from 'lib/types/identity-service-types.js'; +import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { VersionInterceptor, AuthInterceptor } from './interceptor.js'; 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 IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; class IdentityServiceClientWrapper implements IdentityServiceClient { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; constructor(authLayer: ?IdentityServiceAuthLayer) { if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient(authLayer); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(); } static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } static createAuthClient( authLayer: IdentityServiceAuthLayer, ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } static createUnauthClient(): IdentityUnauthClient.IdentityClientServicePromiseClient { const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } deleteUser: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } 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(), - }; + 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(); + const identityInfo = keyserverInfo?.getIdentityInfo(); + const contentPreKey = keyserverInfo?.getContentPrekey(); + const notifPreKey = keyserverInfo?.getNotifPrekey(); + const payload = identityInfo?.getPayload(); + + const keyserverKeys = { + identityKeysBlob: payload ? JSON.parse(payload) : null, + contentInitializationInfo: { + prekey: contentPreKey?.getPrekey(), + prekeySignature: contentPreKey?.getPrekeySignature(), + oneTimeKey: keyserverInfo?.getOneTimeContentPrekey(), + }, + notifInitializationInfo: { + prekey: notifPreKey?.getPrekey(), + prekeySignature: notifPreKey?.getPrekeySignature(), + oneTimeKey: keyserverInfo?.getOneTimeNotifPrekey(), + }, + payloadSignature: identityInfo?.getPayloadSignature(), + socialProof: identityInfo?.getSocialProof(), }; + + 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, keyserverKeysValidator); + }; } export { IdentityServiceClientWrapper };