1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 20:34:04 +00:00

Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v2

This commit is contained in:
Patrick Pimentel
2025-05-08 20:57:26 -04:00
779 changed files with 31936 additions and 10731 deletions

View File

@@ -284,7 +284,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
}
protected async approveFromOtherDevice() {
this.loginEmailService.setLoginEmail(this.email);
await this.router.navigate(["/login-with-device"]);
}
@@ -297,7 +296,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
}
protected async requestAdminApproval() {
this.loginEmailService.setLoginEmail(this.email);
await this.router.navigate(["/admin-approval-requested"]);
}

View File

@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { Observable, filter, firstValueFrom, map, merge, race, take, timer } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@@ -19,7 +19,6 @@ import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
@@ -178,7 +177,26 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private async initStandardAuthRequestFlow(): Promise<void> {
this.flow = Flow.StandardAuthRequest;
this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) || undefined;
// For a standard flow, we can get the user's email from two different places:
// 1. The loginEmailService, which is the email that the user is trying to log in with. This is cleared
// when the user logs in successfully. We can use this when the user is using Login with Device.
// 2. With TDE Login with Another Device, the user is already logged in and we just need to get
// a decryption key, so we can use the active account's email.
const activeAccountEmail$: Observable<string | undefined> =
this.accountService.activeAccount$.pipe(map((a) => a?.email));
const loginEmail$: Observable<string | null> = this.loginEmailService.loginEmail$;
// Use merge as we want to get the first value from either observable.
const firstEmail$ = merge(loginEmail$, activeAccountEmail$).pipe(
filter((e): e is string => !!e), // convert null/undefined to false and filter out so we narrow type to string
take(1), // complete after first value
);
const emailRetrievalTimeout$ = timer(2500).pipe(map(() => undefined as undefined));
// Wait for either the first email or the timeout to occur so we can proceed
// neither above observable will complete, so we have to add a timeout
this.email = await firstValueFrom(race(firstEmail$, emailRetrievalTimeout$));
if (!this.email) {
await this.handleMissingEmail();
@@ -801,8 +819,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private async handlePostLoginNavigation(loginResponse: AuthResult) {
if (loginResponse.requiresTwoFactor) {
await this.router.navigate(["2fa"]);
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
await this.router.navigate(["update-temp-password"]);
} else {
await this.handleSuccessfulLoginNavigation(loginResponse.userId);
}

View File

@@ -17,7 +17,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
@@ -307,10 +306,7 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loginSuccessHandlerService.run(authResult.userId);
// Determine where to send the user next
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
await this.router.navigate(["update-temp-password"]);
return;
}
// The AuthGuard will handle routing to update-temp-password based on state
// TODO: PM-18269 - evaluate if we can combine this with the
// password evaluation done in the password login strategy.
@@ -539,7 +535,7 @@ export class LoginComponent implements OnInit, OnDestroy {
// If we load an email into the form, we need to initialize it for the login process as well
// so that other login components can use it.
// We do this here as it's possible that a user doesn't edit the email field before submitting.
this.loginEmailService.setLoginEmail(storedEmail);
await this.loginEmailService.setLoginEmail(storedEmail);
} else {
this.formGroup.controls.rememberEmail.setValue(false);
}

View File

@@ -136,11 +136,6 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
return;
}
if (authResult.forcePasswordReset) {
await this.router.navigate(["/update-temp-password"]);
return;
}
this.loginSuccessHandlerService.run(authResult.userId);
// If verification succeeds, navigate to vault

View File

@@ -79,7 +79,7 @@ export class PasswordHintComponent implements OnInit {
};
protected async cancel() {
this.loginEmailService.setLoginEmail(this.email);
await this.loginEmailService.setLoginEmail(this.email);
await this.router.navigate(["login"]);
}

View File

@@ -541,14 +541,6 @@ export class SsoComponent implements OnInit {
});
}
private async handleForcePasswordReset(orgIdentifier: string) {
await this.router.navigate(["update-temp-password"], {
queryParams: {
identifier: orgIdentifier,
},
});
}
private async handleSuccessfulLogin() {
await this.router.navigate(["lock"]);
}

View File

@@ -575,25 +575,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
});
}
/**
* Determines if a user needs to reset their password based on certain conditions.
* Users can be forced to reset their password via an admin or org policy disallowing weak passwords.
* Note: this is different from the SSO component login flow as a user can
* login with MP and then have to pass 2FA to finish login and we can actually
* evaluate if they have a weak password at that time.
*
* @param {AuthResult} authResult - The authentication result.
* @returns {boolean} Returns true if a password reset is required, false otherwise.
*/
private isForcePasswordResetRequired(authResult: AuthResult): boolean {
const forceResetReasons = [
ForceSetPasswordReason.AdminForcePasswordReset,
ForceSetPasswordReason.WeakMasterPassword,
];
return forceResetReasons.includes(authResult.forcePasswordReset);
}
showContinueButton() {
return (
this.selectedProviderType != null &&

View File

@@ -296,13 +296,9 @@ describe("LoginStrategy", () => {
const expected = new AuthResult();
expected.userId = userId;
expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
expected.resetMasterPassword = true;
expected.twoFactorProviders = {} as Partial<
Record<TwoFactorProviderType, Record<string, string>>
>;
expected.captchaSiteKey = "";
expected.twoFactorProviders = null;
expected.captchaSiteKey = "";
expect(result).toEqual(expected);
});
@@ -316,13 +312,9 @@ describe("LoginStrategy", () => {
const expected = new AuthResult();
expected.userId = userId;
expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
expected.resetMasterPassword = false;
expected.twoFactorProviders = {} as Partial<
Record<TwoFactorProviderType, Record<string, string>>
>;
expected.captchaSiteKey = "";
expected.twoFactorProviders = null;
expected.captchaSiteKey = "";
expect(result).toEqual(expected);
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(

View File

@@ -277,17 +277,7 @@ export abstract class LoginStrategy {
result.resetMasterPassword = response.resetMasterPassword;
// Convert boolean to enum and set the state for the master password service to
// so we know when we reach the auth guard that we need to guide them properly to admin
// password reset.
if (response.forcePasswordReset) {
result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.AdminForcePasswordReset,
userId,
);
}
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
if (response.twoFactorToken != null) {
// note: we can read email from access token b/c it was saved in saveAccountInformation
@@ -318,6 +308,30 @@ export abstract class LoginStrategy {
return false;
}
/**
* Checks if adminForcePasswordReset is true and sets the ForceSetPasswordReason.AdminForcePasswordReset flag in the master password service.
* @param adminForcePasswordReset - The admin force password reset flag
* @param userId - The user ID
* @returns a promise that resolves to a boolean indicating whether the admin force password reset flag was set
*/
async processForceSetPasswordReason(
adminForcePasswordReset: boolean,
userId: UserId,
): Promise<boolean> {
if (!adminForcePasswordReset) {
return false;
}
// set the flag in the master password service so we know when we reach the auth guard
// that we need to guide them properly to admin password reset.
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.AdminForcePasswordReset,
userId,
);
return true;
}
protected async createKeyPairForOldAccount(userId: UserId) {
try {
const userKey = await this.keyService.getUserKeyWithLegacySupport(userId);

View File

@@ -211,20 +211,18 @@ describe("PasswordLoginStrategy", () => {
it("does not force the user to update their master password when there are no requirements", async () => {
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
const result = await passwordLoginStrategy.logIn(credentials);
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
});
it("does not force the user to update their master password when it meets requirements", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 5 } as any);
policyService.evaluateMasterPassword.mockReturnValue(true);
const result = await passwordLoginStrategy.logIn(credentials);
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
});
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
@@ -232,14 +230,13 @@ describe("PasswordLoginStrategy", () => {
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
const result = await passwordLoginStrategy.logIn(credentials);
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
});
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
@@ -257,13 +254,13 @@ describe("PasswordLoginStrategy", () => {
// First login request fails requiring 2FA
apiService.postIdentityToken.mockResolvedValueOnce(token2FAResponse);
const firstResult = await passwordLoginStrategy.logIn(credentials);
await passwordLoginStrategy.logIn(credentials);
// Second login request succeeds
apiService.postIdentityToken.mockResolvedValueOnce(
identityTokenResponseFactory(masterPasswordPolicy),
);
const secondResult = await passwordLoginStrategy.logInTwoFactor(
await passwordLoginStrategy.logInTwoFactor(
{
provider: TwoFactorProviderType.Authenticator,
token: "123456",
@@ -272,15 +269,11 @@ describe("PasswordLoginStrategy", () => {
"",
);
// First login attempt should not save the force password reset options
expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
// Second login attempt should save the force password reset options and return in result
// Second login attempt should save the force password reset options
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
});
it("handles new device verification login with OTP", async () => {
@@ -298,7 +291,6 @@ describe("PasswordLoginStrategy", () => {
newDeviceOtp: deviceVerificationOtp,
}),
);
expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None);
expect(result.resetMasterPassword).toBe(false);
expect(result.userId).toBe(userId);
});

View File

@@ -109,35 +109,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
return authResult;
}
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
await this.evaluateMasterPasswordIfRequired(identityResponse, credentials, authResult);
// 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;
}
@@ -151,20 +124,6 @@ export class PasswordLoginStrategy extends LoginStrategy {
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;
}
@@ -208,13 +167,58 @@ export class PasswordLoginStrategy extends LoginStrategy {
return !response.key;
}
private getMasterPasswordPolicyOptionsFromResponse(
response:
private async evaluateMasterPasswordIfRequired(
identityResponse:
| IdentityTokenResponse
| IdentityTwoFactorResponse
| IdentityDeviceVerificationResponse,
): MasterPasswordPolicyOptions {
if (response == null || response instanceof IdentityDeviceVerificationResponse) {
credentials: PasswordLoginCredentials,
authResult: AuthResult,
): Promise<void> {
// TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse
// If the response is a device verification response, we don't need to evaluate the password
if (identityResponse instanceof IdentityDeviceVerificationResponse) {
return;
}
// The identity result can contain master password policies for the user's organizations
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
return;
}
// 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;
}
if (identityResponse instanceof IdentityTwoFactorResponse) {
// Save the flag to this strategy for use in 2fa as the master password is about to pass out of scope
this.cache.next({
...this.cache.value,
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
});
}
// Authentication was successful, save the force update password options with the state service
// if there isn't already a reason set (this would only be AdminForcePasswordReset as that can be set server side
// and would have already been processed in the base login strategy processForceSetPasswordReason method)
// Note: masterPasswordService.setForceSetPasswordReason will not allow overwriting
// AdminForcePasswordReset with any other reason except for None. This is because
// an AdminForcePasswordReset will always force a user to update their password to a password that meets the policy.
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
authResult.userId, // userId is only available on successful login
);
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse,
): MasterPasswordPolicyOptions | null {
if (response == null) {
return null;
}
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
@@ -246,4 +250,35 @@ export class PasswordLoginStrategy extends LoginStrategy {
const [authResult] = await this.startLogIn();
return authResult;
}
/**
* Override to handle the WeakMasterPassword reason if no other reason is set.
* @param authResult - The authentication result
* @param userId - The user ID
*/
override async processForceSetPasswordReason(
adminForcePasswordReset: boolean,
userId: UserId,
): Promise<boolean> {
// handle any existing reasons
const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason(
adminForcePasswordReset,
userId,
);
// If we are already processing an admin force password reset, don't process other reasons
if (adminForcePasswordResetFlagSet) {
return false;
}
// If we have a cached weak password reason from login/logInTwoFactor apply it
const cachedReason = this.cache.value.forcePasswordResetReason;
if (cachedReason !== ForceSetPasswordReason.None) {
await this.masterPasswordService.setForceSetPasswordReason(cachedReason, userId);
return true;
}
// If none of the conditions are met, return false
return false;
}
}

View File

@@ -1,5 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@@ -37,10 +37,11 @@ import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../abstractions";
import { UserDecryptionOptions } from "../models";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { SsoLoginStrategy } from "./sso-login.strategy";
import { SsoLoginStrategy, SsoLoginStrategyData } from "./sso-login.strategy";
describe("SsoLoginStrategy", () => {
let accountService: FakeAccountService;
@@ -123,8 +124,11 @@ describe("SsoLoginStrategy", () => {
mockVaultTimeoutBSub.asObservable(),
);
const userDecryptionOptions = new UserDecryptionOptions();
userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions);
ssoLoginStrategy = new SsoLoginStrategy(
null,
{} as SsoLoginStrategyData,
keyConnectorService,
deviceTrustService,
authRequestService,

View File

@@ -4,6 +4,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
@@ -355,4 +356,75 @@ export class SsoLoginStrategy extends LoginStrategy {
sso: this.cache.value,
};
}
/**
* Override to handle SSO-specific ForceSetPasswordReason flags,including TdeOffboarding,
* TdeUserWithoutPasswordHasPasswordResetPermission, and SsoNewJitProvisionedUser cases.
* @param authResult - The authentication result
* @param userId - The user ID
*/
override async processForceSetPasswordReason(
adminForcePasswordReset: boolean,
userId: UserId,
): Promise<boolean> {
// handle any existing reasons
const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason(
adminForcePasswordReset,
userId,
);
// If we are already processing an admin force password reset, don't process other reasons
if (adminForcePasswordResetFlagSet) {
return false;
}
// Check for TDE-related conditions
const userDecryptionOptions = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
if (!userDecryptionOptions) {
return false;
}
// Check for TDE offboarding - user is being offboarded from TDE and needs to set a password
if (userDecryptionOptions.trustedDeviceOption?.isTdeOffboarding) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboarding,
userId,
);
return true;
}
// Check if user has permission to set password but hasn't yet
if (
!userDecryptionOptions.hasMasterPassword &&
userDecryptionOptions.trustedDeviceOption?.hasManageResetPasswordPermission
) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
);
return true;
}
// Check for new SSO JIT provisioned user
// If a user logs in via SSO but has no master password and no alternative encryption methods
// Then they must be a newly provisioned user who needs to set up their encryption
if (
!userDecryptionOptions.hasMasterPassword &&
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
!userDecryptionOptions.trustedDeviceOption
) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.SsoNewJitProvisionedUser,
userId,
);
return true;
}
// If none of the conditions are met, return false
return false;
}
}

View File

@@ -209,7 +209,6 @@ describe("WebAuthnLoginStrategy", () => {
expect(authResult).toBeInstanceOf(AuthResult);
expect(authResult).toMatchObject({
captchaSiteKey: "",
forcePasswordReset: 0,
resetMasterPassword: false,
twoFactorProviders: null,
requiresTwoFactor: false,
@@ -230,7 +229,7 @@ describe("WebAuthnLoginStrategy", () => {
const mockUserKeyArray: Uint8Array = randomBytes(32);
const mockUserKey = new SymmetricCryptoKey(mockUserKeyArray) as UserKey;
encryptService.decryptToBytes.mockResolvedValue(mockPrfPrivateKey);
encryptService.unwrapDecapsulationKey.mockResolvedValue(mockPrfPrivateKey);
encryptService.decapsulateKeyUnsigned.mockResolvedValue(
new SymmetricCryptoKey(mockUserKeyArray),
);
@@ -246,8 +245,8 @@ describe("WebAuthnLoginStrategy", () => {
userId,
);
expect(encryptService.decryptToBytes).toHaveBeenCalledTimes(1);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(
expect(encryptService.unwrapDecapsulationKey).toHaveBeenCalledTimes(1);
expect(encryptService.unwrapDecapsulationKey).toHaveBeenCalledWith(
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedPrivateKey,
webAuthnCredentials.prfKey,
);
@@ -279,7 +278,7 @@ describe("WebAuthnLoginStrategy", () => {
await webAuthnLoginStrategy.logIn(webAuthnCredentials);
// Assert
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
expect(encryptService.unwrapDecapsulationKey).not.toHaveBeenCalled();
expect(encryptService.decapsulateKeyUnsigned).not.toHaveBeenCalled();
expect(keyService.setUserKey).not.toHaveBeenCalled();
});
@@ -314,7 +313,7 @@ describe("WebAuthnLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
encryptService.decryptToBytes.mockResolvedValue(null);
encryptService.unwrapDecapsulationKey.mockResolvedValue(null);
// Act
await webAuthnLoginStrategy.logIn(webAuthnCredentials);

View File

@@ -82,7 +82,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
}
// decrypt prf encrypted private key
const privateKey = await this.encryptService.decryptToBytes(
const privateKey = await this.encryptService.unwrapDecapsulationKey(
webAuthnPrfOption.encryptedPrivateKey,
credentials.prfKey,
);

View File

@@ -8,7 +8,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncString, EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
PIN_DISK,
PIN_MEMORY,
@@ -172,7 +171,7 @@ export class PinService implements PinServiceAbstraction {
const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
);
const kdfConfig = await this.kdfConfigService.getKdfConfig();
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
const pinKey = await this.makePinKey(pin, email, kdfConfig);
return await this.encryptService.wrapSymmetricKey(userKey, pinKey);
@@ -221,7 +220,7 @@ export class PinService implements PinServiceAbstraction {
throw new Error("No UserKey provided. Cannot create userKeyEncryptedPin.");
}
return await this.encryptService.encrypt(pin, userKey);
return await this.encryptService.encryptString(pin, userKey);
}
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
@@ -293,7 +292,7 @@ export class PinService implements PinServiceAbstraction {
const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
);
const kdfConfig = await this.kdfConfigService.getKdfConfig();
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
const userKey: UserKey = await this.decryptUserKey(
userId,
@@ -339,9 +338,9 @@ export class PinService implements PinServiceAbstraction {
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.decryptToBytes(pinKeyEncryptedUserKey, pinKey);
const userKey = await this.encryptService.unwrapSymmetricKey(pinKeyEncryptedUserKey, pinKey);
return new SymmetricCryptoKey(userKey) as UserKey;
return userKey as UserKey;
}
/**
@@ -377,7 +376,7 @@ export class PinService implements PinServiceAbstraction {
this.validateUserId(userId, "Cannot validate PIN.");
const userKeyEncryptedPin = await this.getUserKeyEncryptedPin(userId);
const decryptedPin = await this.encryptService.decryptToUtf8(userKeyEncryptedPin, userKey);
const decryptedPin = await this.encryptService.decryptString(userKeyEncryptedPin, userKey);
const isPinValid = this.cryptoFunctionService.compareFast(decryptedPin, pin);
return isPinValid;

View File

@@ -259,11 +259,11 @@ describe("PinService", () => {
});
it("should create a userKeyEncryptedPin from the provided PIN and userKey", async () => {
encryptService.encrypt.mockResolvedValue(mockUserKeyEncryptedPin);
encryptService.encryptString.mockResolvedValue(mockUserKeyEncryptedPin);
const result = await sut.createUserKeyEncryptedPin(mockPin, mockUserKey);
expect(encryptService.encrypt).toHaveBeenCalledWith(mockPin, mockUserKey);
expect(encryptService.encryptString).toHaveBeenCalledWith(mockPin, mockUserKey);
expect(result).toEqual(mockUserKeyEncryptedPin);
});
});
@@ -425,7 +425,7 @@ describe("PinService", () => {
mockDecryptUserKeyFn();
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
encryptService.decryptToUtf8.mockResolvedValue(mockPin);
encryptService.decryptString.mockResolvedValue(mockPin);
cryptoFunctionService.compareFast.calledWith(mockPin, "1234").mockResolvedValue(true);
}
@@ -434,7 +434,7 @@ describe("PinService", () => {
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
encryptService.decryptToBytes.mockResolvedValue(mockUserKey.toEncoded());
encryptService.unwrapSymmetricKey.mockResolvedValue(mockUserKey);
}
function mockPinEncryptedKeyDataByPinLockType(pinLockType: PinLockType) {
@@ -490,7 +490,7 @@ describe("PinService", () => {
it(`should return null when PIN doesn't match after successful user key decryption`, async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks(pinLockType);
encryptService.decryptToUtf8.mockResolvedValue("9999"); // non matching PIN
encryptService.decryptString.mockResolvedValue("9999"); // non matching PIN
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);