diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 4db33b192..5f3bfb84f 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,168 +1,169 @@ // @flow import t, { type TInterface } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating // OutboundKeyInfoResponse in native/native_rust_library/src/lib.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof: ?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/lib.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof?: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, +socialProof: ?string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, socialProof: t.maybe(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, }; 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 { +deleteUser: () => Promise; +getKeyserverKeys: string => Promise; +registerPasswordUser?: ( username: string, password: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; + +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, }; export const identityAuthResultValidator: TInterface = tShape({ userID: t.String, accessToken: t.String, username: t.String, }); export type IdentityDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index bd4a9d979..e5a61df72 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,452 +1,477 @@ // @flow import { Login } from '@commapp/opaque-ke-wasm'; import identityServiceConfig from 'lib/facts/identity-service.js'; -import type { OneTimeKeysResultValues } from 'lib/types/crypto-types.js'; +import type { + OneTimeKeysResultValues, + SignedPrekeys, +} from 'lib/types/crypto-types.js'; import { type IdentityServiceAuthLayer, type IdentityServiceClient, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type UserDevicesOlmOutboundKeys, type IdentityAuthResult, type IdentityDeviceKeyUpload, identityDeviceTypes, identityAuthResultValidator, type UserDevicesOlmInboundKeys, type DeviceOlmInboundKeys, deviceOlmInboundKeysValidator, userDeviceOlmInboundKeysValidator, } 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 { initOpaque } from '../crypto/opaque-utils.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, } from '../protobufs/identity-unauth-structs.cjs'; import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; class IdentityServiceClientWrapper implements IdentityServiceClient { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; getDeviceKeyUpload: () => Promise; constructor( authLayer: ?IdentityServiceAuthLayer, getDeviceKeyUpload: () => Promise, ) { if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient(authLayer); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(); this.getDeviceKeyUpload = getDeviceKeyUpload; } static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } static createAuthClient( authLayer: IdentityServiceAuthLayer, ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } static createUnauthClient(): IdentityUnauthClient.IdentityClientServicePromiseClient { const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } deleteUser: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.deleteUser(new Empty()); }; getKeyserverKeys: (keyserverID: string) => Promise = async (keyserverID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(keyserverID); const response = await client.getKeyserverKeys(request); const keyserverInfo = response.getKeyserverInfo(); const identityInfo = keyserverInfo?.getIdentityInfo(); const contentPreKey = keyserverInfo?.getContentPrekey(); const notifPreKey = keyserverInfo?.getNotifPrekey(); const payload = identityInfo?.getPayload(); const keyserverKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.getPrekey(), prekeySignature: contentPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeContentPrekey(), }, notifInitializationInfo: { prekey: notifPreKey?.getPrekey(), prekeySignature: notifPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeNotifPrekey(), }, payloadSignature: identityInfo?.getPayloadSignature(), socialProof: identityInfo?.getSocialProof(), }; if (!keyserverKeys.contentInitializationInfo.oneTimeKey) { throw new Error('Missing content one time key'); } if (!keyserverKeys.notifInitializationInfo.oneTimeKey) { throw new Error('Missing notif one time key'); } return assertWithValidator(keyserverKeys, deviceOlmOutboundKeysValidator); }; getOutboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getOutboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: (?UserDevicesOlmOutboundKeys)[] = devicesMap.map( ([deviceID, outboundKeysInfo]) => { const identityInfo = outboundKeysInfo?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = outboundKeysInfo?.contentPrekey; const notifPreKey = outboundKeysInfo?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return null; } if ( !outboundKeysInfo.oneTimeContentPrekey || !outboundKeysInfo.oneTimeNotifPrekey ) { console.log(`Missing one time key for device ${deviceID}`); return { deviceID, keys: null, }; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.prekey, prekeySignature: contentPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: notifPreKey?.prekey, prekeySignature: notifPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeNotifPrekey, }, payloadSignature: identityInfo?.payloadSignature, socialProof: identityInfo?.socialProof, }; 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.getDeviceKeyUpload(), initOpaque(), ]); const opaqueLogin = new Login(); const startRequestBytes = opaqueLogin.start(password); const deviceKeyUpload = authDeviceKeyUpload(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:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } 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:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const userID = loginFinishResponse.getUserId(); const accessToken = loginFinishResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise = async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { const identityDeviceKeyUpload = await this.getDeviceKeyUpload(); const deviceKeyUpload = authDeviceKeyUpload(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:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const userID = loginResponse.getUserId(); const accessToken = loginResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username: walletAddress }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; generateNonce: () => Promise = async () => { const result = await this.unauthClient.generateNonce(new Empty()); return result.getNonce(); }; + + publishWebPrekeys: (prekeys: SignedPrekeys) => Promise = async ( + prekeys: SignedPrekeys, + ) => { + const client = this.authClient; + if (!client) { + throw new Error('Identity service client is not initialized'); + } + + const contentPrekeyUpload = new Prekey(); + contentPrekeyUpload.setPrekey(prekeys.contentPrekey); + contentPrekeyUpload.setPrekeySignature(prekeys.contentPrekeySignature); + + const notifPrekeyUpload = new Prekey(); + notifPrekeyUpload.setPrekey(prekeys.notifPrekey); + notifPrekeyUpload.setPrekeySignature(prekeys.notifPrekeySignature); + + const request = new IdentityAuthStructs.RefreshUserPrekeysRequest(); + request.setNewContentPrekeys(contentPrekeyUpload); + request.setNewNotifPrekeys(notifPrekeyUpload); + await client.refreshUserPrekeys(request); + }; } function authDeviceKeyUpload( uploadData: IdentityDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, contentOneTimeKeys, notifOneTimeKeys, } = uploadData; const contentOneTimeKeysArray = [...contentOneTimeKeys]; const notifOneTimeKeysArray = [...notifOneTimeKeys]; const identityKeyInfo = new IdentityKeyInfo(); identityKeyInfo.setPayload(keyPayload); identityKeyInfo.setPayloadSignature(keyPayloadSignature); const contentPrekeyUpload = new Prekey(); contentPrekeyUpload.setPrekey(contentPrekey); contentPrekeyUpload.setPrekeySignature(contentPrekeySignature); const notifPrekeyUpload = new Prekey(); notifPrekeyUpload.setPrekey(notifPrekey); notifPrekeyUpload.setPrekeySignature(notifPrekeySignature); const deviceKeyUpload = new DeviceKeyUpload(); deviceKeyUpload.setDeviceKeyInfo(identityKeyInfo); deviceKeyUpload.setContentUpload(contentPrekeyUpload); deviceKeyUpload.setNotifUpload(notifPrekeyUpload); deviceKeyUpload.setOneTimeContentPrekeysList(contentOneTimeKeysArray); deviceKeyUpload.setOneTimeNotifPrekeysList(notifOneTimeKeysArray); deviceKeyUpload.setDeviceType(identityDeviceTypes.WEB); return deviceKeyUpload; } export { IdentityServiceClientWrapper };