diff --git a/lib/hooks/peer-list-hooks.js b/lib/hooks/peer-list-hooks.js index 2e59cd130..e8f968c90 100644 --- a/lib/hooks/peer-list-hooks.js +++ b/lib/hooks/peer-list-hooks.js @@ -1,40 +1,43 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; import { getRelativeUserIDs } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { convertSignedDeviceListsToRawDeviceLists } from '../utils/device-list-utils.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; function useCreateInitialPeerList(): () => Promise<void> { const dispatch = useDispatch(); const relativeUserIDs = useSelector(getRelativeUserIDs); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); return React.useCallback(async () => { if (!identityContext) { return; } try { - const userDeviceLists = + const peersDeviceLists = await identityContext.identityClient.getDeviceListsForUsers( relativeUserIDs, ); - const usersRawDeviceLists = - convertSignedDeviceListsToRawDeviceLists(userDeviceLists); + const usersRawDeviceLists = convertSignedDeviceListsToRawDeviceLists( + peersDeviceLists.usersSignedDeviceLists, + ); + const usersPlatformDetails = peersDeviceLists.usersDevicesPlatformDetails; + dispatch({ type: setPeerDeviceListsActionType, - payload: { deviceLists: usersRawDeviceLists }, + payload: { deviceLists: usersRawDeviceLists, usersPlatformDetails }, }); } catch (e) { console.log(`Error creating initial peer list: ${e.message}`); } }, [dispatch, identityContext, relativeUserIDs]); } export { useCreateInitialPeerList }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index dc952d030..958bc347a 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,344 +1,365 @@ // @flow import t, { type TInterface, type TList, type TDict, type TEnums } 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 { values } from '../utils/objects.js'; import { tUserID, 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<DeviceOlmOutboundKeys> = tShape<DeviceOlmOutboundKeys>({ 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<DeviceOlmInboundKeys> = tShape<DeviceOlmInboundKeys>({ 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<FarcasterUser> = tShape<FarcasterUser>({ userID: tUserID, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList<Array<FarcasterUser>> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface<UserDevicesOlmInboundKeys> = tShape<UserDevicesOlmInboundKeys>({ 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<void>; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise<void>; +logOut: () => Promise<void>; +logOutSecondaryDevice: () => Promise<void>; +getKeyserverKeys: string => Promise<DeviceOlmOutboundKeys>; // Users cannot register from web +registerPasswordUser?: ( username: string, password: string, fid: ?string, ) => Promise<IdentityAuthResult>; // Users cannot register from web +registerReservedPasswordUser?: ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => Promise<IdentityAuthResult>; +logInPasswordUser: ( username: string, password: string, ) => Promise<IdentityAuthResult>; +getOutboundKeysForUser: ( userID: string, ) => Promise<UserDevicesOlmOutboundKeys[]>; +getInboundKeysForUser: ( userID: string, ) => Promise<UserDevicesOlmInboundKeys>; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise<void>; +generateNonce: () => Promise<string>; // Users cannot register from web +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise<IdentityAuthResult>; // Users cannot register from web +registerReservedWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, keyserverMessage: string, keyserverSignature: string, ) => Promise<IdentityAuthResult>; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise<IdentityAuthResult>; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise<void>; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray<SignedDeviceList>>; +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray<string>, - ) => Promise<UsersSignedDeviceLists>; + ) => Promise<PeersDeviceLists>; // 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<void>; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise<IdentityAuthResult>; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray<string>, ) => Promise<$ReadOnlyArray<FarcasterUser>>; +linkFarcasterAccount: (farcasterID: string) => Promise<void>; +unlinkFarcasterAccount: () => Promise<void>; +findUserIdentities: (userIDs: $ReadOnlyArray<string>) => Promise<Identities>; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface<IdentityAuthResult> = tShape<IdentityAuthResult>({ userID: tUserID, 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<string>, +notifOneTimeKeys: $ReadOnlyArray<string>, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray<string>, +timestamp: number, }; export const rawDeviceListValidator: TInterface<RawDeviceList> = tShape<RawDeviceList>({ devices: t.list(t.String), timestamp: t.Number, }); export type UsersRawDeviceLists = { +[userID: string]: RawDeviceList, }; // User Identity types export type EthereumIdentity = { walletAddress: string, siweMessage: string, siweSignature: string, }; export type Identity = { +username: string, +ethIdentity: ?EthereumIdentity, +farcasterID: ?string, }; export type Identities = { +[userID: string]: Identity, }; export const ethereumIdentityValidator: TInterface<EthereumIdentity> = tShape<EthereumIdentity>({ walletAddress: t.String, siweMessage: t.String, siweSignature: t.String, }); export const identityValidator: TInterface<Identity> = tShape<Identity>({ username: t.String, ethIdentity: t.maybe(ethereumIdentityValidator), farcasterID: t.maybe(t.String), }); export const identitiesValidator: TDict<Identities> = t.dict( t.String, identityValidator, ); export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, // Current primary device signature. Absent for Identity Service generated // device lists. +curPrimarySignature?: string, // Previous primary device signature. Present only if primary device // has changed since last update. +lastPrimarySignature?: string, }; export const signedDeviceListValidator: TInterface<SignedDeviceList> = tShape<SignedDeviceList>({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(t.String), }); export const signedDeviceListHistoryValidator: TList<Array<SignedDeviceList>> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict<UsersSignedDeviceLists> = 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, }); export type IdentityDeviceType = $Values<typeof identityDeviceTypes>; export const identityDeviceTypeValidator: TEnums = t.enums.of( values(identityDeviceTypes), ); export type IdentityPlatformDetails = { +deviceType: IdentityDeviceType, +codeVersion: number, +stateVersion?: number, +majorDesktopVersion?: number, }; export const identityPlatformDetailsValidator: TInterface<IdentityPlatformDetails> = tShape<IdentityPlatformDetails>({ deviceType: identityDeviceTypeValidator, codeVersion: t.Number, stateVersion: t.maybe(t.Number), majorDesktopVersion: t.maybe(t.Number), }); + +export type UserDevicesPlatformDetails = { + +[deviceID: string]: IdentityPlatformDetails, +}; +export const userDevicesPlatformDetailsValidator: TDict<UserDevicesPlatformDetails> = + t.dict(t.String, identityPlatformDetailsValidator); + +export type UsersDevicesPlatformDetails = { + +[userID: string]: UserDevicesPlatformDetails, +}; +export const usersDevicesPlatformDetailsValidator: TDict<UsersDevicesPlatformDetails> = + t.dict(t.String, userDevicesPlatformDetailsValidator); + +export type PeersDeviceLists = { + +usersSignedDeviceLists: UsersSignedDeviceLists, + +usersDevicesPlatformDetails: UsersDevicesPlatformDetails, +}; +export const peersDeviceListsValidator: TInterface<PeersDeviceLists> = tShape({ + usersSignedDeviceLists: usersSignedDeviceListsValidator, + usersDevicesPlatformDetails: usersDevicesPlatformDetailsValidator, +}); diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js index 2ea762dd4..12ed510e3 100644 --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -1,699 +1,706 @@ // @flow import * as React from 'react'; import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; import { createAndSignInitialDeviceList } from 'lib/shared/device-list-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { type IdentityKeysBlob, identityKeysBlobValidator, type OneTimeKeysResultValues, } from 'lib/types/crypto-types.js'; import { type DeviceOlmInboundKeys, deviceOlmInboundKeysValidator, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, farcasterUsersValidator, identityAuthResultValidator, type IdentityServiceClient, ONE_TIME_KEYS_NUMBER, type SignedDeviceList, signedDeviceListHistoryValidator, type SignedNonce, type UserAuthMetadata, userDeviceOlmInboundKeysValidator, type UserDevicesOlmInboundKeys, type UserDevicesOlmOutboundKeys, type UsersSignedDeviceLists, - usersSignedDeviceListsValidator, identitiesValidator, + type UsersDevicesPlatformDetails, + peersDeviceListsValidator, } from 'lib/types/identity-service-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.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 { useSelector } from '../redux/redux-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userIDPromiseRef = React.useRef<?Promise<?string>>(); if (!userIDPromiseRef.current) { userIDPromiseRef.current = (async () => { const { userID } = await commCoreModule.getCommServicesAuthMetadata(); return userID; })(); } React.useEffect(() => { const metadataEmitter = getCommServicesAuthMetadataEmitter(); const subscription = metadataEmitter.addListener( 'commServicesAuthMetadata', (authMetadata: UserAuthMetadata) => { userIDPromiseRef.current = Promise.resolve(authMetadata.userID); }, ); return () => subscription.remove(); }, []); const accessToken = useSelector(state => state.commServicesAccessToken); const getAuthMetadata = React.useCallback< () => Promise<{ +deviceID: string, +userID: string, +accessToken: string, }>, >(async () => { const deviceID = await getContentSigningKey(); const userID = await userIDPromiseRef.current; if (!deviceID || !userID || !accessToken) { throw new Error('Identity service client is not initialized'); } return { deviceID, userID, accessToken }; }, [accessToken]); const processAuthResult = async (authResult: string, deviceID: string) => { const { userID, accessToken: token, username } = JSON.parse(authResult); const identityAuthResult = { accessToken: token, userID, username, }; const validatedResult = assertWithValidator( identityAuthResult, identityAuthResultValidator, ); await commCoreModule.setCommServicesAuthMetadata( validatedResult.userID, deviceID, validatedResult.accessToken, ); return validatedResult; }; const client = React.useMemo<IdentityServiceClient>( () => ({ deleteWalletUser: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.deleteWalletUser(userID, deviceID, token); }, deletePasswordUser: async (password: string) => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.deletePasswordUser( userID, deviceID, token, password, ); }, logOut: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.logOut(userID, deviceID, token); }, logOutSecondaryDevice: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.logOutSecondaryDevice(userID, deviceID, token); }, getKeyserverKeys: async ( keyserverID: string, ): Promise<DeviceOlmOutboundKeys> => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getKeyserverKeys( userID, deviceID, token, 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, }; return assertWithValidator( keyserverKeys, deviceOlmOutboundKeysValidator, ); }, getOutboundKeysForUser: async ( targetUserID: string, ): Promise<UserDevicesOlmOutboundKeys[]> => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getOutboundKeysForUser( userID, authDeviceID, token, 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; 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, }; 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); }, getInboundKeysForUser: async ( targetUserID: string, ): Promise<UserDevicesOlmInboundKeys> => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getInboundKeysForUser( userID, authDeviceID, token, targetUserID, ); const resultArray = JSON.parse(result); const devicesKeys: { [deviceID: string]: ?DeviceOlmInboundKeys, } = {}; resultArray.forEach(inboundKeysInfo => { try { const payload = inboundKeysInfo?.payload; const identityKeysBlob: IdentityKeysBlob = assertWithValidator( payload ? JSON.parse(payload) : null, identityKeysBlobValidator, ); const deviceID = identityKeysBlob.primaryIdentityPublicKeys.ed25519; const deviceKeys = { identityKeysBlob, signedPrekeys: { contentPrekey: inboundKeysInfo?.contentPrekey, contentPrekeySignature: inboundKeysInfo?.contentPrekeySignature, notifPrekey: inboundKeysInfo?.notifPrekey, notifPrekeySignature: inboundKeysInfo?.notifPrekeySignature, }, payloadSignature: inboundKeysInfo?.payloadSignature, }; try { devicesKeys[deviceID] = assertWithValidator( deviceKeys, deviceOlmInboundKeysValidator, ); } catch (e) { console.log(e); devicesKeys[deviceID] = null; } } catch (e) { console.log(e); } }); const device = resultArray?.[0]; const inboundUserKeys = { keys: devicesKeys, username: device?.username, walletAddress: device?.walletAddress, }; return assertWithValidator( inboundUserKeys, userDeviceOlmInboundKeysValidator, ); }, uploadOneTimeKeys: async (oneTimeKeys: OneTimeKeysResultValues) => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); await commRustModule.uploadOneTimeKeys( userID, authDeviceID, token, oneTimeKeys.contentOneTimeKeys, oneTimeKeys.notificationsOneTimeKeys, ); }, registerPasswordUser: async ( username: string, password: string, fid: ?string, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const initialDeviceList = await createAndSignInitialDeviceList( primaryIdentityPublicKeys.ed25519, ); const registrationResult = await commRustModule.registerPasswordUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), fid ?? '', JSON.stringify(initialDeviceList), ); return await processAuthResult( registrationResult, primaryIdentityPublicKeys.ed25519, ); }, registerReservedPasswordUser: async ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const initialDeviceList = await createAndSignInitialDeviceList( primaryIdentityPublicKeys.ed25519, ); const registrationResult = await commRustModule.registerReservedPasswordUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), keyserverMessage, keyserverSignature, JSON.stringify(initialDeviceList), ); return await processAuthResult( registrationResult, primaryIdentityPublicKeys.ed25519, ); }, logInPasswordUser: async (username: string, password: string) => { await commCoreModule.initializeCryptoAccount(); const [{ blobPayload, signature, primaryIdentityPublicKeys }, prekeys] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.validateAndGetPrekeys(), ]); const loginResult = await commRustModule.logInPasswordUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, ); return await processAuthResult( loginResult, primaryIdentityPublicKeys.ed25519, ); }, registerWalletUser: async ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const initialDeviceList = await createAndSignInitialDeviceList( primaryIdentityPublicKeys.ed25519, ); const registrationResult = await commRustModule.registerWalletUser( siweMessage, siweSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), fid ?? '', JSON.stringify(initialDeviceList), ); return await processAuthResult( registrationResult, primaryIdentityPublicKeys.ed25519, ); }, registerReservedWalletUser: async ( walletAddress: string, siweMessage: string, siweSignature: string, keyserverMessage: string, keyserverSignature: string, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const initialDeviceList = await createAndSignInitialDeviceList( primaryIdentityPublicKeys.ed25519, ); const registrationResult = await commRustModule.registerReservedWalletUser( siweMessage, siweSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), keyserverMessage, keyserverSignature, JSON.stringify(initialDeviceList), ); return await processAuthResult( registrationResult, primaryIdentityPublicKeys.ed25519, ); }, logInWalletUser: async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { await commCoreModule.initializeCryptoAccount(); const [{ blobPayload, signature, primaryIdentityPublicKeys }, prekeys] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.validateAndGetPrekeys(), ]); const loginResult = await commRustModule.logInWalletUser( siweMessage, siweSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, ); return await processAuthResult( loginResult, primaryIdentityPublicKeys.ed25519, ); }, uploadKeysForRegisteredDeviceAndLogIn: async ( userID: string, nonceChallengeResponse: SignedNonce, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const { nonce, nonceSignature } = nonceChallengeResponse; const registrationResult = await commRustModule.uploadSecondaryDeviceKeysAndLogIn( userID, nonce, nonceSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), ); return await processAuthResult( registrationResult, primaryIdentityPublicKeys.ed25519, ); }, generateNonce: commRustModule.generateNonce, getDeviceListHistoryForUser: async ( userID: string, sinceTimestamp?: number, ) => { const { deviceID: authDeviceID, userID: authUserID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getDeviceListForUser( authUserID, authDeviceID, token, userID, sinceTimestamp, ); const rawPayloads: string[] = JSON.parse(result); const deviceLists: SignedDeviceList[] = rawPayloads.map(payload => JSON.parse(payload), ); return assertWithValidator( deviceLists, signedDeviceListHistoryValidator, ); }, getDeviceListsForUsers: async (userIDs: $ReadOnlyArray<string>) => { const { deviceID: authDeviceID, userID: authUserID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getDeviceListsForUsers( authUserID, authDeviceID, token, userIDs, ); - const rawPayloads: { +[userID: string]: string } = JSON.parse(result); + + const rawPayloads: { + +usersDeviceLists: { +[userID: string]: string }, + +usersDevicesPlatformDetails: UsersDevicesPlatformDetails, + } = JSON.parse(result); let usersDeviceLists: UsersSignedDeviceLists = {}; - for (const userID in rawPayloads) { + for (const userID in rawPayloads.usersDeviceLists) { usersDeviceLists = { ...usersDeviceLists, - [userID]: JSON.parse(rawPayloads[userID]), + [userID]: JSON.parse(rawPayloads.usersDeviceLists[userID]), }; } - return assertWithValidator( - usersDeviceLists, - usersSignedDeviceListsValidator, - ); + const peersDeviceLists = { + usersSignedDeviceLists: usersDeviceLists, + usersDevicesPlatformDetails: rawPayloads.usersDevicesPlatformDetails, + }; + + return assertWithValidator(peersDeviceLists, peersDeviceListsValidator); }, updateDeviceList: async (newDeviceList: SignedDeviceList) => { const { deviceID: authDeviceID, userID, accessToken: authAccessToken, } = await getAuthMetadata(); const payload = JSON.stringify(newDeviceList); await commRustModule.updateDeviceList( userID, authDeviceID, authAccessToken, payload, ); }, getFarcasterUsers: async (farcasterIDs: $ReadOnlyArray<string>) => { const farcasterUsersJSONString = await commRustModule.getFarcasterUsers(farcasterIDs); const farcasterUsers = JSON.parse(farcasterUsersJSONString); return assertWithValidator(farcasterUsers, farcasterUsersValidator); }, linkFarcasterAccount: async (farcasterID: string) => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.linkFarcasterAccount( userID, deviceID, token, farcasterID, ); }, unlinkFarcasterAccount: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.unlinkFarcasterAccount(userID, deviceID, token); }, findUserIdentities: async (userIDs: $ReadOnlyArray<string>) => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.findUserIdentities( userID, deviceID, token, userIDs, ); const identities = JSON.parse(result); return assertWithValidator(identities, identitiesValidator); }, }), [getAuthMetadata], ); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( <IdentityClientContext.Provider value={value}> {children} </IdentityClientContext.Provider> ); } export default IdentityServiceContextProvider; diff --git a/native/native_rust_library/src/identity/device_list.rs b/native/native_rust_library/src/identity/device_list.rs index 979fd6714..f3c760018 100644 --- a/native/native_rust_library/src/identity/device_list.rs +++ b/native/native_rust_library/src/identity/device_list.rs @@ -1,144 +1,205 @@ use grpc_clients::identity::get_auth_client; use grpc_clients::identity::protos::auth::{ - GetDeviceListRequest, PeersDeviceListsRequest, UpdateDeviceListRequest, + GetDeviceListRequest, PeersDeviceListsRequest, PeersDeviceListsResponse, + UpdateDeviceListRequest, }; +use std::collections::HashMap; +use super::PLATFORM_METADATA; use crate::identity::AuthInfo; use crate::utils::jsi_callbacks::{ handle_string_result_as_callback, handle_void_result_as_callback, }; use crate::{Error, IDENTITY_SOCKET_ADDR, RUNTIME}; - -use super::PLATFORM_METADATA; +use serde::Serialize; pub mod ffi { use super::*; pub fn get_device_list_for_user( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_id: String, since_timestamp: i64, promise_id: u32, ) { RUNTIME.spawn(async move { let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let since_timestamp = Option::from(since_timestamp).filter(|&t| t > 0); let result = get_device_list_for_user_helper(auth_info, user_id, since_timestamp) .await; handle_string_result_as_callback(result, promise_id); }); } pub fn get_device_lists_for_users( auth_user_id: String, auth_device_id: String, auth_access_token: String, user_ids: Vec<String>, promise_id: u32, ) { RUNTIME.spawn(async move { let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = get_device_lists_for_users_helper(auth_info, user_ids).await; handle_string_result_as_callback(result, promise_id); }); } pub fn update_device_list( auth_user_id: String, auth_device_id: String, auth_access_token: String, update_payload: String, promise_id: u32, ) { RUNTIME.spawn(async move { let auth_info = AuthInfo { access_token: auth_access_token, user_id: auth_user_id, device_id: auth_device_id, }; let result = update_device_list_helper(auth_info, update_payload).await; handle_void_result_as_callback(result, promise_id); }); } } async fn get_device_list_for_user_helper( auth_info: AuthInfo, user_id: String, since_timestamp: Option<i64>, ) -> Result<String, Error> { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, PLATFORM_METADATA.clone(), ) .await?; let response = identity_client .get_device_list_for_user(GetDeviceListRequest { user_id, since_timestamp, }) .await? .into_inner(); let payload = serde_json::to_string(&response.device_list_updates)?; Ok(payload) } async fn get_device_lists_for_users_helper( auth_info: AuthInfo, user_ids: Vec<String>, ) -> Result<String, Error> { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, PLATFORM_METADATA.clone(), ) .await?; - let response = identity_client + let PeersDeviceListsResponse { + users_device_lists, + users_devices_platform_details, + } = identity_client .get_device_lists_for_users(PeersDeviceListsRequest { user_ids }) .await? .into_inner(); - let payload = serde_json::to_string(&response.users_device_lists)?; + let nested_users_devices_platform_details: HashMap< + String, + HashMap<String, PlatformDetails>, + > = users_devices_platform_details + .into_iter() + .map(|(user_id, user_devices_platform_details)| { + ( + user_id, + user_devices_platform_details + .devices_platform_details + .into_iter() + .map(|(device_id, platform_details)| { + (device_id, platform_details.into()) + }) + .collect(), + ) + }) + .collect(); + + let result = PeersDeviceLists { + users_device_lists: users_device_lists, + users_devices_platform_details: nested_users_devices_platform_details, + }; + + let payload = serde_json::to_string(&result)?; Ok(payload) } async fn update_device_list_helper( auth_info: AuthInfo, update_payload: String, ) -> Result<(), Error> { let mut identity_client = get_auth_client( IDENTITY_SOCKET_ADDR, auth_info.user_id, auth_info.device_id, auth_info.access_token, PLATFORM_METADATA.clone(), ) .await?; let update_request = UpdateDeviceListRequest { new_device_list: update_payload, }; identity_client.update_device_list(update_request).await?; Ok(()) } + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct PlatformDetails { + device_type: i32, + code_version: u64, + state_version: Option<u64>, + major_desktop_version: Option<u64>, +} + +impl From<grpc_clients::identity::protos::auth::PlatformDetails> + for PlatformDetails +{ + fn from( + value: grpc_clients::identity::protos::auth::PlatformDetails, + ) -> Self { + Self { + device_type: value.device_type, + code_version: value.code_version, + state_version: value.state_version, + major_desktop_version: value.major_desktop_version, + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct PeersDeviceLists { + users_device_lists: HashMap<String, String>, + users_devices_platform_details: + HashMap<String, HashMap<String, PlatformDetails>>, +} diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index f05e4acf6..37a329536 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,748 +1,767 @@ // @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, type Identities, identitiesValidator, + type PeersDeviceLists, + peersDeviceListsValidator, + type IdentityPlatformDetails, } 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<IdentityNewDeviceKeyUpload>; getExistingDeviceKeyUpload: () => Promise<IdentityExistingDeviceKeyUpload>; constructor( platformDetails: PlatformDetails, overridedOpaqueFilepath: string, authLayer: ?IdentityServiceAuthLayer, getNewDeviceKeyUpload: () => Promise<IdentityNewDeviceKeyUpload>, getExistingDeviceKeyUpload: () => Promise<IdentityExistingDeviceKeyUpload>, ) { 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<Request, Response>( platformDetails, ); const authInterceptor = new AuthInterceptor<Request, Response>( 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<Request, Response>( platformDetails, ); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } logOut: () => Promise<void> = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutUser(new Empty()); }; logOutSecondaryDevice: () => Promise<void> = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutSecondaryDevice(new Empty()); }; getKeyserverKeys: (keyserverID: string) => Promise<DeviceOlmOutboundKeys> = 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(), }; return assertWithValidator(keyserverKeys, deviceOlmOutboundKeysValidator); }; getOutboundKeysForUser: ( userID: string, ) => Promise<UserDevicesOlmOutboundKeys[]> = 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; } 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<UserDevicesOlmInboundKeys> = 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<void> = 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<IdentityAuthResult> = 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 usernameResponse = loginFinishResponse.getUsername(); const identityAuthResult = { accessToken, userID, username: usernameResponse, }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise<IdentityAuthResult> = 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 username = loginResponse.getUsername(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; uploadKeysForRegisteredDeviceAndLogIn: ( ownerUserID: string, nonceChallengeResponse: SignedNonce, ) => Promise<IdentityAuthResult> = 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 username = response.getUsername(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; generateNonce: () => Promise<string> = async () => { const result = await this.unauthClient.generateNonce(new Empty()); return result.getNonce(); }; publishWebPrekeys: (prekeys: SignedPrekeys) => Promise<void> = 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<SignedDeviceList>> = 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<string>, - ) => Promise<UsersSignedDeviceLists> = async userIDs => { + ) => Promise<PeersDeviceLists> = 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; + const rawUsersDevicesPlatformDetails = + response.toObject()?.usersDevicesPlatformDetailsMap; let usersDeviceLists: UsersSignedDeviceLists = {}; rawPayloads.forEach(([userID, rawPayload]) => { usersDeviceLists = { ...usersDeviceLists, [userID]: JSON.parse(rawPayload), }; }); - return assertWithValidator( - usersDeviceLists, - usersSignedDeviceListsValidator, - ); + const usersDevicesPlatformDetails: { + [userID: string]: { +[deviceID: string]: IdentityPlatformDetails }, + } = {}; + + for (const [ + userID, + rawUserDevicesPlatformDetails, + ] of rawUsersDevicesPlatformDetails) { + usersDevicesPlatformDetails[userID] = Object.fromEntries( + rawUserDevicesPlatformDetails.devicesPlatformDetailsMap, + ); + } + + const peersDeviceLists = { + usersSignedDeviceLists: usersDeviceLists, + usersDevicesPlatformDetails, + }; + + return assertWithValidator(peersDeviceLists, peersDeviceListsValidator); }; getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray<string>, ) => Promise<$ReadOnlyArray<FarcasterUser>> = 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<void> = 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<void> = async () => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await client.unlinkFarcasterAccount(new Empty()); }; findUserIdentities: (userIDs: $ReadOnlyArray<string>) => Promise<Identities> = async userIDs => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.UserIdentitiesRequest(); request.setUserIdsList([...userIDs]); const response = await client.findUserIdentities(request); const identityObjects = response.toObject()?.identitiesMap; let identities: Identities = {}; identityObjects.forEach(([userID, identityObject]) => { identities = { ...identities, [userID]: { ethIdentity: identityObject.ethIdentity, username: identityObject.username, farcasterID: identityObject.farcasterId, }, }; }); return assertWithValidator(identities, identitiesValidator); }; } 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<string> = [], notifOneTimeKeys: $ReadOnlyArray<string> = [], ): 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 };