From 70d6337ec2bd874c2283a1c6e35e98323c21feb6 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:54:36 -0400 Subject: [PATCH] Innovation/OPAQUE - Add and cleanup some TODOs (#13873) * LoginStrategyServiceAbstraction - add TODO to refactor makePrePasswordLoginMasterKey in future * OpaqueLoginCredentials - add kdfConfig so we can derive master key for user verification scenarios. * LoginStrategyService.logIn - add TODO * OpaqueTokenRequest - add more docs * CipherConfiguration - add todo for more docs * DefaultOpaqueService - add todo * OpaqueLoginStrategy - (1) Add docs (2) clean up todos (3) add todos --- .../abstractions/login-strategy.service.ts | 3 +++ .../login-strategies/opaque-login.strategy.ts | 27 ++++++++++++------- .../common/models/domain/login-credentials.ts | 2 ++ .../login-strategy.service.ts | 3 +++ .../identity-token/opaque-token.request.ts | 4 +-- .../src/auth/opaque/default-opaque.service.ts | 3 +++ .../opaque/models/cipher-configuration.ts | 2 ++ 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 8c01e248685..70cd38597f5 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -70,6 +70,9 @@ export abstract class LoginStrategyServiceAbstraction { // TODO: PM-15162 - deprecate captchaResponse captchaResponse: string, ) => Promise; + + // TODO: PM-19273 - Refactor makePrePasswordLoginMasterKey to no longer be on the service + // once PM-18176 removes the Recover2faComponent dependency. /** * Creates a master key from the provided master password and email. * If a KdfConfig is provided, it will be used to generate the key. 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 084c83aa7a0..19021ecabf7 100644 --- a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts @@ -14,6 +14,7 @@ import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/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 { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration"; 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"; @@ -38,6 +39,10 @@ export class OpaqueLoginStrategyData implements LoginStrategyData { /** The user's master key */ masterKey: MasterKey; + /* The user's OPAQUE cipher configuration which controls + the encryption schemes used during key derivation and key exchange */ + cipherConfiguration: CipherConfiguration; + /** * Tracks if the user needs to be forced to update their password */ @@ -52,10 +57,12 @@ export class OpaqueLoginStrategyData implements LoginStrategyData { } } -// TODO: link to RFC and give simple, brief explanation of the protocol /** * - * A login strategy that uses the ... + * A login strategy that uses the OPAQUE protocol for password authentication. + * OPAQUE (Oblivious Pseudorandom Function (OPRF)-based Password Authentication and Key Exchange) + * is a protocol that allows a client to authenticate to a server without revealing the password to the server. + * RFC: https://www.ietf.org/archive/id/draft-irtf-cfrg-opaque-03.html */ export class OpaqueLoginStrategy extends BaseLoginStrategy { /** The email address of the user attempting to log in. */ @@ -81,19 +88,20 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); } - // TODO: build OpaqueLoginCredentials override async logIn(credentials: OpaqueLoginCredentials) { - const { email, masterPassword, twoFactor } = credentials; + const { email, masterPassword, kdfConfig, cipherConfiguration, twoFactor } = credentials; const data = new OpaqueLoginStrategyData(); - // TODO: we will still generate a master key here but we need to extract the prelogin call out of the makePreloginKey - // and simply rename it deriveMasterKey or something similar + data.userEnteredEmail = email; + + // Even though we are completing OPAQUE authN and not logging in with password hash, + // we still need to hash the master password for logged in user verification scenarios. data.masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey( masterPassword, email, + kdfConfig, ); - data.userEnteredEmail = email; // Hash the password early (before authentication) so we don't persist it in memory in plaintext data.localMasterKeyHash = await this.keyService.hashMasterKey( @@ -102,12 +110,10 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { HashPurpose.LocalAuthorization, ); - // const serverMasterKeyHash = await this.keyService.hashMasterKey(masterPassword, data.masterKey); + data.cipherConfiguration = cipherConfiguration; - // TODO: we must figure out how we will handle 2FA at some point. data.tokenRequest = new OpaqueTokenRequest( email, - undefined, await this.buildTwoFactor(twoFactor, email), await this.buildDeviceRequest(), ); @@ -193,6 +199,7 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { // We still need this for local user verification scenarios await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); + // TODO: why not re-use master key from strategy data cache? const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index c7c07ec8653..4b6355e2a3d 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -44,6 +44,7 @@ export class PasswordLoginCredentials { ? new OpaqueLoginCredentials( this.email, this.masterPassword, + preLoginResponse.toKdfConfig(), preLoginResponse.opaqueConfiguration, ) : new PasswordHashLoginCredentials( @@ -158,6 +159,7 @@ export class OpaqueLoginCredentials { constructor( public email: string, public masterPassword: string, + public kdfConfig: KdfConfig, public cipherConfiguration: CipherConfiguration, public twoFactor?: TokenTwoFactorRequest, ) {} 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 73041b45067..dee653ece9c 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 @@ -226,6 +226,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // Password credentials may use the PasswordHashLoginStrategy or the OpaqueLoginStrategy if (credentials.type === AuthenticationType.Password) { const preLoginRequest = new PrePasswordLoginRequest(credentials.email); + + // TODO: OPAQUE: we have to save off whether or not to enroll the user in OPAQUE based on the + // response from the pre-password-login request and execute the enroll in the password login strategy const preLoginResponse = await this.prePasswordLoginApiService.postPrePasswordLogin(preLoginRequest); ownedCredentials = credentials.toSpecificLoginCredentials(preLoginResponse); 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 512e44c16de..4c587b1f09d 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 @@ -6,10 +6,10 @@ import { TokenTwoFactorRequest } from "./token-two-factor.request"; import { TokenRequest } from "./token.request"; // TODO: we might have to support both login start and login finish requests within this? +// or, we could have separate OpaqueStartTokenRequest and OpaqueFinishTokenRequest classes export class OpaqueTokenRequest extends TokenRequest { constructor( public email: string, - public masterPasswordHash: string, protected twoFactor: TokenTwoFactorRequest, device?: DeviceRequest, public newDeviceOtp?: string, @@ -20,9 +20,9 @@ export class OpaqueTokenRequest extends TokenRequest { toIdentityToken(clientId: ClientType) { const obj = super.toIdentityToken(clientId); + // TODO: what grant type for OPAQUE? obj.grant_type = "password"; obj.username = this.email; - obj.password = this.masterPasswordHash; if (this.newDeviceOtp) { obj.newDeviceOtp = this.newDeviceOtp; diff --git a/libs/common/src/auth/opaque/default-opaque.service.ts b/libs/common/src/auth/opaque/default-opaque.service.ts index ee1fdaf954b..17b9b28f05e 100644 --- a/libs/common/src/auth/opaque/default-opaque.service.ts +++ b/libs/common/src/auth/opaque/default-opaque.service.ts @@ -69,6 +69,9 @@ export class DefaultOpaqueService implements OpaqueService { return registrationStartResponse.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. async login( email: string, masterPassword: string, diff --git a/libs/common/src/auth/opaque/models/cipher-configuration.ts b/libs/common/src/auth/opaque/models/cipher-configuration.ts index db91ab2febb..d5a6731db32 100644 --- a/libs/common/src/auth/opaque/models/cipher-configuration.ts +++ b/libs/common/src/auth/opaque/models/cipher-configuration.ts @@ -1,5 +1,7 @@ import { CipherConfiguration as CipherConfigurationSdk } from "@bitwarden/sdk-internal"; +// TODO: add js docs to all types / classes here. + export type CipherSuite = OPAQUEKE3_RISTRETTO255_3DH_ARGON2ID13_SUITE; export type OPAQUEKE3_RISTRETTO255_3DH_ARGON2ID13_SUITE = "OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF";