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 @@ -59,6 +59,10 @@ username: string, password: string, ) => Promise; + +logInPasswordUser: ( + username: string, + password: string, + ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; @@ -89,6 +93,12 @@ +accessToken: string, +username: string, }; +export const identityAuthResultValidator: TInterface = + tShape({ + userID: t.String, + accessToken: t.String, + username: t.String, + }); export type IdentityDeviceKeyUpload = { +keyPayload: string, @@ -102,3 +112,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 @@ -2,7 +2,7 @@ import * as React from 'react'; -import { getOneTimeKeyArray } from 'lib/shared/crypto-utils.js'; +import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { type IdentityKeysBlob, @@ -15,7 +15,10 @@ type UserDevicesOlmOutboundKeys, type UserLoginResponse, } 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'; @@ -217,12 +220,43 @@ prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, - getOneTimeKeyArray(contentOneTimeKeys), - getOneTimeKeyArray(notificationsOneTimeKeys), + getOneTimeKeyValues(contentOneTimeKeys), + getOneTimeKeyValues(notificationsOneTimeKeys), ); const { userID, accessToken: token } = JSON.parse(registrationResult); return { accessToken: token, userID, username }; }, + logInPasswordUser: async (username: string, password: string) => { + await commCoreModule.initializeCryptoAccount(); + const [ + { blobPayload, signature }, + { contentOneTimeKeys, notificationsOneTimeKeys }, + prekeys, + ] = await Promise.all([ + commCoreModule.getUserPublicKey(), + commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), + commCoreModule.validateAndGetPrekeys(), + ]); + const loginResult = await commRustModule.logInPasswordUser( + username, + password, + blobPayload, + signature, + prekeys.contentPrekey, + prekeys.contentPrekeySignature, + prekeys.notifPrekey, + prekeys.notifPrekeySignature, + getOneTimeKeyValues(contentOneTimeKeys), + getOneTimeKeyValues(notificationsOneTimeKeys), + ); + const { userID, accessToken: token } = JSON.parse(loginResult); + const identityAuthResult = { accessToken: token, 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,5 +1,7 @@ // @flow +import { Login } from '@commapp/opaque-ke-wasm'; + import identityServiceConfig from 'lib/facts/identity-service.js'; import { type IdentityServiceAuthLayer, @@ -7,25 +9,43 @@ type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type UserDevicesOlmOutboundKeys, + type IdentityAuthResult, + type IdentityDeviceKeyUpload, + 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 { @@ -202,6 +222,105 @@ return devicesKeys.filter(Boolean); }; + + 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 @@ -8,6 +8,7 @@ } 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 = { @@ -21,6 +22,7 @@ const deviceID = useSelector( state => state.cryptoStore?.primaryIdentityKeys.ed25519, ); + const getDeviceKeyUpload = useGetDeviceKeyUpload(); const client = React.useMemo(() => { let authLayer = null; @@ -31,8 +33,8 @@ commServicesAccessToken: accessToken, }; } - return new IdentityServiceClientWrapper(authLayer); - }, [accessToken, deviceID, userID]); + return new IdentityServiceClientWrapper(authLayer, getDeviceKeyUpload); + }, [accessToken, deviceID, getDeviceKeyUpload, userID]); const getAuthMetadata = React.useCallback<() => Promise>( async () => ({