diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index e07dac5971b..e1ae15ed703 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -11,7 +11,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; -import { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration"; +import { OpaqueCipherConfiguration } from "@bitwarden/common/auth/opaque/models/opaque-cipher-configuration"; import { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -229,7 +229,7 @@ export class ChangePasswordComponent }); } else { const userConfiguredKdf = await this.kdfConfigService.getKdfConfig(); - const cipherConfig = CipherConfiguration.fromKdfConfig( + const cipherConfig = OpaqueCipherConfiguration.fromKdfConfig( userConfiguredKdf.kdfType === KdfType.Argon2id ? userConfiguredKdf : DEFAULT_OPAQUE_KDF_CONFIG, diff --git a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts index 1e9ca31b540..bd5f7f9542c 100644 --- a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts @@ -8,13 +8,13 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { OpaqueTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/opaque-token.request"; -import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { OpaqueCipherConfiguration } from "@bitwarden/common/auth/opaque/models/opaque-cipher-configuration"; +import { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -49,7 +49,7 @@ export class OpaqueLoginStrategyData implements LoginStrategyData { static fromJSON(obj: Jsonify): OpaqueLoginStrategyData { const data = Object.assign(new OpaqueLoginStrategyData(), obj, { - tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest), + tokenRequest: OpaqueTokenRequest.fromJSON(obj.tokenRequest), masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey), }); return data; @@ -76,6 +76,7 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { data: OpaqueLoginStrategyData, private passwordStrengthService: PasswordStrengthServiceAbstraction, private policyService: PolicyService, + private opaqueKeyExchangeService: OpaqueKeyExchangeService, ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); @@ -87,8 +88,15 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { } override async logIn(credentials: OpaqueLoginCredentials) { + this.logService.info("Logging in with OPAQUE"); const { email, masterPassword, kdfConfig, cipherConfiguration, twoFactor } = credentials; + const { sessionId } = await this.opaqueKeyExchangeService.login( + email, + masterPassword, + OpaqueCipherConfiguration.fromAny(cipherConfiguration), + ); + const data = new OpaqueLoginStrategyData(); data.userEnteredEmail = email; @@ -109,6 +117,7 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { data.tokenRequest = new OpaqueTokenRequest( email, await this.buildTwoFactor(twoFactor, email), + sessionId, await this.buildDeviceRequest(), ); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 56dd0f80f6d..01c94f71b0d 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -303,7 +303,12 @@ export class PasswordLoginStrategy extends BaseLoginStrategy { ); try { - await this.opaqueKeyExchangeService.register(masterPassword, userKey, cipherConfig); + const sessionId = await this.opaqueKeyExchangeService.register( + masterPassword, + userKey, + cipherConfig, + ); + await this.opaqueKeyExchangeService.setRegistrationActive(sessionId); } catch (error) { // If this process fails for any reason, we don't want to stop the login process // so just log the error and continue. diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 9c92e96cbb6..9b5899dea2b 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -433,6 +433,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { data?.opaque ?? new OpaqueLoginStrategyData(), this.passwordStrengthService, this.policyService, + this.opaqueKeyExchangeService, ...sharedDeps, ); default: diff --git a/libs/common/src/auth/models/request/identity-token/opaque-token.request.ts b/libs/common/src/auth/models/request/identity-token/opaque-token.request.ts index 4c587b1f09d..9f8a481360b 100644 --- a/libs/common/src/auth/models/request/identity-token/opaque-token.request.ts +++ b/libs/common/src/auth/models/request/identity-token/opaque-token.request.ts @@ -11,6 +11,7 @@ export class OpaqueTokenRequest extends TokenRequest { constructor( public email: string, protected twoFactor: TokenTwoFactorRequest, + public sessionId: string, device?: DeviceRequest, public newDeviceOtp?: string, ) { @@ -21,8 +22,9 @@ export class OpaqueTokenRequest extends TokenRequest { const obj = super.toIdentityToken(clientId); // TODO: what grant type for OPAQUE? - obj.grant_type = "password"; + obj.grant_type = "opaque-ke"; obj.username = this.email; + obj.sessionId = this.sessionId; if (this.newDeviceOtp) { obj.newDeviceOtp = this.newDeviceOtp; diff --git a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts index fbfdde93b85..508a63af628 100644 --- a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts +++ b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts @@ -13,6 +13,7 @@ import { LoginStartRequest } from "./models/login-start.request"; import { OpaqueCipherConfiguration } from "./models/opaque-cipher-configuration"; import { RegistrationFinishRequest } from "./models/registration-finish.request"; import { RegistrationStartRequest } from "./models/registration-start.request"; +import { SetRegistrationActiveRequest } from "./models/set-registration-active.request"; import { OpaqueKeyExchangeApiService } from "./opaque-key-exchange-api.service"; import { OpaqueKeyExchangeService } from "./opaque-key-exchange.service"; @@ -74,6 +75,12 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService return registrationStartResponse.sessionId; } + async setRegistrationActive(sessionId: OpaqueSessionId): Promise { + await this.opaqueKeyExchangeApiService.setRegistrationActive( + new SetRegistrationActiveRequest(sessionId), + ); + } + // TODO: we will likely have to break this apart to return the start / finish requests // so that the opaque login strategy can send both to the identity token endpoint // in separate calls. @@ -81,7 +88,7 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService email: string, masterPassword: string, cipherConfig: OpaqueCipherConfiguration, - ): Promise { + ): Promise<{ sessionId: string; exportKey: Uint8Array }> { if (!email || !masterPassword || !cipherConfig) { throw new Error( `Unable to log in user with missing parameters. email exists: ${!!email}; masterPassword exists: ${!!masterPassword}; cipherConfig exists: ${!!cipherConfig}`, @@ -112,6 +119,6 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService throw new Error("Login failed"); } - return loginFinish.export_key; + return { sessionId: loginStartResponse.sessionId, exportKey: loginFinish.export_key }; } } diff --git a/libs/common/src/auth/opaque/models/opaque-cipher-configuration.ts b/libs/common/src/auth/opaque/models/opaque-cipher-configuration.ts index ac652c4e5d5..9d74deca7e3 100644 --- a/libs/common/src/auth/opaque/models/opaque-cipher-configuration.ts +++ b/libs/common/src/auth/opaque/models/opaque-cipher-configuration.ts @@ -17,6 +17,16 @@ export class OpaqueCipherConfiguration { this.argon2Parameters = ksf; } + static fromAny(config: any): OpaqueCipherConfiguration { + if ( + config.cipherSuite !== + "OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF" + ) { + throw new Error("Unsupported cipher suite"); + } + return new OpaqueCipherConfiguration(config.argon2Parameters); + } + /** * Converts from Bitwarden KDF configs to OPAQUE KSF configs. * diff --git a/libs/common/src/auth/opaque/models/set-registration-active.request.ts b/libs/common/src/auth/opaque/models/set-registration-active.request.ts new file mode 100644 index 00000000000..ce37b5fa181 --- /dev/null +++ b/libs/common/src/auth/opaque/models/set-registration-active.request.ts @@ -0,0 +1,5 @@ +import { OpaqueSessionId } from "@bitwarden/common/types/guid"; + +export class SetRegistrationActiveRequest { + constructor(readonly sessionId: OpaqueSessionId) {} +} diff --git a/libs/common/src/auth/opaque/opaque-key-exchange-api.service.ts b/libs/common/src/auth/opaque/opaque-key-exchange-api.service.ts index 4d98dd61a2b..b0c61d0f0a9 100644 --- a/libs/common/src/auth/opaque/opaque-key-exchange-api.service.ts +++ b/libs/common/src/auth/opaque/opaque-key-exchange-api.service.ts @@ -10,6 +10,7 @@ import { RegistrationFinishRequest } from "./models/registration-finish.request" import { RegistrationFinishResponse } from "./models/registration-finish.response"; import { RegistrationStartRequest } from "./models/registration-start.request"; import { RegistrationStartResponse } from "./models/registration-start.response"; +import { SetRegistrationActiveRequest } from "./models/set-registration-active.request"; export class OpaqueKeyExchangeApiService { constructor( @@ -45,13 +46,25 @@ export class OpaqueKeyExchangeApiService { return new RegistrationFinishResponse(response); } + async setRegistrationActive(request: SetRegistrationActiveRequest): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + await this.apiService.send( + "POST", + `/opaque/set-registration-active`, + request, + true, + true, + env.getApiUrl(), + ); + } + async loginStart(request: LoginStartRequest): Promise { const env = await firstValueFrom(this.environmentService.environment$); const response = await this.apiService.send( "POST", `/opaque/start-login`, request, - true, + false, true, env.getApiUrl(), ); @@ -64,10 +77,10 @@ export class OpaqueKeyExchangeApiService { "POST", `/opaque/finish-login`, request, - true, + false, true, env.getApiUrl(), ); - return response.success; + return response == true; } } diff --git a/libs/common/src/auth/opaque/opaque-key-exchange.service.ts b/libs/common/src/auth/opaque/opaque-key-exchange.service.ts index 374781a5b84..172c3806d75 100644 --- a/libs/common/src/auth/opaque/opaque-key-exchange.service.ts +++ b/libs/common/src/auth/opaque/opaque-key-exchange.service.ts @@ -14,6 +14,11 @@ export abstract class OpaqueKeyExchangeService { cipherConfiguration: OpaqueCipherConfiguration, ): Promise; + /** + * Set the registration as the active authentication method for the user. + */ + abstract setRegistrationActive(sessionId: OpaqueSessionId): Promise; + /** * Authenticate using the Opaque login method. Returns the export key, which must be used * in combination with the rotateable keyset returned from the token endpoint. @@ -23,5 +28,8 @@ export abstract class OpaqueKeyExchangeService { email: string, masterPassword: string, cipherConfiguration: OpaqueCipherConfiguration, - ): Promise; + ): Promise<{ + sessionId: string; + exportKey: Uint8Array; + }>; }