diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index bbefac3a1..168826cfc 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,375 +1,385 @@ // @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 Platform } from './device-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 = 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: tUserID, 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; // This log out type is specific to primary device, and web cannot be a // primary device +logOutPrimaryDevice?: () => Promise; +logOutSecondaryDevice: () => Promise; +getKeyserverKeys: string => Promise; // Users cannot register from web +registerPasswordUser?: ( username: string, password: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedPasswordUser?: ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; // Users cannot register from web +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?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: ( 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; +syncPlatformDetails: () => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; +findUserIdentities: (userIDs: $ReadOnlyArray) => Promise; +versionSupported: () => Promise; +changePassword: (oldPassword: string, newPassword: string) => 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: 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, +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 const rawDeviceListValidator: TInterface = tShape({ 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 = tShape({ walletAddress: t.String, siweMessage: t.String, siweSignature: t.String, }); export const identityValidator: TInterface = tShape({ username: t.String, ethIdentity: t.maybe(ethereumIdentityValidator), farcasterID: t.maybe(t.String), }); export const identitiesValidator: TDict = 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 = tShape({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(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, }); export type IdentityDeviceType = $Values; export const identityDeviceTypeToPlatform: { +[identityDeviceType: IdentityDeviceType]: Platform, -} = { +} = Object.freeze({ [identityDeviceTypes.WEB]: 'web', [identityDeviceTypes.ANDROID]: 'android', [identityDeviceTypes.IOS]: 'ios', [identityDeviceTypes.WINDOWS]: 'windows', [identityDeviceTypes.MAC_OS]: 'macos', -}; +}); + +export const platformToIdentityDeviceType: { + +[platform: Platform]: IdentityDeviceType, +} = Object.freeze({ + web: identityDeviceTypes.WEB, + android: identityDeviceTypes.ANDROID, + ios: identityDeviceTypes.IOS, + windows: identityDeviceTypes.WINDOWS, + macos: identityDeviceTypes.MAC_OS, +}); export const identityDeviceTypeValidator: TEnums = t.enums.of( values(identityDeviceTypes), ); export type IdentityPlatformDetails = { +deviceType: IdentityDeviceType, +codeVersion: number, +stateVersion?: number, +majorDesktopVersion?: number, }; export const identityPlatformDetailsValidator: TInterface = tShape({ 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 = t.dict(t.String, identityPlatformDetailsValidator); export type UsersDevicesPlatformDetails = { +[userID: string]: UserDevicesPlatformDetails, }; export const usersDevicesPlatformDetailsValidator: TDict = t.dict(t.String, userDevicesPlatformDetailsValidator); export type PeersDeviceLists = { +usersSignedDeviceLists: UsersSignedDeviceLists, +usersDevicesPlatformDetails: UsersDevicesPlatformDetails, }; export const peersDeviceListsValidator: TInterface = tShape({ usersSignedDeviceLists: usersSignedDeviceListsValidator, usersDevicesPlatformDetails: usersDevicesPlatformDetailsValidator, }); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index 2dedd5acc..26f3ed2f1 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,856 +1,870 @@ // @flow import { Login, Registration } 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, type Identities, identitiesValidator, type PeersDeviceLists, peersDeviceListsValidator, type IdentityPlatformDetails, + platformToIdentityDeviceType, } 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 * as identityUnauthStructs from '../protobufs/identity-unauth-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; + platformDetails: PlatformDetails; authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; getNewDeviceKeyUpload: () => Promise; getExistingDeviceKeyUpload: () => Promise; constructor( platformDetails: PlatformDetails, overridedOpaqueFilepath: string, authLayer: ?IdentityServiceAuthLayer, getNewDeviceKeyUpload: () => Promise, getExistingDeviceKeyUpload: () => Promise, ) { this.overridedOpaqueFilepath = overridedOpaqueFilepath; + this.platformDetails = platformDetails; 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()); }; logOutSecondaryDevice: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutSecondaryDevice(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(), }; 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; } 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( + this.platformDetails, 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 = async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { const identityDeviceKeyUpload = await this.getExistingDeviceKeyUpload(); const deviceKeyUpload = authExistingDeviceKeyUpload( + this.platformDetails, 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 = async ( ownerUserID, nonceChallengeResponse, ) => { const identityDeviceKeyUpload = await this.getNewDeviceKeyUpload(); - const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); + const deviceKeyUpload = authNewDeviceKeyUpload( + this.platformDetails, + 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 = 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.setNewContentPrekey(contentPrekeyUpload); request.setNewNotifPrekey(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; const rawUsersDevicesPlatformDetails = response.toObject()?.usersDevicesPlatformDetailsMap; let usersDeviceLists: UsersSignedDeviceLists = {}; rawPayloads.forEach(([userID, rawPayload]) => { usersDeviceLists = { ...usersDeviceLists, [userID]: JSON.parse(rawPayload), }; }); 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); }; syncPlatformDetails: () => Promise = async () => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await client.syncPlatformDetails(new identityUnauthStructs.Empty()); }; 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()); }; findUserIdentities: (userIDs: $ReadOnlyArray) => Promise = 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); }; versionSupported: () => Promise = async () => { const client = this.unauthClient; try { await client.ping(new Empty()); return true; } catch (e) { if (getMessageForException(e) === 'unsupported_version') { return false; } throw e; } }; changePassword: (oldPassword: string, newPassword: string) => Promise = async (oldPassword, newPassword) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await initOpaque(this.overridedOpaqueFilepath); const opaqueLogin = new Login(); const loginStartRequestBytes = opaqueLogin.start(oldPassword); const opaqueRegistration = new Registration(); const registrationStartRequestBytes = opaqueRegistration.start(newPassword); const updatePasswordStartRequest = new IdentityAuthStructs.UpdateUserPasswordStartRequest(); updatePasswordStartRequest.setOpaqueLoginRequest(loginStartRequestBytes); updatePasswordStartRequest.setOpaqueRegistrationRequest( registrationStartRequestBytes, ); let updatePasswordStartResponse; try { updatePasswordStartResponse = await client.updateUserPasswordStart( updatePasswordStartRequest, ); } catch (e) { console.log('Error calling updateUserPasswordStart:', e); throw new Error( `updateUserPasswordStart RPC failed: ${ getMessageForException(e) ?? 'unknown' }`, ); } const loginFinishRequestBytes = opaqueLogin.finish( updatePasswordStartResponse.getOpaqueLoginResponse_asU8(), ); const registrationFinishRequestBytes = opaqueRegistration.finish( newPassword, updatePasswordStartResponse.getOpaqueRegistrationResponse_asU8(), ); const updatePasswordFinishRequest = new IdentityAuthStructs.UpdateUserPasswordFinishRequest(); updatePasswordFinishRequest.setSessionId( updatePasswordStartResponse.getSessionId(), ); updatePasswordFinishRequest.setOpaqueLoginUpload(loginFinishRequestBytes); updatePasswordFinishRequest.setOpaqueRegistrationUpload( registrationFinishRequestBytes, ); try { await client.updateUserPasswordFinish(updatePasswordFinishRequest); } catch (e) { console.log('Error calling updateUserPasswordFinish:', e); throw new Error( `updateUserPasswordFinish RPC failed: ${ getMessageForException(e) ?? 'unknown' }`, ); } }; } function authNewDeviceKeyUpload( + platformDetails: PlatformDetails, 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( + platformDetails, identityKeyInfo, contentPrekeyUpload, notifPrekeyUpload, contentOneTimeKeys, notifOneTimeKeys, ); return deviceKeyUpload; } function authExistingDeviceKeyUpload( + platformDetails: PlatformDetails, 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( + platformDetails, 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( + platformDetails: PlatformDetails, 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); + deviceKeyUpload.setDeviceType( + platformToIdentityDeviceType[platformDetails.platform], + ); return deviceKeyUpload; } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/interceptor.js b/web/grpc/interceptor.js index 3757c6a1a..1bd38def2 100644 --- a/web/grpc/interceptor.js +++ b/web/grpc/interceptor.js @@ -1,68 +1,75 @@ // @flow import * as grpcWeb from 'grpc-web'; import type { PlatformDetails } from 'lib/types/device-types.js'; class VersionInterceptor { platformDetails: PlatformDetails; constructor(platformDetails: PlatformDetails) { this.platformDetails = platformDetails; } intercept( request: grpcWeb.Request, invoker: ( request: grpcWeb.Request, ) => Promise>, ): Promise> { const metadata = request.getMetadata(); const { codeVersion, stateVersion, majorDesktopVersion, platform: deviceType, } = this.platformDetails; if (codeVersion) { metadata['code_version'] = codeVersion.toString(); } if (stateVersion) { metadata['state_version'] = stateVersion.toString(); } if (majorDesktopVersion) { metadata['major_desktop_version'] = majorDesktopVersion.toString(); } - metadata['device_type'] = deviceType; + + let identityDeviceType; + if (deviceType === 'macos') { + identityDeviceType = 'mac_os'; + } else { + identityDeviceType = deviceType; + } + metadata['device_type'] = identityDeviceType; return invoker(request); } } class AuthInterceptor { userID: string; deviceID: string; accessToken: string; constructor(userID: string, deviceID: string, accessToken: string) { this.userID = userID; this.deviceID = deviceID; this.accessToken = accessToken; } intercept( request: grpcWeb.Request, invoker: ( request: grpcWeb.Request, ) => Promise>, ): Promise> { const metadata = request.getMetadata(); metadata['user_id'] = this.userID; metadata['device_id'] = this.deviceID; metadata['access_token'] = this.accessToken; return invoker(request); } } export { VersionInterceptor, AuthInterceptor };