diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -54,6 +54,10 @@ username: string, password: string, ) => Promise; + +logInPasswordUser: ( + username: string, + password: string, + ) => Promise; } export type IdentityServiceAuthLayer = { @@ -81,6 +85,12 @@ +accessToken: string, +username: string, }; +export const identityAuthResultValidator: TInterface = + tShape({ + userID: t.String, + accessToken: t.String, + username: t.String, + }); export type IdentityDeviceKeyUpload = { +keyPayload: string, @@ -94,3 +104,12 @@ }; 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/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -10,7 +10,10 @@ type KeyserverKeys, keyserverKeysValidator, } from 'lib/types/identity-service-types.js'; -import { ONE_TIME_KEYS_NUMBER } from 'lib/types/identity-service-types.js'; +import { + ONE_TIME_KEYS_NUMBER, + identityAuthResultValidator, +} from 'lib/types/identity-service-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { getCommServicesAuthMetadataEmitter } from '../event-emitters/csa-auth-metadata-emitter.js'; @@ -134,6 +137,39 @@ const { userID, accessToken } = JSON.parse(registrationResult); return { accessToken, userID, username }; }, + logInPasswordUser: async (username: string, password: string) => { + await commCoreModule.initializeCryptoAccount(); + const [ + { blobPayload, signature }, + notificationsOneTimeKeys, + primaryOneTimeKeys, + prekeys, + ] = await Promise.all([ + commCoreModule.getUserPublicKey(), + commCoreModule.getNotificationsOneTimeKeys(ONE_TIME_KEYS_NUMBER), + commCoreModule.getPrimaryOneTimeKeys(ONE_TIME_KEYS_NUMBER), + commCoreModule.generateAndGetPrekeys(), + ]); + const loginResult = await commRustModule.logInPasswordUser( + username, + password, + blobPayload, + signature, + prekeys.contentPrekey, + prekeys.contentPrekeySignature, + prekeys.notifPrekey, + prekeys.notifPrekeySignature, + getOneTimeKeyArray(primaryOneTimeKeys), + getOneTimeKeyArray(notificationsOneTimeKeys), + ); + const { userID, accessToken } = JSON.parse(loginResult); + const identityAuthResult = { accessToken, userID, username }; + + return assertWithValidator( + identityAuthResult, + identityAuthResultValidator, + ); + }, }), [getAuthMetadata], ); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,30 +1,50 @@ // @flow +import { Login } from '@commapp/opaque-ke-wasm'; + import identityServiceConfig from 'lib/facts/identity-service.js'; import { type IdentityServiceAuthLayer, type IdentityServiceClient, type KeyserverKeys, + type IdentityAuthResult, + type IdentityDeviceKeyUpload, keyserverKeysValidator, + identityDeviceTypes, + identityAuthResultValidator, } 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 { Empty } from '../protobufs/identity-unauth-structs.cjs'; +import { + DeviceKeyUpload, + Empty, + IdentityKeyInfo, + OpaqueLoginFinishRequest, + OpaqueLoginStartRequest, + Prekey, +} 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) { + constructor( + authLayer: ?IdentityServiceAuthLayer, + getDeviceKeyUpload: () => Promise, + ) { if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient(authLayer); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(); + this.getDeviceKeyUpload = getDeviceKeyUpload; } static determineSocketAddr(): string { @@ -124,6 +144,105 @@ return assertWithValidator(keyserverKeys, keyserverKeysValidator); }; + + 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 { + keyPayload, + keyPayloadSignature, + contentPrekey, + contentPrekeySignature, + notifPrekey, + notifPrekeySignature, + contentOneTimeKeys, + notifOneTimeKeys, + } = identityDeviceKeyUpload; + + const contentOneTimeKeysArray = [...contentOneTimeKeys]; + const notifOneTimeKeysArray = [...notifOneTimeKeys]; + + const opaqueLogin = new Login(); + const startRequestBytes = opaqueLogin.start(password); + + 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); + + 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( + `logInPasswordUserStart RPC failed: ${ + 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( + `logInPasswordUserFinish RPC failed: ${ + getMessageForException(e) ?? 'unknown' + }`, + ); + } + + const userID = loginFinishResponse.getUserId(); + const accessToken = loginFinishResponse.getAccessToken(); + const identityAuthResult = { accessToken, userID, username }; + + return assertWithValidator(identityAuthResult, identityAuthResultValidator); + }; } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -5,6 +5,7 @@ import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js'; +import { useGetDeviceKeyUpload } from '../account/account-hooks.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { @@ -18,6 +19,7 @@ const deviceID = useSelector( state => state.cryptoStore?.primaryIdentityKeys.ed25519, ); + const getDeviceKeyUpload = useGetDeviceKeyUpload(); const client = React.useMemo(() => { let authLayer = null; @@ -28,8 +30,8 @@ commServicesAccessToken: accessToken, }; } - return new IdentityServiceClientWrapper(authLayer); - }, [accessToken, deviceID, userID]); + return new IdentityServiceClientWrapper(authLayer, getDeviceKeyUpload); + }, [accessToken, deviceID, getDeviceKeyUpload, userID]); const value = React.useMemo( () => ({