1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

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 <trittson@bitwarden.com>
This commit is contained in:
Jared Snider
2025-03-17 06:41:46 -04:00
committed by GitHub
parent b2d949dd1c
commit a2ba965abd
35 changed files with 695 additions and 254 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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({

View File

@@ -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({

View File

@@ -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<AuthResult>;
/**
* 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<MasterKey>;
makePrePasswordLoginMasterKey: (
masterPassword: string,
email: string,
kdfConfig?: KdfConfig,
) => Promise<MasterKey>;
/**
* Emits true if the authentication session has expired.
*/

View File

@@ -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;

View File

@@ -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<string>;
accessCode$: Observable<string>;
authRequestId$: Observable<string>;
@@ -39,7 +39,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
constructor(
data: AuthRequestLoginStrategyData,
private deviceTrustService: DeviceTrustServiceAbstraction,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
super(...sharedDeps);

View File

@@ -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<LoginStrategyData>;
protected sessionTimeoutSubject = new BehaviorSubject<boolean>(false);
sessionTimeout$: Observable<boolean> = this.sessionTimeoutSubject.asObservable();
@@ -102,10 +105,11 @@ export abstract class LoginStrategy {
abstract logIn(
credentials:
| UserApiLoginCredentials
| PasswordLoginCredentials
| PasswordHashLoginCredentials
| SsoLoginCredentials
| AuthRequestLoginCredentials
| WebAuthnLoginCredentials,
| WebAuthnLoginCredentials
| OpaqueLoginCredentials,
): Promise<AuthResult>;
async logInTwoFactor(

View File

@@ -0,0 +1 @@
// TODO: add tests for OpaqueLoginStrategy once it is implemented

View File

@@ -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>): 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<string>;
/** The local master key hash we store client side */
localMasterKeyHash$: Observable<string | null>;
protected cache: BehaviorSubject<OpaqueLoginStrategyData>;
constructor(
data: OpaqueLoginStrategyData,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
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<AuthResult> {
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<void> {
// 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<void> {
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<AuthResult> {
const data = this.cache.value;
data.tokenRequest.newDeviceOtp = deviceVerificationOtp;
this.cache.next(data);
const [authResult] = await this.startLogIn();
return authResult;
}
}

View File

@@ -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)

View File

@@ -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<string>;
/** 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<typeof LoginStrategy>
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
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(

View File

@@ -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", () => {

View File

@@ -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<typeof LoginStrategy>
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
super(...sharedDeps);

View File

@@ -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", () => {

View File

@@ -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<UserApiLoginStrategyData>;
constructor(
data: UserApiLoginStrategyData,
private keyConnectorService: KeyConnectorService,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
super(...sharedDeps);

View File

@@ -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", () => {

View File

@@ -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<WebAuthnLoginStrategyData>;
constructor(
data: WebAuthnLoginStrategyData,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
super(...sharedDeps);

View File

@@ -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,
) {}
}

View File

@@ -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<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let prePasswordLoginApiService: MockProxy<PrePasswordLoginApiService>;
let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
@@ -107,6 +111,7 @@ describe("LoginStrategyService", () => {
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
taskSchedulerService = mock<TaskSchedulerService>();
prePasswordLoginApiService = mock<PrePasswordLoginApiService>();
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,

View File

@@ -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<AuthenticationType | null>;
@@ -94,14 +101,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
authenticationSessionTimeout$: Observable<boolean> =
this.authenticationTimeoutSubject.asObservable();
private loginStrategy$: Observable<
| UserApiLoginStrategy
| PasswordLoginStrategy
| SsoLoginStrategy
| AuthRequestLoginStrategy
| WebAuthnLoginStrategy
| null
>;
private loginStrategy$: Observable<LoginStrategy | null>;
currentAuthType$: Observable<AuthenticationType | null>;
@@ -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<MasterKey> {
async makePrePasswordLoginMasterKey(
masterPassword: string,
email: string,
kdfConfig?: KdfConfig,
): Promise<MasterKey> {
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<void> {
@@ -383,9 +395,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
}
private initializeLoginStrategy(
source: Observable<[AuthenticationType | null, CacheData | null]>,
) {
const sharedDeps: ConstructorParameters<typeof LoginStrategy> = [
strategy: AuthenticationType | null,
data: CacheData | null,
): LoginStrategy | null {
const sharedDeps: ConstructorParameters<typeof BaseLoginStrategy> = [
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;
}
}
}

View File

@@ -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<string | nul
export type CacheData = {
password?: PasswordLoginStrategyData;
opaque?: OpaqueLoginStrategyData;
sso?: SsoLoginStrategyData;
userApiKey?: UserApiLoginStrategyData;
authRequest?: AuthRequestLoginStrategyData;
@@ -68,6 +70,7 @@ export const CACHE_KEY = new KeyDefinition<CacheData | null>(
}
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)

View File

@@ -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<ProfileResponse>;
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
postEmail: (request: EmailRequest) => Promise<any>;
postPassword: (request: PasswordRequest) => Promise<any>;

View File

@@ -4,4 +4,6 @@ export enum AuthenticationType {
UserApiKey = 2,
AuthRequest = 3,
WebAuthn = 4,
PasswordHash = 5,
Opaque = 6,
}

View File

@@ -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,
});
}
}

View File

@@ -1,4 +1,4 @@
export class PreloginRequest {
export class PrePasswordLoginRequest {
email: string;
constructor(email: string) {

View File

@@ -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);
}
}

View File

@@ -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<RegistrationStartResponse> {
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<RegistrationFinishResponse> {
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<LoginStartResponse> {
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<boolean> {
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;
}
}

View File

@@ -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<RegistrationStartResponse>;
abstract registrationFinish(
export class OpaqueApiService {
constructor(
private apiService: ApiService,
private environmentService: EnvironmentService,
) {}
async registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse> {
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<RegistrationFinishResponse>;
abstract loginStart(request: LoginStartRequest): Promise<LoginStartResponse>;
abstract loginFinish(request: LoginFinishRequest): Promise<boolean>;
): Promise<RegistrationFinishResponse> {
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<LoginStartResponse> {
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<boolean> {
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;
}
}

View File

@@ -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<PrePasswordLoginResponse> {
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);
}
}

View File

@@ -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<PreloginResponse> {
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<any> {
return this.send("POST", "/accounts/email-token", request, true, false);
}

View File

@@ -14,6 +14,7 @@ export {
PBKDF2KdfConfig,
Argon2KdfConfig,
KdfConfig,
createKdfConfig,
DEFAULT_KDF_CONFIG,
} from "./models/kdf-config";
export { KdfConfigService } from "./abstractions/kdf-config.service";

View File

@@ -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.`,
);
});

View File

@@ -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.`,