diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 61cf9372e..e4a56131c 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,255 +1,255 @@ // @flow import t, { type TInterface, type TList, type TDict } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { currentUserInfoValidator, type CurrentUserInfo, } from './user-types.js'; import { tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating OutboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; // This type should not be altered without also updating InboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, }); export type UserDevicesOlmOutboundKeys = { +deviceID: string, +keys: ?DeviceOlmOutboundKeys, }; export type DeviceOlmInboundKeys = { +identityKeysBlob: IdentityKeysBlob, +signedPrekeys: SignedPrekeys, +payloadSignature: string, }; export const deviceOlmInboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, signedPrekeys: signedPrekeysValidator, payloadSignature: t.String, }); export type UserDevicesOlmInboundKeys = { +keys: { +[deviceID: string]: ?DeviceOlmInboundKeys, }, +username?: ?string, +walletAddress?: ?string, }; // This type should not be altered without also updating FarcasterUser in // keyserver/addons/rust-node-addon/src/identity_client/get_farcaster_users.rs export type FarcasterUser = { +userID: string, +username: string, +farcasterID: string, }; export const farcasterUserValidator: TInterface = tShape({ userID: t.String, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface = tShape({ keys: t.dict(t.String, t.maybe(deviceOlmInboundKeysValidator)), username: t.maybe(t.String), walletAddress: t.maybe(t.String), }); export interface IdentityServiceClient { // Only a primary device can initiate account deletion, and web cannot be a // primary device +deleteWalletUser?: () => Promise; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise; +logOut: () => Promise; +getKeyserverKeys: string => Promise; +registerPasswordUser?: ( username: string, password: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray>; - +getDeviceListsForUsers?: ( + +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise; // updating device list is possible only on Native // web cannot be a primary device, so there's no need to expose it to JS +updateDeviceList?: (newDeviceList: SignedDeviceList) => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface = tShape({ userID: t.String, accessToken: t.String, username: t.String, preRequestUserState: t.maybe(currentUserInfoValidator), }); export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict = t.dict(t.String, signedDeviceListValidator); export type SignedNonce = { +nonce: string, +nonceSignature: string, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index 44972952e..f20d11ee0 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,696 +1,724 @@ // @flow import { Login } from '@commapp/opaque-ke-wasm'; import identityServiceConfig from 'lib/facts/identity-service.js'; import type { OneTimeKeysResultValues, SignedPrekeys, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import { type SignedDeviceList, signedDeviceListHistoryValidator, type SignedNonce, type IdentityServiceAuthLayer, type IdentityServiceClient, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type UserDevicesOlmOutboundKeys, type IdentityAuthResult, type IdentityNewDeviceKeyUpload, type IdentityExistingDeviceKeyUpload, identityDeviceTypes, identityAuthResultValidator, type UserDevicesOlmInboundKeys, type DeviceOlmInboundKeys, deviceOlmInboundKeysValidator, userDeviceOlmInboundKeysValidator, type FarcasterUser, farcasterUsersValidator, + type UsersSignedDeviceLists, + usersSignedDeviceListsValidator, } from 'lib/types/identity-service-types.js'; import { getMessageForException } from 'lib/utils/errors.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 { DeviceKeyUpload, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, Prekey, WalletAuthRequest, SecondaryDeviceKeysUploadRequest, GetFarcasterUsersRequest, } from '../protobufs/identity-unauth-structs.cjs'; import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; import { initOpaque } from '../shared-worker/utils/opaque-utils.js'; class IdentityServiceClientWrapper implements IdentityServiceClient { overridedOpaqueFilepath: string; authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; getNewDeviceKeyUpload: () => Promise; getExistingDeviceKeyUpload: () => Promise; constructor( platformDetails: PlatformDetails, overridedOpaqueFilepath: string, authLayer: ?IdentityServiceAuthLayer, getNewDeviceKeyUpload: () => Promise, getExistingDeviceKeyUpload: () => Promise, ) { this.overridedOpaqueFilepath = overridedOpaqueFilepath; if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient( platformDetails, authLayer, ); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(platformDetails); this.getNewDeviceKeyUpload = getNewDeviceKeyUpload; this.getExistingDeviceKeyUpload = getExistingDeviceKeyUpload; } static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } static createAuthClient( platformDetails: PlatformDetails, authLayer: IdentityServiceAuthLayer, ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } static createUnauthClient( platformDetails: PlatformDetails, ): IdentityUnauthClient.IdentityClientServicePromiseClient { const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } logOut: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutUser(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(); 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(), }; 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: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getOutboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: (?UserDevicesOlmOutboundKeys)[] = devicesMap.map( ([deviceID, outboundKeysInfo]) => { const identityInfo = outboundKeysInfo?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = outboundKeysInfo?.contentPrekey; const notifPreKey = outboundKeysInfo?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return null; } if ( !outboundKeysInfo.oneTimeContentPrekey || !outboundKeysInfo.oneTimeNotifPrekey ) { console.log(`Missing one time key for device ${deviceID}`); return { deviceID, keys: null, }; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.prekey, prekeySignature: contentPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: notifPreKey?.prekey, prekeySignature: notifPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeNotifPrekey, }, payloadSignature: identityInfo?.payloadSignature, }; try { const validatedKeys = assertWithValidator( deviceKeys, deviceOlmOutboundKeysValidator, ); return { deviceID, keys: validatedKeys, }; } catch (e) { console.log(e); return { deviceID, keys: null, }; } }, ); return devicesKeys.filter(Boolean); }; getInboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.InboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getInboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: { [deviceID: string]: ?DeviceOlmInboundKeys, } = {}; devicesMap.forEach(([deviceID, inboundKeys]) => { const identityInfo = inboundKeys?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = inboundKeys?.contentPrekey; const notifPreKey = inboundKeys?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, signedPrekeys: { contentPrekey: contentPreKey?.prekey, contentPrekeySignature: contentPreKey?.prekeySignature, notifPrekey: notifPreKey?.prekey, notifPrekeySignature: notifPreKey?.prekeySignature, }, payloadSignature: identityInfo?.payloadSignature, }; try { devicesKeys[deviceID] = assertWithValidator( deviceKeys, deviceOlmInboundKeysValidator, ); } catch (e) { console.log(e); devicesKeys[deviceID] = null; } }); const identityInfo = response?.getIdentity(); const inboundUserKeys = { keys: devicesKeys, username: identityInfo?.getUsername(), walletAddress: identityInfo?.getEthIdentity()?.getWalletAddress(), }; return assertWithValidator( inboundUserKeys, userDeviceOlmInboundKeysValidator, ); }; uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise = async (oneTimeKeys: OneTimeKeysResultValues) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentOneTimeKeysArray = [...oneTimeKeys.contentOneTimeKeys]; const notifOneTimeKeysArray = [...oneTimeKeys.notificationsOneTimeKeys]; const request = new IdentityAuthStructs.UploadOneTimeKeysRequest(); request.setContentOneTimePrekeysList(contentOneTimeKeysArray); request.setNotifOneTimePrekeysList(notifOneTimeKeysArray); await client.uploadOneTimeKeys(request); }; logInPasswordUser: ( username: string, password: string, ) => Promise = async ( username: string, password: string, ) => { const client = this.unauthClient; if (!client) { throw new Error('Identity service client is not initialized'); } const [identityDeviceKeyUpload] = await Promise.all([ this.getExistingDeviceKeyUpload(), initOpaque(this.overridedOpaqueFilepath), ]); const opaqueLogin = new Login(); const startRequestBytes = opaqueLogin.start(password); const deviceKeyUpload = authExistingDeviceKeyUpload( identityDeviceKeyUpload, ); const loginStartRequest = new OpaqueLoginStartRequest(); loginStartRequest.setUsername(username); loginStartRequest.setOpaqueLoginRequest(startRequestBytes); loginStartRequest.setDeviceKeyUpload(deviceKeyUpload); let loginStartResponse; try { loginStartResponse = await client.logInPasswordUserStart(loginStartRequest); } catch (e) { console.log( 'Error calling logInPasswordUserStart:', getMessageForException(e) ?? 'unknown', ); throw e; } const finishRequestBytes = opaqueLogin.finish( loginStartResponse.getOpaqueLoginResponse_asU8(), ); const loginFinishRequest = new OpaqueLoginFinishRequest(); loginFinishRequest.setSessionId(loginStartResponse.getSessionId()); loginFinishRequest.setOpaqueLoginUpload(finishRequestBytes); let loginFinishResponse; try { loginFinishResponse = await client.logInPasswordUserFinish(loginFinishRequest); } catch (e) { console.log( 'Error calling logInPasswordUserFinish:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = loginFinishResponse.getUserId(); const accessToken = loginFinishResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise = async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { const identityDeviceKeyUpload = await this.getExistingDeviceKeyUpload(); const deviceKeyUpload = authExistingDeviceKeyUpload( identityDeviceKeyUpload, ); const loginRequest = new WalletAuthRequest(); loginRequest.setSiweMessage(siweMessage); loginRequest.setSiweSignature(siweSignature); loginRequest.setDeviceKeyUpload(deviceKeyUpload); let loginResponse; try { loginResponse = await this.unauthClient.logInWalletUser(loginRequest); } catch (e) { console.log( 'Error calling logInWalletUser:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = loginResponse.getUserId(); const accessToken = loginResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username: walletAddress }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; uploadKeysForRegisteredDeviceAndLogIn: ( ownerUserID: string, nonceChallengeResponse: SignedNonce, ) => Promise = async ( ownerUserID, nonceChallengeResponse, ) => { const identityDeviceKeyUpload = await this.getNewDeviceKeyUpload(); const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); const { nonce, nonceSignature } = nonceChallengeResponse; const request = new SecondaryDeviceKeysUploadRequest(); request.setUserId(ownerUserID); request.setNonce(nonce); request.setNonceSignature(nonceSignature); request.setDeviceKeyUpload(deviceKeyUpload); let response; try { response = await this.unauthClient.uploadKeysForRegisteredDeviceAndLogIn(request); } catch (e) { console.log( 'Error calling uploadKeysForRegisteredDeviceAndLogIn:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = response.getUserId(); const accessToken = response.getAccessToken(); const identityAuthResult = { accessToken, userID, username: '' }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; generateNonce: () => Promise = async () => { const result = await this.unauthClient.generateNonce(new Empty()); return result.getNonce(); }; publishWebPrekeys: (prekeys: SignedPrekeys) => Promise = async ( prekeys: SignedPrekeys, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentPrekeyUpload = new Prekey(); contentPrekeyUpload.setPrekey(prekeys.contentPrekey); contentPrekeyUpload.setPrekeySignature(prekeys.contentPrekeySignature); const notifPrekeyUpload = new Prekey(); notifPrekeyUpload.setPrekey(prekeys.notifPrekey); notifPrekeyUpload.setPrekeySignature(prekeys.notifPrekeySignature); const request = new IdentityAuthStructs.RefreshUserPrekeysRequest(); request.setNewContentPrekeys(contentPrekeyUpload); request.setNewNotifPrekeys(notifPrekeyUpload); await client.refreshUserPrekeys(request); }; getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray> = async ( userID, sinceTimestamp, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.GetDeviceListRequest(); request.setUserId(userID); if (sinceTimestamp) { request.setSinceTimestamp(sinceTimestamp); } const response = await client.getDeviceListForUser(request); const rawPayloads = response.getDeviceListUpdatesList(); const deviceListUpdates: SignedDeviceList[] = rawPayloads.map(payload => JSON.parse(payload), ); return assertWithValidator( deviceListUpdates, signedDeviceListHistoryValidator, ); }; + getDeviceListsForUsers: ( + userIDs: $ReadOnlyArray, + ) => Promise = async userIDs => { + const client = this.authClient; + if (!client) { + throw new Error('Identity service client is not initialized'); + } + const request = new IdentityAuthStructs.PeersDeviceListsRequest(); + request.setUserIdsList([...userIDs]); + const response = await client.getDeviceListsForUsers(request); + const rawPayloads = response.toObject()?.usersDeviceListsMap; + + let usersDeviceLists: UsersSignedDeviceLists = {}; + rawPayloads.forEach(([userID, rawPayload]) => { + usersDeviceLists = { + ...usersDeviceLists, + [userID]: JSON.parse(rawPayload), + }; + }); + + return assertWithValidator( + usersDeviceLists, + usersSignedDeviceListsValidator, + ); + }; + getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = async farcasterIDs => { const getFarcasterUsersRequest = new GetFarcasterUsersRequest(); getFarcasterUsersRequest.setFarcasterIdsList([...farcasterIDs]); let getFarcasterUsersResponse; try { getFarcasterUsersResponse = await this.unauthClient.getFarcasterUsers( getFarcasterUsersRequest, ); } catch (e) { console.log( 'Error calling getFarcasterUsers:', getMessageForException(e) ?? 'unknown', ); throw e; } const farcasterUsersList = getFarcasterUsersResponse.getFarcasterUsersList(); const returnList = []; for (const user of farcasterUsersList) { returnList.push({ userID: user.getUserId(), username: user.getUsername(), farcasterID: user.getFarcasterId(), }); } return assertWithValidator(returnList, farcasterUsersValidator); }; linkFarcasterAccount: (farcasterID: string) => Promise = async farcasterID => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const linkFarcasterAccountRequest = new IdentityAuthStructs.LinkFarcasterAccountRequest(); linkFarcasterAccountRequest.setFarcasterId(farcasterID); await client.linkFarcasterAccount(linkFarcasterAccountRequest); }; unlinkFarcasterAccount: () => Promise = async () => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await client.unlinkFarcasterAccount(new Empty()); }; } function authNewDeviceKeyUpload( uploadData: IdentityNewDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, contentOneTimeKeys, notifOneTimeKeys, } = uploadData; const identityKeyInfo = createIdentityKeyInfo( keyPayload, keyPayloadSignature, ); const contentPrekeyUpload = createPrekey( contentPrekey, contentPrekeySignature, ); const notifPrekeyUpload = createPrekey(notifPrekey, notifPrekeySignature); const deviceKeyUpload = createDeviceKeyUpload( identityKeyInfo, contentPrekeyUpload, notifPrekeyUpload, contentOneTimeKeys, notifOneTimeKeys, ); return deviceKeyUpload; } function authExistingDeviceKeyUpload( uploadData: IdentityExistingDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, } = uploadData; const identityKeyInfo = createIdentityKeyInfo( keyPayload, keyPayloadSignature, ); const contentPrekeyUpload = createPrekey( contentPrekey, contentPrekeySignature, ); const notifPrekeyUpload = createPrekey(notifPrekey, notifPrekeySignature); const deviceKeyUpload = createDeviceKeyUpload( identityKeyInfo, contentPrekeyUpload, notifPrekeyUpload, ); return deviceKeyUpload; } function createIdentityKeyInfo( keyPayload: string, keyPayloadSignature: string, ): IdentityKeyInfo { const identityKeyInfo = new IdentityKeyInfo(); identityKeyInfo.setPayload(keyPayload); identityKeyInfo.setPayloadSignature(keyPayloadSignature); return identityKeyInfo; } function createPrekey(prekey: string, prekeySignature: string): Prekey { const prekeyUpload = new Prekey(); prekeyUpload.setPrekey(prekey); prekeyUpload.setPrekeySignature(prekeySignature); return prekeyUpload; } function createDeviceKeyUpload( identityKeyInfo: IdentityKeyInfo, contentPrekeyUpload: Prekey, notifPrekeyUpload: Prekey, contentOneTimeKeys: $ReadOnlyArray = [], notifOneTimeKeys: $ReadOnlyArray = [], ): DeviceKeyUpload { const deviceKeyUpload = new DeviceKeyUpload(); deviceKeyUpload.setDeviceKeyInfo(identityKeyInfo); deviceKeyUpload.setContentUpload(contentPrekeyUpload); deviceKeyUpload.setNotifUpload(notifPrekeyUpload); deviceKeyUpload.setOneTimeContentPrekeysList([...contentOneTimeKeys]); deviceKeyUpload.setOneTimeNotifPrekeysList([...notifOneTimeKeys]); deviceKeyUpload.setDeviceType(identityDeviceTypes.WEB); return deviceKeyUpload; } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js index 6dec5c8bd..edc78f086 100644 --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -1,158 +1,159 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { IdentityClientContext, type AuthMetadata, } from 'lib/shared/identity-client-context.js'; import type { IdentityServiceClient, IdentityServiceAuthLayer, } from 'lib/types/identity-service-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { getOpaqueWasmPath } from '../shared-worker/utils/constants.js'; import { workerRequestMessageTypes, workerResponseMessageTypes, } from '../types/worker-types.js'; type CreateMethodWorkerProxy = ( method: $Keys, ) => (...args: $ReadOnlyArray) => Promise; 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 getAuthMetadata = React.useCallback< () => Promise, >(async () => { const contentSigningKey = await getContentSigningKey(); return { userID, deviceID: contentSigningKey, accessToken, }; }, [accessToken, userID]); const workerClientAuthMetadata = React.useRef(null); const ensureThatWorkerClientAuthMetadataIsCurrent = React.useCallback(async () => { const [sharedWorker, authMetadata] = await Promise.all([ getCommSharedWorker(), getAuthMetadata(), ]); if (_isEqual(authMetadata, workerClientAuthMetadata.current)) { return; } workerClientAuthMetadata.current = authMetadata; let authLayer: ?IdentityServiceAuthLayer = null; if ( authMetadata.userID && authMetadata.deviceID && authMetadata.accessToken ) { authLayer = { userID: authMetadata.userID, deviceID: authMetadata.deviceID, commServicesAccessToken: authMetadata.accessToken, }; } await sharedWorker.schedule({ type: workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, opaqueWasmPath: getOpaqueWasmPath(), authLayer, }); }, [getAuthMetadata]); React.useEffect(() => { void ensureThatWorkerClientAuthMetadataIsCurrent(); }, [ensureThatWorkerClientAuthMetadataIsCurrent]); const proxyMethodToWorker: CreateMethodWorkerProxy = React.useCallback( method => async (...args: $ReadOnlyArray) => { await ensureThatWorkerClientAuthMetadataIsCurrent(); const sharedWorker = await getCommSharedWorker(); const result = await sharedWorker.schedule({ type: workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD, method, args, }); if (!result) { throw new Error( `Worker identity call didn't return expected message`, ); } else if ( result.type !== workerResponseMessageTypes.CALL_IDENTITY_CLIENT_METHOD ) { throw new Error( `Worker identity call didn't return expected message. Instead got: ${JSON.stringify( result, )}`, ); } // Worker should return a message with the corresponding return type return (result.result: any); }, [ensureThatWorkerClientAuthMetadataIsCurrent], ); const client = React.useMemo(() => { return { deleteWalletUser: proxyMethodToWorker('deleteWalletUser'), deletePasswordUser: proxyMethodToWorker('deletePasswordUser'), logOut: proxyMethodToWorker('logOut'), getKeyserverKeys: proxyMethodToWorker('getKeyserverKeys'), getOutboundKeysForUser: proxyMethodToWorker('getOutboundKeysForUser'), getInboundKeysForUser: proxyMethodToWorker('getInboundKeysForUser'), uploadOneTimeKeys: proxyMethodToWorker('uploadOneTimeKeys'), logInPasswordUser: proxyMethodToWorker('logInPasswordUser'), logInWalletUser: proxyMethodToWorker('logInWalletUser'), uploadKeysForRegisteredDeviceAndLogIn: proxyMethodToWorker( 'uploadKeysForRegisteredDeviceAndLogIn', ), generateNonce: proxyMethodToWorker('generateNonce'), publishWebPrekeys: proxyMethodToWorker('publishWebPrekeys'), getDeviceListHistoryForUser: proxyMethodToWorker( 'getDeviceListHistoryForUser', ), + getDeviceListsForUsers: proxyMethodToWorker('getDeviceListsForUsers'), getFarcasterUsers: proxyMethodToWorker('getFarcasterUsers'), linkFarcasterAccount: proxyMethodToWorker('linkFarcasterAccount'), unlinkFarcasterAccount: proxyMethodToWorker('unlinkFarcasterAccount'), }; }, [proxyMethodToWorker]); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider;