From a2ba965abd9bc9dab3461847b7dcc89dfff2fa36 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 17 Mar 2025 06:41:46 -0400 Subject: [PATCH] PM-19061 - Innovation Sprint - add OPAQUE Login Strategy (#13832) * ChangePassword - add TODOs to clean up code * LoginComp - Add TODOs for identifying the login strategy ahead of time. * DefaultOpaqueService - Add TODOs * PasswordLoginStrategy - add TODO for renaming * WIP first draft of opaque login strategy * Per discussion with platform, we don't need an abstraction for api services so clean that up. * Extract pre-login method into own service from ApiService + move request model to auth * LoginStrategyService - add todo for adding support for opaque login strategy * PreLoginApiService - add renaming todo * LoginComp + PasswordLoginCredentials - (1) Start integrating pre-login logic into login comp (2) update PasswordLoginCredentials to include kdfConfig to pass into login strat * LoginStrategyServiceAbstraction - login - add OpaqueLoginCredentials * CLI - add todos * LoginComp - add TODO * Add createKdfConfig factory function * LoginStrategyService: switch out to more specific password strategy * Fix type errors * Add jsdoc * Revert / remove TODOs and old draft work * add missing dep * PreLoginResponse - Adjust KM import * PreLogin renamed to PrePasswordLogin * Renames + some login strategy service test updates * LoginComp - remove unused import * KdfConfig - Rename validateKdfConfigForPrelogin to validateKdfConfigForPreLogin * LoginStrategyService - (1) Rename makePreloginKey to makePrePasswordLoginMasterKey (2) Refactor makePrePasswordLoginMasterKey to accept an optional KdfConfig so we can keep the logic tested on the LoginStrategyService * LoginStrategyService - add TODOs * Fix non-sdk build errors --------- Co-authored-by: Thomas Rittson --- apps/cli/src/auth/commands/login.command.ts | 2 + .../app/auth/recover-two-factor.component.ts | 12 +- .../settings/change-password.component.ts | 9 +- .../src/services/jslib-services.module.ts | 8 +- .../abstractions/login-strategy.service.ts | 10 +- .../auth-request-login.strategy.spec.ts | 2 +- .../auth-request-login.strategy.ts | 6 +- ...gy.spec.ts => base-login.strategy.spec.ts} | 0 ...gin.strategy.ts => base-login.strategy.ts} | 12 +- .../opaque-login.strategy.spec.ts | 1 + .../login-strategies/opaque-login.strategy.ts | 262 ++++++++++++++++++ .../password-login.strategy.spec.ts | 4 +- .../password-login.strategy.ts | 22 +- .../sso-login.strategy.spec.ts | 2 +- .../login-strategies/sso-login.strategy.ts | 6 +- .../user-api-login.strategy.spec.ts | 2 +- .../user-api-login.strategy.ts | 6 +- .../webauthn-login.strategy.spec.ts | 2 +- .../webauthn-login.strategy.ts | 6 +- .../common/models/domain/login-credentials.ts | 58 ++++ .../login-strategy.service.spec.ts | 28 +- .../login-strategy.service.ts | 203 +++++++------- .../login-strategies/login-strategy.state.ts | 3 + libs/common/src/abstractions/api.service.ts | 7 +- .../src/auth/enums/authentication-type.ts | 2 + .../identity-token/opaque-token.request.ts | 46 +++ .../request/pre-password-login.request.ts} | 2 +- ...onse.ts => pre-password-login.response.ts} | 8 +- .../auth/opaque/default-opaque-api.service.ts | 74 ----- .../src/auth/opaque/opaque-api.service.ts | 69 ++++- .../pre-password-login-api.service.ts | 31 +++ libs/common/src/services/api.service.ts | 15 - libs/key-management/src/index.ts | 1 + .../src/models/kdf-config.spec.ts | 10 +- libs/key-management/src/models/kdf-config.ts | 18 +- 35 files changed, 695 insertions(+), 254 deletions(-) rename libs/auth/src/common/login-strategies/{login.strategy.spec.ts => base-login.strategy.spec.ts} (100%) rename libs/auth/src/common/login-strategies/{login.strategy.ts => base-login.strategy.ts} (98%) create mode 100644 libs/auth/src/common/login-strategies/opaque-login.strategy.spec.ts create mode 100644 libs/auth/src/common/login-strategies/opaque-login.strategy.ts create mode 100644 libs/common/src/auth/models/request/identity-token/opaque-token.request.ts rename libs/common/src/{models/request/prelogin.request.ts => auth/models/request/pre-password-login.request.ts} (66%) rename libs/common/src/auth/models/response/{prelogin.response.ts => pre-password-login.response.ts} (78%) delete mode 100644 libs/common/src/auth/opaque/default-opaque-api.service.ts create mode 100644 libs/common/src/auth/services/pre-password-login-api.service.ts diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 985816fb0dd..7cd206a77cf 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -225,6 +225,8 @@ export class LoginCommand { "Encryption key migration required. Please login through the web vault to update your encryption key.", ); } + // TODO: PM-15162 - captcha is deprecated as part of UI refresh work + if (response.captchaSiteKey) { const credentials = new PasswordLoginCredentials(email, password); const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials); diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 996b324ce56..430f6bdc600 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -88,8 +88,16 @@ export class RecoverTwoFactorComponent implements OnInit { const request = new TwoFactorRecoveryRequest(); request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase(); request.email = this.email.trim().toLowerCase(); - const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email); - request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key); + + const masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey( + this.masterPassword, + request.email, + ); + + request.masterPasswordHash = await this.keyService.hashMasterKey( + this.masterPassword, + masterKey, + ); if (this.recoveryCodeLoginFeatureFlagEnabled) { await this.handleRecoveryLogin(request); 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 082dc3de49b..68ac9f5f1b2 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -231,11 +231,10 @@ export class ChangePasswordComponent this.formPromise = this.apiService.postPassword(request); } - await this.formPromise; - await this.opaqueService.login(this.email, this.masterPassword, { - memory: 256 * 1024, - iterations: 3, - parallelism: 4, + // TODO: remove this test code + await this.opaqueService.register(this.masterPassword, newUserKey[0], { + algorithm: "argon2id", + parameters: { memory: 256 * 1024, iterations: 3, parallelism: 4 }, }); this.toastService.showToast({ diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 14a59b2741a..1021ac1b4ed 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -104,7 +104,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@ import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; -import { DefaultOpaqueApiService } from "@bitwarden/common/auth/opaque/default-opaque-api.service"; import { DefaultOpaqueService } from "@bitwarden/common/auth/opaque/default-opaque.service"; import { OpaqueApiService } from "@bitwarden/common/auth/opaque/opaque-api.service"; import { OpaqueService } from "@bitwarden/common/auth/opaque/opaque.service"; @@ -119,6 +118,7 @@ import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; +import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -472,6 +472,7 @@ const safeProviders: SafeProvider[] = [ VaultTimeoutSettingsService, KdfConfigService, TaskSchedulerService, + PrePasswordLoginApiService, ], }), safeProvider({ @@ -1479,9 +1480,12 @@ const safeProviders: SafeProvider[] = [ ToastService, ], }), + safeProvider({ + provide: PrePasswordLoginApiService, + deps: [ApiServiceAbstraction, EnvironmentService], + }), safeProvider({ provide: OpaqueApiService, - useClass: DefaultOpaqueApiService, deps: [ApiServiceAbstraction, EnvironmentService], }), safeProvider({ diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index e9fa780b0fe..8c01e248685 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -6,6 +6,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { MasterKey } from "@bitwarden/common/types/key"; +import { KdfConfig } from "@bitwarden/key-management"; import { UserApiLoginCredentials, @@ -71,8 +72,15 @@ export abstract class LoginStrategyServiceAbstraction { ) => Promise; /** * Creates a master key from the provided master password and email. + * If a KdfConfig is provided, it will be used to generate the key. + * Otherwise, the PrePasswordLogin endpoint will be used to retrieve the user's + * KdfConfig. */ - makePreloginKey: (masterPassword: string, email: string) => Promise; + makePrePasswordLoginMasterKey: ( + masterPassword: string, + email: string, + kdfConfig?: KdfConfig, + ) => Promise; /** * Emits true if the authentication session has expired. */ diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 0646da4862b..f0fead336dd 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -34,7 +34,7 @@ import { AuthRequestLoginStrategy, AuthRequestLoginStrategyData, } from "./auth-request-login.strategy"; -import { identityTokenResponseFactory } from "./login.strategy.spec"; +import { identityTokenResponseFactory } from "./base-login.strategy.spec"; describe("AuthRequestLoginStrategy", () => { let cache: AuthRequestLoginStrategyData; diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index e546f89032b..82913c24dc1 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -13,7 +13,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -import { LoginStrategy, LoginStrategyData } from "./login.strategy"; +import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy"; export class AuthRequestLoginStrategyData implements LoginStrategyData { tokenRequest: PasswordTokenRequest; @@ -29,7 +29,7 @@ export class AuthRequestLoginStrategyData implements LoginStrategyData { } } -export class AuthRequestLoginStrategy extends LoginStrategy { +export class AuthRequestLoginStrategy extends BaseLoginStrategy { email$: Observable; accessCode$: Observable; authRequestId$: Observable; @@ -39,7 +39,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, private deviceTrustService: DeviceTrustServiceAbstraction, - ...sharedDeps: ConstructorParameters + ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/base-login.strategy.spec.ts similarity index 100% rename from libs/auth/src/common/login-strategies/login.strategy.spec.ts rename to libs/auth/src/common/login-strategies/base-login.strategy.spec.ts diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/base-login.strategy.ts similarity index 98% rename from libs/auth/src/common/login-strategies/login.strategy.ts rename to libs/auth/src/common/login-strategies/base-login.strategy.ts index 89802c609c0..3c47ef9b5d0 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/base-login.strategy.ts @@ -9,6 +9,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request"; +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 { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; @@ -45,10 +46,11 @@ import { import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials, - PasswordLoginCredentials, SsoLoginCredentials, AuthRequestLoginCredentials, WebAuthnLoginCredentials, + OpaqueLoginCredentials, + PasswordHashLoginCredentials, } from "../models/domain/login-credentials"; import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -63,6 +65,7 @@ export abstract class LoginStrategyData { tokenRequest: | UserApiTokenRequest | PasswordTokenRequest + | OpaqueTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest | undefined; @@ -72,7 +75,7 @@ export abstract class LoginStrategyData { abstract userEnteredEmail?: string; } -export abstract class LoginStrategy { +export abstract class BaseLoginStrategy { protected abstract cache: BehaviorSubject; protected sessionTimeoutSubject = new BehaviorSubject(false); sessionTimeout$: Observable = this.sessionTimeoutSubject.asObservable(); @@ -102,10 +105,11 @@ export abstract class LoginStrategy { abstract logIn( credentials: | UserApiLoginCredentials - | PasswordLoginCredentials + | PasswordHashLoginCredentials | SsoLoginCredentials | AuthRequestLoginCredentials - | WebAuthnLoginCredentials, + | WebAuthnLoginCredentials + | OpaqueLoginCredentials, ): Promise; async logInTwoFactor( diff --git a/libs/auth/src/common/login-strategies/opaque-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/opaque-login.strategy.spec.ts new file mode 100644 index 00000000000..99b008049c1 --- /dev/null +++ b/libs/auth/src/common/login-strategies/opaque-login.strategy.spec.ts @@ -0,0 +1 @@ +// TODO: add tests for OpaqueLoginStrategy once it is implemented diff --git a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts new file mode 100644 index 00000000000..084c83aa7a0 --- /dev/null +++ b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts @@ -0,0 +1,262 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +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 { 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"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey } from "@bitwarden/common/types/key"; + +import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { OpaqueLoginCredentials } from "../models/domain/login-credentials"; +import { CacheData } from "../services/login-strategies/login-strategy.state"; + +import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy"; + +export class OpaqueLoginStrategyData implements LoginStrategyData { + tokenRequest: OpaqueTokenRequest; + + /** User's entered email obtained pre-login. Always present in MP login. */ + userEnteredEmail: string; + + /** The local version of the user's master key hash */ + localMasterKeyHash: string; + + /** The user's master key */ + masterKey: MasterKey; + + /** + * Tracks if the user needs to be forced to update their password + */ + forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None; + + static fromJSON(obj: Jsonify): OpaqueLoginStrategyData { + const data = Object.assign(new OpaqueLoginStrategyData(), obj, { + tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest), + masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey), + }); + return data; + } +} + +// TODO: link to RFC and give simple, brief explanation of the protocol +/** + * + * A login strategy that uses the ... + */ +export class OpaqueLoginStrategy extends BaseLoginStrategy { + /** The email address of the user attempting to log in. */ + email$: Observable; + + /** The local master key hash we store client side */ + localMasterKeyHash$: Observable; + + protected cache: BehaviorSubject; + + constructor( + data: OpaqueLoginStrategyData, + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private policyService: PolicyService, + private loginStrategyService: LoginStrategyServiceAbstraction, + ...sharedDeps: ConstructorParameters + ) { + super(...sharedDeps); + + this.cache = new BehaviorSubject(data); + this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email)); + + this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); + } + + // TODO: build OpaqueLoginCredentials + override async logIn(credentials: OpaqueLoginCredentials) { + const { email, masterPassword, 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.masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey( + masterPassword, + email, + ); + 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( + masterPassword, + data.masterKey, + HashPurpose.LocalAuthorization, + ); + + // const serverMasterKeyHash = await this.keyService.hashMasterKey(masterPassword, data.masterKey); + + // 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(), + ); + + this.cache.next(data); + + const [authResult, identityResponse] = await this.startLogIn(); + + if (identityResponse instanceof IdentityCaptchaResponse) { + return authResult; + } + + const masterPasswordPolicyOptions = + this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); + + // The identity result can contain master password policies for the user's organizations + if (masterPasswordPolicyOptions?.enforceOnLogin) { + // If there is a policy active, evaluate the supplied password before its no longer in memory + const meetsRequirements = this.evaluateMasterPassword( + credentials, + masterPasswordPolicyOptions, + ); + if (meetsRequirements) { + return authResult; + } + + if (identityResponse instanceof IdentityTwoFactorResponse) { + // Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope + this.cache.next({ + ...this.cache.value, + forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword, + }); + } else { + // Authentication was successful, save the force update password options with the state service + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + authResult.userId, // userId is only available on successful login + ); + authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; + } + } + return authResult; + } + + override async logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise { + const data = this.cache.value; + this.cache.next(data); + + const result = await super.logInTwoFactor(twoFactor); + + // 2FA was successful, save the force update password options with the state service if defined + const forcePasswordResetReason = this.cache.value.forcePasswordResetReason; + if ( + !result.requiresTwoFactor && + !result.requiresCaptcha && + forcePasswordResetReason != ForceSetPasswordReason.None + ) { + await this.masterPasswordService.setForceSetPasswordReason( + forcePasswordResetReason, + result.userId, + ); + result.forcePasswordReset = forcePasswordResetReason; + } + + return result; + } + + protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { + const { masterKey, localMasterKeyHash } = this.cache.value; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); + } + + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { + // If migration is required, we won't have a user key to set yet. + if (this.encryptionKeyMigrationRequired(response)) { + return; + } + + // We still need this for local user verification scenarios + await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); + + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + if (masterKey) { + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( + masterKey, + userId, + ); + await this.keyService.setUserKey(userKey, userId); + } + } + + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { + await this.keyService.setPrivateKey( + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, + ); + } + + protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean { + return !response.key; + } + + private getMasterPasswordPolicyOptionsFromResponse( + response: + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse, + ): MasterPasswordPolicyOptions | null { + if ( + response == null || + response instanceof IdentityDeviceVerificationResponse || + response.masterPasswordPolicy == null + ) { + return null; + } + return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); + } + + private evaluateMasterPassword( + { masterPassword, email }: OpaqueLoginCredentials, + options: MasterPasswordPolicyOptions, + ): boolean { + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + email, + )?.score; + + return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options); + } + + exportCache(): CacheData { + return { + opaque: this.cache.value, + }; + } + + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + const data = this.cache.value; + data.tokenRequest.newDeviceOtp = deviceVerificationOtp; + this.cache.next(data); + + const [authResult] = await this.startLogIn(); + return authResult; + } +} diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 0821405e535..d512ff60901 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -40,7 +40,7 @@ import { LoginStrategyServiceAbstraction } from "../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { PasswordLoginCredentials } from "../models/domain/login-credentials"; -import { identityTokenResponseFactory } from "./login.strategy.spec"; +import { identityTokenResponseFactory } from "./base-login.strategy.spec"; import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; const email = "hello@world.com"; @@ -115,7 +115,7 @@ describe("PasswordLoginStrategy", () => { sub: userId, }); - loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); + loginStrategyService.makePrePasswordLoginMasterKey.mockResolvedValue(masterKey); keyService.hashMasterKey .calledWith(masterPassword, expect.anything(), undefined) 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 f0a8d40f914..ce64b0a547e 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -20,11 +20,12 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; -import { PasswordLoginCredentials } from "../models/domain/login-credentials"; +import { PasswordHashLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -import { LoginStrategy, LoginStrategyData } from "./login.strategy"; +import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy"; +// TODO: consider renaming LegacyPasswordLoginStrategy? Or PasswordHashLoginStrategy? export class PasswordLoginStrategyData implements LoginStrategyData { tokenRequest: PasswordTokenRequest; @@ -51,7 +52,7 @@ export class PasswordLoginStrategyData implements LoginStrategyData { } } -export class PasswordLoginStrategy extends LoginStrategy { +export class PasswordLoginStrategy extends BaseLoginStrategy { /** The email address of the user attempting to log in. */ email$: Observable; /** The master key hash used for authentication */ @@ -66,7 +67,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private passwordStrengthService: PasswordStrengthServiceAbstraction, private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, - ...sharedDeps: ConstructorParameters + ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); @@ -78,11 +79,16 @@ export class PasswordLoginStrategy extends LoginStrategy { this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); } - override async logIn(credentials: PasswordLoginCredentials) { - const { email, masterPassword, captchaToken, twoFactor } = credentials; + override async logIn(credentials: PasswordHashLoginCredentials) { + const { email, masterPassword, captchaToken, twoFactor, kdfConfig } = credentials; const data = new PasswordLoginStrategyData(); - data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email); + + 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 @@ -221,7 +227,7 @@ export class PasswordLoginStrategy extends LoginStrategy { } private evaluateMasterPassword( - { masterPassword, email }: PasswordLoginCredentials, + { masterPassword, email }: PasswordHashLoginCredentials, options: MasterPasswordPolicyOptions, ): boolean { const passwordStrength = this.passwordStrengthService.getPasswordStrength( diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 6efb17a8d26..187d83cf5bf 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -39,7 +39,7 @@ import { } from "../abstractions"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; -import { identityTokenResponseFactory } from "./login.strategy.spec"; +import { identityTokenResponseFactory } from "./base-login.strategy.spec"; import { SsoLoginStrategy } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index ac00ff69a4d..c12110ea1df 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -19,7 +19,7 @@ import { AuthRequestServiceAbstraction } from "../abstractions"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -import { LoginStrategyData, LoginStrategy } from "./login.strategy"; +import { LoginStrategyData, BaseLoginStrategy } from "./base-login.strategy"; export class SsoLoginStrategyData implements LoginStrategyData { captchaBypassToken: string; @@ -51,7 +51,7 @@ export class SsoLoginStrategyData implements LoginStrategyData { } } -export class SsoLoginStrategy extends LoginStrategy { +export class SsoLoginStrategy extends BaseLoginStrategy { /** * @see {@link SsoLoginStrategyData.email} */ @@ -73,7 +73,7 @@ export class SsoLoginStrategy extends LoginStrategy { private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, - ...sharedDeps: ConstructorParameters + ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index c0c7e828b68..afe20c57e5e 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -32,7 +32,7 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; -import { identityTokenResponseFactory } from "./login.strategy.spec"; +import { identityTokenResponseFactory } from "./base-login.strategy.spec"; import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy"; describe("UserApiLoginStrategy", () => { diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 0bff20b4a65..716135585fe 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -12,7 +12,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -import { LoginStrategy, LoginStrategyData } from "./login.strategy"; +import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy"; export class UserApiLoginStrategyData implements LoginStrategyData { tokenRequest: UserApiTokenRequest; @@ -25,13 +25,13 @@ export class UserApiLoginStrategyData implements LoginStrategyData { } } -export class UserApiLoginStrategy extends LoginStrategy { +export class UserApiLoginStrategy extends BaseLoginStrategy { protected cache: BehaviorSubject; constructor( data: UserApiLoginStrategyData, private keyConnectorService: KeyConnectorService, - ...sharedDeps: ConstructorParameters + ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 837c6a2a910..1d33661f380 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -31,7 +31,7 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; -import { identityTokenResponseFactory } from "./login.strategy.spec"; +import { identityTokenResponseFactory } from "./base-login.strategy.spec"; import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-login.strategy"; describe("WebAuthnLoginStrategy", () => { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 97696a26699..ca157a86da9 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -14,7 +14,7 @@ import { UserKey } from "@bitwarden/common/types/key"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -import { LoginStrategy, LoginStrategyData } from "./login.strategy"; +import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy"; export class WebAuthnLoginStrategyData implements LoginStrategyData { tokenRequest: WebAuthnLoginTokenRequest; @@ -29,12 +29,12 @@ export class WebAuthnLoginStrategyData implements LoginStrategyData { } } -export class WebAuthnLoginStrategy extends LoginStrategy { +export class WebAuthnLoginStrategy extends BaseLoginStrategy { protected cache: BehaviorSubject; constructor( data: WebAuthnLoginStrategyData, - ...sharedDeps: ConstructorParameters + ...sharedDeps: ConstructorParameters ) { super(...sharedDeps); diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index 72cc7413bec..c7c07ec8653 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -4,10 +4,28 @@ import { Jsonify } from "type-fest"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { PrePasswordLoginResponse } from "@bitwarden/common/auth/models/response/pre-password-login.response"; +import { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { KdfConfig } from "../../../../../key-management/src"; + +export type LoginCredentials = + | PasswordLoginCredentials + | SsoLoginCredentials + | UserApiLoginCredentials + | AuthRequestLoginCredentials + | WebAuthnLoginCredentials + | PasswordHashLoginCredentials + | OpaqueLoginCredentials; + +/** + * Represents email and master password login credentials. + * This is not used directly by a LogInStrategy, rather it is transformed into a more specific + * PasswordHashLoginCredentials or OpaqueLoginCredentials depending on which strategy is to be used. + */ export class PasswordLoginCredentials { readonly type = AuthenticationType.Password; @@ -18,6 +36,22 @@ export class PasswordLoginCredentials { public captchaToken?: string, public twoFactor?: TokenTwoFactorRequest, ) {} + + toSpecificLoginCredentials( + preLoginResponse: PrePasswordLoginResponse, + ): PasswordHashLoginCredentials | OpaqueLoginCredentials { + return preLoginResponse.opaqueConfiguration + ? new OpaqueLoginCredentials( + this.email, + this.masterPassword, + preLoginResponse.opaqueConfiguration, + ) + : new PasswordHashLoginCredentials( + this.email, + this.masterPassword, + preLoginResponse.toKdfConfig(), + ); + } } export class SsoLoginCredentials { @@ -104,3 +138,27 @@ export class WebAuthnLoginCredentials { ); } } + +export class PasswordHashLoginCredentials { + readonly type = AuthenticationType.PasswordHash; + + constructor( + public email: string, + public masterPassword: string, + public kdfConfig: KdfConfig, + // TODO: PM-15162 - captcha is deprecated as part of UI refresh work + public captchaToken?: string, + public twoFactor?: TokenTwoFactorRequest, + ) {} +} + +export class OpaqueLoginCredentials { + readonly type = AuthenticationType.Opaque; + + constructor( + public email: string, + public masterPassword: string, + public cipherConfiguration: CipherConfiguration, + public twoFactor?: TokenTwoFactorRequest, + ) {} +} diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index d8d16fa3701..f2542410979 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -12,8 +12,9 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; -import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogin.response"; +import { PrePasswordLoginResponse } from "@bitwarden/common/auth/models/response/pre-password-login.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { @@ -48,6 +49,8 @@ import { UserDecryptionOptionsService } from "../user-decryption-options/user-de import { LoginStrategyService } from "./login-strategy.service"; import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; +// TODO: update tests to pass +// TODO: test makePrePasswordLoginMasterKey describe("LoginStrategyService", () => { let sut: LoginStrategyService; @@ -75,6 +78,7 @@ describe("LoginStrategyService", () => { let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let taskSchedulerService: MockProxy; + let prePasswordLoginApiService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -107,6 +111,7 @@ describe("LoginStrategyService", () => { vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); taskSchedulerService = mock(); + prePasswordLoginApiService = mock(); sut = new LoginStrategyService( accountService, @@ -134,6 +139,7 @@ describe("LoginStrategyService", () => { vaultTimeoutSettingsService, kdfConfigService, taskSchedulerService, + prePasswordLoginApiService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); @@ -173,8 +179,8 @@ describe("LoginStrategyService", () => { token_type: "Bearer", }), ); - apiService.postPrelogin.mockResolvedValue( - new PreloginResponse({ + prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue( + new PrePasswordLoginResponse({ Kdf: KdfType.Argon2id, KdfIterations: 2, KdfMemory: 16, @@ -207,8 +213,8 @@ describe("LoginStrategyService", () => { }), ); - apiService.postPrelogin.mockResolvedValue( - new PreloginResponse({ + prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue( + new PrePasswordLoginResponse({ Kdf: KdfType.Argon2id, KdfIterations: 2, KdfMemory: 16, @@ -266,8 +272,8 @@ describe("LoginStrategyService", () => { }), ); - apiService.postPrelogin.mockResolvedValue( - new PreloginResponse({ + prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue( + new PrePasswordLoginResponse({ Kdf: KdfType.Argon2id, KdfIterations: 2, KdfMemory: 16, @@ -305,8 +311,8 @@ describe("LoginStrategyService", () => { token_type: "Bearer", }), ); - apiService.postPrelogin.mockResolvedValue( - new PreloginResponse({ + prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue( + new PrePasswordLoginResponse({ Kdf: KdfType.PBKDF2_SHA256, KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, }), @@ -329,8 +335,8 @@ describe("LoginStrategyService", () => { const deviceVerificationOtp = "123456"; // Setup initial login and device verification response - apiService.postPrelogin.mockResolvedValue( - new PreloginResponse({ + prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue( + new PrePasswordLoginResponse({ Kdf: KdfType.Argon2id, KdfIterations: 2, KdfMemory: 16, 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 4068c09338b..73041b45067 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 @@ -20,10 +20,11 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { PrePasswordLoginRequest } from "@bitwarden/common/auth/models/request/pre-password-login.request"; +import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; -import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -36,14 +37,7 @@ import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/plat import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; -import { - KdfType, - KeyService, - Argon2KdfConfig, - KdfConfig, - PBKDF2KdfConfig, - KdfConfigService, -} from "@bitwarden/key-management"; +import { KeyService, KdfConfig, KdfConfigService } from "@bitwarden/key-management"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; @@ -51,7 +45,11 @@ import { AuthRequestLoginStrategy, AuthRequestLoginStrategyData, } from "../../login-strategies/auth-request-login.strategy"; -import { LoginStrategy } from "../../login-strategies/login.strategy"; +import { BaseLoginStrategy } from "../../login-strategies/base-login.strategy"; +import { + OpaqueLoginStrategy, + OpaqueLoginStrategyData, +} from "../../login-strategies/opaque-login.strategy"; import { PasswordLoginStrategy, PasswordLoginStrategyData, @@ -71,6 +69,7 @@ import { SsoLoginCredentials, AuthRequestLoginCredentials, WebAuthnLoginCredentials, + LoginCredentials, } from "../../models"; import { @@ -83,6 +82,14 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes +type LoginStrategy = + | UserApiLoginStrategy + | PasswordLoginStrategy + | SsoLoginStrategy + | AuthRequestLoginStrategy + | WebAuthnLoginStrategy + | OpaqueLoginStrategy; + export class LoginStrategyService implements LoginStrategyServiceAbstraction { private sessionTimeoutSubscription: Subscription | undefined; private currentAuthnTypeState: GlobalState; @@ -94,14 +101,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { authenticationSessionTimeout$: Observable = this.authenticationTimeoutSubject.asObservable(); - private loginStrategy$: Observable< - | UserApiLoginStrategy - | PasswordLoginStrategy - | SsoLoginStrategy - | AuthRequestLoginStrategy - | WebAuthnLoginStrategy - | null - >; + private loginStrategy$: Observable; currentAuthType$: Observable; @@ -131,6 +131,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, protected taskSchedulerService: TaskSchedulerService, + protected prePasswordLoginApiService: PrePasswordLoginApiService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -154,7 +155,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe( distinctUntilChanged(), combineLatestWith(this.loginStrategyCacheState.state$), - this.initializeLoginStrategy.bind(this), + map(([strategy, data]) => this.initializeLoginStrategy(strategy, data)), shareReplay({ refCount: true, bufferSize: 1 }), ); } @@ -215,16 +216,27 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { await this.clearCache(); this.authenticationTimeoutSubject.next(false); - await this.currentAuthnTypeState.update((_) => credentials.type); - - const strategy = await firstValueFrom(this.loginStrategy$); - // Note: We aren't passing the credentials directly to the strategy since they are // created in the popup and can cause DeadObject references on Firefox. // This is a shallow copy, but use deep copy in future if objects are added to credentials // that were created in popup. // If the popup uses its own instance of this service, this can be removed. - const ownedCredentials = { ...credentials }; + let ownedCredentials: LoginCredentials; + + // Password credentials may use the PasswordHashLoginStrategy or the OpaqueLoginStrategy + if (credentials.type === AuthenticationType.Password) { + const preLoginRequest = new PrePasswordLoginRequest(credentials.email); + const preLoginResponse = + await this.prePasswordLoginApiService.postPrePasswordLogin(preLoginRequest); + ownedCredentials = credentials.toSpecificLoginCredentials(preLoginResponse); + } else { + // Shallow copy + ownedCredentials = { ...credentials }; + } + + await this.currentAuthnTypeState.update((_) => credentials.type); + + const strategy = await firstValueFrom(this.loginStrategy$); const result = await strategy?.logIn(ownedCredentials as any); @@ -309,33 +321,33 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } } - async makePreloginKey(masterPassword: string, email: string): Promise { + async makePrePasswordLoginMasterKey( + masterPassword: string, + email: string, + kdfConfig?: KdfConfig, + ): Promise { email = email.trim().toLowerCase(); - let kdfConfig: KdfConfig | undefined; - try { - const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); - if (preloginResponse != null) { - kdfConfig = - preloginResponse.kdf === KdfType.PBKDF2_SHA256 - ? new PBKDF2KdfConfig(preloginResponse.kdfIterations) - : new Argon2KdfConfig( - preloginResponse.kdfIterations, - preloginResponse.kdfMemory, - preloginResponse.kdfParallelism, - ); - } - } catch (e: any) { - if (e == null || e.statusCode !== 404) { - throw e; - } - } if (!kdfConfig) { - throw new Error("KDF config is required"); - } - kdfConfig.validateKdfConfigForPrelogin(); + try { + const preloginResponse = await this.prePasswordLoginApiService.postPrePasswordLogin( + new PrePasswordLoginRequest(email), + ); + kdfConfig = preloginResponse?.toKdfConfig(); + } catch (e: any) { + if (e == null || e.statusCode !== 404) { + throw e; + } + } - return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); + if (!kdfConfig) { + throw new Error("KDF config is required"); + } + } + + kdfConfig.validateKdfConfigForPreLogin(); + + return this.keyService.makeMasterKey(masterPassword, email, kdfConfig); } private async clearCache(): Promise { @@ -383,9 +395,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } private initializeLoginStrategy( - source: Observable<[AuthenticationType | null, CacheData | null]>, - ) { - const sharedDeps: ConstructorParameters = [ + strategy: AuthenticationType | null, + data: CacheData | null, + ): LoginStrategy | null { + const sharedDeps: ConstructorParameters = [ this.accountService, this.masterPasswordService, this.keyService, @@ -404,49 +417,51 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.kdfConfigService, this.environmentService, ]; - - return source.pipe( - map(([strategy, data]) => { - if (strategy == null) { - return null; - } - switch (strategy) { - case AuthenticationType.Password: - return new PasswordLoginStrategy( - data?.password ?? new PasswordLoginStrategyData(), - this.passwordStrengthService, - this.policyService, - this, - ...sharedDeps, - ); - case AuthenticationType.Sso: - return new SsoLoginStrategy( - data?.sso ?? new SsoLoginStrategyData(), - this.keyConnectorService, - this.deviceTrustService, - this.authRequestService, - this.i18nService, - ...sharedDeps, - ); - case AuthenticationType.UserApiKey: - return new UserApiLoginStrategy( - data?.userApiKey ?? new UserApiLoginStrategyData(), - this.keyConnectorService, - ...sharedDeps, - ); - case AuthenticationType.AuthRequest: - return new AuthRequestLoginStrategy( - data?.authRequest ?? new AuthRequestLoginStrategyData(), - this.deviceTrustService, - ...sharedDeps, - ); - case AuthenticationType.WebAuthn: - return new WebAuthnLoginStrategy( - data?.webAuthn ?? new WebAuthnLoginStrategyData(), - ...sharedDeps, - ); - } - }), - ); + switch (strategy) { + case AuthenticationType.PasswordHash: + return new PasswordLoginStrategy( + data?.password ?? new PasswordLoginStrategyData(), + this.passwordStrengthService, + this.policyService, + this, + ...sharedDeps, + ); + case AuthenticationType.Sso: + return new SsoLoginStrategy( + data?.sso ?? new SsoLoginStrategyData(), + this.keyConnectorService, + this.deviceTrustService, + this.authRequestService, + this.i18nService, + ...sharedDeps, + ); + case AuthenticationType.UserApiKey: + return new UserApiLoginStrategy( + data?.userApiKey ?? new UserApiLoginStrategyData(), + this.keyConnectorService, + ...sharedDeps, + ); + case AuthenticationType.AuthRequest: + return new AuthRequestLoginStrategy( + data?.authRequest ?? new AuthRequestLoginStrategyData(), + this.deviceTrustService, + ...sharedDeps, + ); + case AuthenticationType.WebAuthn: + return new WebAuthnLoginStrategy( + data?.webAuthn ?? new WebAuthnLoginStrategyData(), + ...sharedDeps, + ); + case AuthenticationType.Opaque: + return new OpaqueLoginStrategy( + data?.opaque ?? new OpaqueLoginStrategyData(), + this.passwordStrengthService, + this.policyService, + this, + ...sharedDeps, + ); + default: + return null; + } } } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.state.ts b/libs/auth/src/common/services/login-strategies/login-strategy.state.ts index 1592c51453c..285d0732558 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.state.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.state.ts @@ -4,6 +4,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { KeyDefinition, LOGIN_STRATEGY_MEMORY } from "@bitwarden/common/platform/state"; import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy"; +import { OpaqueLoginStrategyData } from "../../login-strategies/opaque-login.strategy"; import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy"; import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy"; import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy"; @@ -48,6 +49,7 @@ export const AUTH_REQUEST_PUSH_NOTIFICATION_KEY = new KeyDefinition( } return { password: data.password ? PasswordLoginStrategyData.fromJSON(data.password) : undefined, + opaque: data.opaque ? OpaqueLoginStrategyData.fromJSON(data.opaque) : undefined, sso: data.sso ? SsoLoginStrategyData.fromJSON(data.sso) : undefined, userApiKey: data.userApiKey ? UserApiLoginStrategyData.fromJSON(data.userApiKey) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index fe3f356719b..107d227807e 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -43,6 +43,7 @@ import { DeviceVerificationRequest } from "../auth/models/request/device-verific import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; import { EmailRequest } from "../auth/models/request/email.request"; +import { OpaqueTokenRequest } from "../auth/models/request/identity-token/opaque-token.request"; import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request"; import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request"; @@ -74,7 +75,6 @@ import { IdentityDeviceVerificationResponse } from "../auth/models/response/iden import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; -import { PreloginResponse } from "../auth/models/response/prelogin.response"; import { RegisterResponse } from "../auth/models/response/register.response"; import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response"; import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response"; @@ -100,7 +100,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; import { EventRequest } from "../models/request/event.request"; import { KdfRequest } from "../models/request/kdf.request"; import { KeysRequest } from "../models/request/keys.request"; -import { PreloginRequest } from "../models/request/prelogin.request"; import { RegisterRequest } from "../models/request/register.request"; import { StorageRequest } from "../models/request/storage.request"; import { UpdateAvatarRequest } from "../models/request/update-avatar.request"; @@ -151,7 +150,8 @@ export abstract class ApiService { | PasswordTokenRequest | SsoTokenRequest | UserApiTokenRequest - | WebAuthnLoginTokenRequest, + | WebAuthnLoginTokenRequest + | OpaqueTokenRequest, ) => Promise< | IdentityTokenResponse | IdentityTwoFactorResponse @@ -166,7 +166,6 @@ export abstract class ApiService { putProfile: (request: UpdateProfileRequest) => Promise; putAvatar: (request: UpdateAvatarRequest) => Promise; putTaxInfo: (request: TaxInfoUpdateRequest) => Promise; - postPrelogin: (request: PreloginRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; diff --git a/libs/common/src/auth/enums/authentication-type.ts b/libs/common/src/auth/enums/authentication-type.ts index 35b50e6400a..30dda1fb206 100644 --- a/libs/common/src/auth/enums/authentication-type.ts +++ b/libs/common/src/auth/enums/authentication-type.ts @@ -4,4 +4,6 @@ export enum AuthenticationType { UserApiKey = 2, AuthRequest = 3, WebAuthn = 4, + PasswordHash = 5, + Opaque = 6, } 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 new file mode 100644 index 00000000000..512e44c16de --- /dev/null +++ b/libs/common/src/auth/models/request/identity-token/opaque-token.request.ts @@ -0,0 +1,46 @@ +import { ClientType } from "../../../../enums"; +import { Utils } from "../../../../platform/misc/utils"; + +import { DeviceRequest } from "./device.request"; +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? +export class OpaqueTokenRequest extends TokenRequest { + constructor( + public email: string, + public masterPasswordHash: string, + protected twoFactor: TokenTwoFactorRequest, + device?: DeviceRequest, + public newDeviceOtp?: string, + ) { + super(twoFactor, device); + } + + toIdentityToken(clientId: ClientType) { + const obj = super.toIdentityToken(clientId); + + obj.grant_type = "password"; + obj.username = this.email; + obj.password = this.masterPasswordHash; + + if (this.newDeviceOtp) { + obj.newDeviceOtp = this.newDeviceOtp; + } + + return obj; + } + + alterIdentityTokenHeaders(headers: Headers) { + headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email)); + } + + static fromJSON(json: any) { + return Object.assign(Object.create(OpaqueTokenRequest.prototype), json, { + device: json.device ? DeviceRequest.fromJSON(json.device) : undefined, + twoFactor: json.twoFactor + ? Object.assign(new TokenTwoFactorRequest(), json.twoFactor) + : undefined, + }); + } +} diff --git a/libs/common/src/models/request/prelogin.request.ts b/libs/common/src/auth/models/request/pre-password-login.request.ts similarity index 66% rename from libs/common/src/models/request/prelogin.request.ts rename to libs/common/src/auth/models/request/pre-password-login.request.ts index 689204b79ef..2210bff5f4a 100644 --- a/libs/common/src/models/request/prelogin.request.ts +++ b/libs/common/src/auth/models/request/pre-password-login.request.ts @@ -1,4 +1,4 @@ -export class PreloginRequest { +export class PrePasswordLoginRequest { email: string; constructor(email: string) { diff --git a/libs/common/src/auth/models/response/prelogin.response.ts b/libs/common/src/auth/models/response/pre-password-login.response.ts similarity index 78% rename from libs/common/src/auth/models/response/prelogin.response.ts rename to libs/common/src/auth/models/response/pre-password-login.response.ts index b54cda98073..c7fa1cecc20 100644 --- a/libs/common/src/auth/models/response/prelogin.response.ts +++ b/libs/common/src/auth/models/response/pre-password-login.response.ts @@ -1,9 +1,9 @@ -import { KdfType } from "@bitwarden/key-management"; +import { KdfType, createKdfConfig } from "@bitwarden/key-management"; import { BaseResponse } from "../../../models/response/base.response"; import { CipherConfiguration } from "../../opaque/models/cipher-configuration"; -export class PreloginResponse extends BaseResponse { +export class PrePasswordLoginResponse extends BaseResponse { kdf: KdfType; kdfIterations: number; kdfMemory?: number; @@ -19,4 +19,8 @@ export class PreloginResponse extends BaseResponse { this.kdfParallelism = this.getResponseProperty("KdfParallelism"); this.opaqueConfiguration = this.getResponseProperty("OpaqueConfiguration"); } + + toKdfConfig() { + return createKdfConfig(this); + } } diff --git a/libs/common/src/auth/opaque/default-opaque-api.service.ts b/libs/common/src/auth/opaque/default-opaque-api.service.ts deleted file mode 100644 index 5448fcf5bc1..00000000000 --- a/libs/common/src/auth/opaque/default-opaque-api.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; - -import { LoginFinishRequest } from "./models/login-finish.request"; -import { LoginStartRequest } from "./models/login-start.request"; -import { LoginStartResponse } from "./models/login-start.response"; -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 { OpaqueApiService } from "./opaque-api.service"; - -export class DefaultOpaqueApiService implements OpaqueApiService { - constructor( - private apiService: ApiService, - private environmentService: EnvironmentService, - ) {} - - async registrationStart(request: RegistrationStartRequest): Promise { - const env = await firstValueFrom(this.environmentService.environment$); - const response = await this.apiService.send( - "POST", - `/opaque/start-registration`, - request, - true, - true, - env.getApiUrl(), - ); - return new RegistrationStartResponse(response); - } - - async registrationFinish( - request: RegistrationFinishRequest, - ): Promise { - const env = await firstValueFrom(this.environmentService.environment$); - const response = await this.apiService.send( - "POST", - `/opaque/finish-registration`, - request, - true, - true, - env.getApiUrl(), - ); - return new RegistrationFinishResponse(response); - } - - async loginStart(request: LoginStartRequest): Promise { - const env = await firstValueFrom(this.environmentService.environment$); - const response = await this.apiService.send( - "POST", - `/opaque/start-login`, - request, - true, - true, - env.getApiUrl(), - ); - return new LoginStartResponse(response); - } - - async loginFinish(request: LoginFinishRequest): Promise { - const env = await firstValueFrom(this.environmentService.environment$); - const response = await this.apiService.send( - "POST", - `/opaque/finish-login`, - request, - true, - true, - env.getApiUrl(), - ); - return response.success; - } -} diff --git a/libs/common/src/auth/opaque/opaque-api.service.ts b/libs/common/src/auth/opaque/opaque-api.service.ts index e694f5ad053..33911d121bf 100644 --- a/libs/common/src/auth/opaque/opaque-api.service.ts +++ b/libs/common/src/auth/opaque/opaque-api.service.ts @@ -1,3 +1,8 @@ +import { firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; + import { LoginFinishRequest } from "./models/login-finish.request"; import { LoginStartRequest } from "./models/login-start.request"; import { LoginStartResponse } from "./models/login-start.response"; @@ -6,11 +11,63 @@ import { RegistrationFinishResponse } from "./models/registration-finish.respons import { RegistrationStartRequest } from "./models/registration-start.request"; import { RegistrationStartResponse } from "./models/registration-start.response"; -export abstract class OpaqueApiService { - abstract registrationStart(request: RegistrationStartRequest): Promise; - abstract registrationFinish( +export class OpaqueApiService { + constructor( + private apiService: ApiService, + private environmentService: EnvironmentService, + ) {} + + async registrationStart(request: RegistrationStartRequest): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const response = await this.apiService.send( + "POST", + `/opaque/start-registration`, + request, + true, + true, + env.getApiUrl(), + ); + return new RegistrationStartResponse(response); + } + + async registrationFinish( request: RegistrationFinishRequest, - ): Promise; - abstract loginStart(request: LoginStartRequest): Promise; - abstract loginFinish(request: LoginFinishRequest): Promise; + ): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const response = await this.apiService.send( + "POST", + `/opaque/finish-registration`, + request, + true, + true, + env.getApiUrl(), + ); + return new RegistrationFinishResponse(response); + } + + async loginStart(request: LoginStartRequest): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const response = await this.apiService.send( + "POST", + `/opaque/start-login`, + request, + true, + true, + env.getApiUrl(), + ); + return new LoginStartResponse(response); + } + + async loginFinish(request: LoginFinishRequest): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const response = await this.apiService.send( + "POST", + `/opaque/finish-login`, + request, + true, + true, + env.getApiUrl(), + ); + return response.success; + } } diff --git a/libs/common/src/auth/services/pre-password-login-api.service.ts b/libs/common/src/auth/services/pre-password-login-api.service.ts new file mode 100644 index 00000000000..5e2a1179f5c --- /dev/null +++ b/libs/common/src/auth/services/pre-password-login-api.service.ts @@ -0,0 +1,31 @@ +import { firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; + +import { PrePasswordLoginRequest } from "../models/request/pre-password-login.request"; +import { PrePasswordLoginResponse } from "../models/response/pre-password-login.response"; + +/** + * An API service which facilitates retrieving key derivation information + * required for password-based login before the user has authenticated. + */ +export class PrePasswordLoginApiService { + constructor( + private apiService: ApiService, + private environmentService: EnvironmentService, + ) {} + + async postPrePasswordLogin(request: PrePasswordLoginRequest): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const r = await this.apiService.send( + "POST", + "/accounts/prelogin", + request, + false, + true, + env.getIdentityUrl(), + ); + return new PrePasswordLoginResponse(r); + } +} diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 7a43daccf6e..61f82d05664 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -81,7 +81,6 @@ import { IdentityDeviceVerificationResponse } from "../auth/models/response/iden import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; -import { PreloginResponse } from "../auth/models/response/prelogin.response"; import { RegisterResponse } from "../auth/models/response/register.response"; import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response"; import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response"; @@ -111,7 +110,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; import { EventRequest } from "../models/request/event.request"; import { KdfRequest } from "../models/request/kdf.request"; import { KeysRequest } from "../models/request/keys.request"; -import { PreloginRequest } from "../models/request/prelogin.request"; import { RegisterRequest } from "../models/request/register.request"; import { StorageRequest } from "../models/request/storage.request"; import { UpdateAvatarRequest } from "../models/request/update-avatar.request"; @@ -353,19 +351,6 @@ export class ApiService implements ApiServiceAbstraction { return this.send("PUT", "/accounts/tax", request, true, false); } - async postPrelogin(request: PreloginRequest): Promise { - const env = await firstValueFrom(this.environmentService.environment$); - const r = await this.send( - "POST", - "/accounts/prelogin", - request, - false, - true, - env.getIdentityUrl(), - ); - return new PreloginResponse(r); - } - postEmailToken(request: EmailTokenRequest): Promise { return this.send("POST", "/accounts/email-token", request, true, false); } diff --git a/libs/key-management/src/index.ts b/libs/key-management/src/index.ts index f3efd75b098..d7a2d9c8fd5 100644 --- a/libs/key-management/src/index.ts +++ b/libs/key-management/src/index.ts @@ -14,6 +14,7 @@ export { PBKDF2KdfConfig, Argon2KdfConfig, KdfConfig, + createKdfConfig, DEFAULT_KDF_CONFIG, } from "./models/kdf-config"; export { KdfConfigService } from "./abstractions/kdf-config.service"; diff --git a/libs/key-management/src/models/kdf-config.spec.ts b/libs/key-management/src/models/kdf-config.spec.ts index 347f574fc00..4b0ddfcd180 100644 --- a/libs/key-management/src/models/kdf-config.spec.ts +++ b/libs/key-management/src/models/kdf-config.spec.ts @@ -34,19 +34,19 @@ describe("KdfConfig", () => { it("validateKdfConfigForPrelogin(): should validate the PBKDF2 KDF config", () => { const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + expect(() => kdfConfig.validateKdfConfigForPreLogin()).not.toThrow(); }); it("validateKdfConfigForPrelogin(): should validate the Argon2id KDF config", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + expect(() => kdfConfig.validateKdfConfigForPreLogin()).not.toThrow(); }); it("validateKdfConfigForPrelogin(): should throw an error for too low PBKDF2 iterations", () => { const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig( PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow( `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, ); }); @@ -57,7 +57,7 @@ describe("KdfConfig", () => { 64, 4, ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow( `Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, ); }); @@ -68,7 +68,7 @@ describe("KdfConfig", () => { Argon2KdfConfig.PRELOGIN_MEMORY_MIN - 1, 4, ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow( `Argon2 memory must be at least ${Argon2KdfConfig.PRELOGIN_MEMORY_MIN} MiB, but was ${kdfConfig.memory} MiB; possible pre-login downgrade attack detected.`, ); }); diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index 689da77c163..e52255c5e12 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -9,6 +9,20 @@ import { KdfType } from "../enums/kdf-type.enum"; */ export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; +/** + * A factory function that instantiates a new KdfConfig from an object that may represent one of several KdfTypes. + * This is useful for instantiating the correct KdfConfig from a server response object. + */ +export const createKdfConfig = (obj: { + kdf: KdfType; + kdfIterations: number; + kdfMemory?: number; + kdfParallelism?: number; +}): KdfConfig => + obj.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(obj.kdfIterations) + : new Argon2KdfConfig(obj.kdfIterations, obj.kdfMemory, obj.kdfParallelism); + /** * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. */ @@ -38,7 +52,7 @@ export class PBKDF2KdfConfig { * Validates the PBKDF2 KDF configuration for pre-login. * A Valid PBKDF2 KDF configuration has KDF iterations between the 5000 and 2_000_000. */ - validateKdfConfigForPrelogin(): void { + validateKdfConfigForPreLogin(): void { if (PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN > this.iterations) { throw new Error( `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${this.iterations}; possible pre-login downgrade attack detected.`, @@ -101,7 +115,7 @@ export class Argon2KdfConfig { /** * Validates the Argon2 KDF configuration for pre-login. */ - validateKdfConfigForPrelogin(): void { + validateKdfConfigForPreLogin(): void { if (Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN > this.iterations) { throw new Error( `Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${this.iterations}; possible pre-login downgrade attack detected.`,