diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index 71c3830b4cc..2ce0d425007 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -172,9 +172,7 @@ export abstract class BaseProgram { } else { const command = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -184,7 +182,6 @@ export abstract class BaseProgram { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/cli/src/key-management/commands/unlock.command.spec.ts b/apps/cli/src/key-management/commands/unlock.command.spec.ts index 50ef414ec37..a722469f7bb 100644 --- a/apps/cli/src/key-management/commands/unlock.command.spec.ts +++ b/apps/cli/src/key-management/commands/unlock.command.spec.ts @@ -3,21 +3,16 @@ import { of } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { MasterPasswordVerificationResponse } from "@bitwarden/common/auth/types/verification"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { ConsoleLogService } from "@bitwarden/logging"; import { UserId } from "@bitwarden/user-core"; @@ -32,9 +27,7 @@ describe("UnlockCommand", () => { let command: UnlockCommand; const accountService = mock(); - const masterPasswordService = mock(); const keyService = mock(); - const userVerificationService = mock(); const cryptoFunctionService = mock(); const logService = mock(); const keyConnectorService = mock(); @@ -44,7 +37,6 @@ describe("UnlockCommand", () => { const i18nService = mock(); const encryptedMigrator = mock(); const masterPasswordUnlockService = mock(); - const configService = mock(); const mockMasterPassword = "testExample"; const activeAccount: Account = { @@ -73,9 +65,6 @@ describe("UnlockCommand", () => { ); expectedSuccessMessage.raw = b64sessionKey; - // Legacy test data - const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey; - beforeEach(async () => { jest.clearAllMocks(); @@ -86,9 +75,7 @@ describe("UnlockCommand", () => { command = new UnlockCommand( accountService, - masterPasswordService, keyService, - userVerificationService, cryptoFunctionService, logService, keyConnectorService, @@ -98,7 +85,6 @@ describe("UnlockCommand", () => { i18nService, encryptedMigrator, masterPasswordUnlockService, - configService, ); }); @@ -133,116 +119,46 @@ describe("UnlockCommand", () => { }, ); - describe("UnlockWithMasterPasswordUnlockData feature flag enabled", () => { - beforeEach(() => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - }); + it("calls masterPasswordUnlockService successfully", async () => { + masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); - it("calls masterPasswordUnlockService successfully", async () => { - masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - }); - - it("returns error response if unlockWithMasterPassword fails", async () => { - masterPasswordUnlockService.unlockWithMasterPassword.mockRejectedValue( - new Error("Unlock failed"), - ); - - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(false); - expect(response.message).toEqual("Unlock failed"); - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - expect(keyService.setUserKey).not.toHaveBeenCalled(); - }); + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(response.data).toEqual(expectedSuccessMessage); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); }); - describe("unlock with feature flag off", () => { - beforeEach(() => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - }); + it("returns error response if unlockWithMasterPassword fails", async () => { + masterPasswordUnlockService.unlockWithMasterPassword.mockRejectedValue( + new Error("Unlock failed"), + ); - it("calls decryptUserKeyWithMasterKey successfully", async () => { - userVerificationService.verifyUserByMasterPassword.mockResolvedValue({ - masterKey: mockMasterKey, - } as MasterPasswordVerificationResponse); - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - }); - - it("returns error response when verifyUserByMasterPassword throws", async () => { - userVerificationService.verifyUserByMasterPassword.mockRejectedValue( - new Error("Verification failed"), - ); - - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(false); - expect(response.message).toEqual("Verification failed"); - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).not.toHaveBeenCalled(); - expect(keyService.setUserKey).not.toHaveBeenCalled(); - }); + expect(response).not.toBeNull(); + expect(response.success).toEqual(false); + expect(response.message).toEqual("Unlock failed"); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); describe("calls convertToKeyConnectorCommand if required", () => { let convertToKeyConnectorSpy: jest.SpyInstance; beforeEach(() => { keyConnectorService.convertAccountRequired$ = of(true); - - // Feature flag on masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); - - // Feature flag off - userVerificationService.verifyUserByMasterPassword.mockResolvedValue({ - masterKey: mockMasterKey, - } as MasterPasswordVerificationResponse); - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey); }); - test.each([true, false])("returns failure when feature flag is %s", async (flagValue) => { - configService.getFeatureFlag$.mockReturnValue(of(flagValue)); - + it("returns error on failure", async () => { // Mock the ConvertToKeyConnectorCommand const mockRun = jest.fn().mockResolvedValue({ success: false, message: "convert failed" }); convertToKeyConnectorSpy = jest @@ -257,67 +173,32 @@ describe("UnlockCommand", () => { expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - if (flagValue === true) { - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - } else { - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - } + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); }); - test.each([true, false])( - "returns expected success when feature flag is %s", - async (flagValue) => { - configService.getFeatureFlag$.mockReturnValue(of(flagValue)); + it("returns success on successful conversion", async () => { + // Mock the ConvertToKeyConnectorCommand + const mockRun = jest.fn().mockResolvedValue({ success: true }); + const convertToKeyConnectorSpy = jest + .spyOn(ConvertToKeyConnectorCommand.prototype, "run") + .mockImplementation(mockRun); - // Mock the ConvertToKeyConnectorCommand - const mockRun = jest.fn().mockResolvedValue({ success: true }); - const convertToKeyConnectorSpy = jest - .spyOn(ConvertToKeyConnectorCommand.prototype, "run") - .mockImplementation(mockRun); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(response.data).toEqual(expectedSuccessMessage); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); + expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - - if (flagValue === true) { - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - } else { - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - } - }, - ); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + }); }); }); }); diff --git a/apps/cli/src/key-management/commands/unlock.command.ts b/apps/cli/src/key-management/commands/unlock.command.ts index c88d9ae1cc4..5f82b721d07 100644 --- a/apps/cli/src/key-management/commands/unlock.command.ts +++ b/apps/cli/src/key-management/commands/unlock.command.ts @@ -4,20 +4,13 @@ import { firstValueFrom } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { MasterKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { Response } from "../../models/response"; @@ -29,9 +22,7 @@ import { ConvertToKeyConnectorCommand } from "../convert-to-key-connector.comman export class UnlockCommand { constructor( private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private keyService: KeyService, - private userVerificationService: UserVerificationService, private cryptoFunctionService: CryptoFunctionService, private logService: ConsoleLogService, private keyConnectorService: KeyConnectorService, @@ -41,7 +32,6 @@ export class UnlockCommand { private i18nService: I18nService, private encryptedMigrator: EncryptedMigrator, private masterPasswordUnlockService: MasterPasswordUnlockService, - private configService: ConfigService, ) {} async run(password: string, cmdOptions: Record) { @@ -61,46 +51,15 @@ export class UnlockCommand { } const userId = activeAccount.id; - if ( - await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.UnlockWithMasterPasswordUnlockData), - ) - ) { - try { - const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword( - password, - userId, - ); - - await this.keyService.setUserKey(userKey, userId); - } catch (e) { - return Response.error(e.message); - } - } else { - const email = activeAccount.email; - const verification = { - type: VerificationType.MasterPassword, - secret: password, - } as MasterPasswordVerification; - - let masterKey: MasterKey; - try { - const response = await this.userVerificationService.verifyUserByMasterPassword( - verification, - userId, - email, - ); - masterKey = response.masterKey; - } catch (e) { - // verification failure throws - return Response.error(e.message); - } - - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - masterKey, + try { + const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword( + password, userId, ); + await this.keyService.setUserKey(userKey, userId); + } catch (e) { + return Response.error(e.message); } if (await firstValueFrom(this.keyConnectorService.convertAccountRequired$)) { diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index b2cca2a644b..e0385534cb7 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -166,9 +166,7 @@ export class OssServeConfigurator { ); this.unlockCommand = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -178,7 +176,6 @@ export class OssServeConfigurator { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 870d743095d..7856fc3588c 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -303,9 +303,7 @@ export class Program extends BaseProgram { await this.exitIfNotAuthed(); const command = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -315,7 +313,6 @@ export class Program extends BaseProgram { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); const response = await command.run(password, cmd); this.processResponse(response); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5dfee6054b2..20da219e8d7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -38,7 +38,6 @@ export enum FeatureFlag { ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings", PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption", LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2", - UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data", NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change", DataRecoveryTool = "pm-28813-data-recovery-tool", ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component", @@ -147,7 +146,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ForceUpdateKDFSettings]: FALSE, [FeatureFlag.PM25174_DisableType0Decryption]: FALSE, [FeatureFlag.LinuxBiometricsV2]: FALSE, - [FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE, [FeatureFlag.NoLogoutOnKdfChange]: FALSE, [FeatureFlag.DataRecoveryTool]: FALSE, [FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE, diff --git a/libs/key-management-ui/src/lock/components/lock.component.html b/libs/key-management-ui/src/lock/components/lock.component.html index 71201361a0c..c1577b76a4d 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.html +++ b/libs/key-management-ui/src/lock/components/lock.component.html @@ -121,11 +121,7 @@ - @if ( - (unlockWithMasterPasswordUnlockDataFlag$ | async) && - unlockOptions.masterPassword.enabled && - activeUnlockOption === UnlockOption.MasterPassword - ) { + @if (unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword) { - } @else { - -
- - {{ "masterPass" | i18n }} - - - - - - -
- - -

{{ "or" | i18n }}

- - - - - - - - - - -
-
-
} diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 5d35746ff19..054212f8851 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -1,10 +1,8 @@ -import { DebugElement } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { firstValueFrom, interval, map, of, takeWhile, timeout } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import { ZXCVBNResult } from "zxcvbn"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -13,20 +11,13 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; -import { - MasterPasswordVerification, - MasterPasswordVerificationResponse, -} from "@bitwarden/common/auth/types/verification"; import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -36,7 +27,7 @@ import { SyncService } from "@bitwarden/common/platform/sync"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { AnonLayoutWrapperDataService, AsyncActionsModule, @@ -94,7 +85,6 @@ describe("LockComponent", () => { const mockAnonLayoutWrapperDataService = mock(); const mockBroadcasterService = mock(); const mockEncryptedMigrator = mock(); - const mockConfigService = mock(); const mockActivatedRoute = { snapshot: { paramMap: { @@ -161,7 +151,6 @@ describe("LockComponent", () => { { provide: BroadcasterService, useValue: mockBroadcasterService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: EncryptedMigrator, useValue: mockEncryptedMigrator }, - { provide: ConfigService, useValue: mockConfigService }, ], }) .overrideProvider(DialogService, { useValue: mockDialogService }) @@ -171,207 +160,6 @@ describe("LockComponent", () => { component = fixture.componentInstance; }); - describe("when master password unlock is active", () => { - let form: DebugElement; - - beforeEach(async () => { - const unlockOptions: UnlockOptions = { - masterPassword: { enabled: true }, - pin: { enabled: false }, - biometrics: { - enabled: false, - biometricsStatus: BiometricsStatus.NotEnabledLocally, - }, - }; - - component.activeUnlockOption = UnlockOption.MasterPassword; - mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(unlockOptions)); - await mockAccountService.switchAccount(userId); - mockPlatformUtilsService.getClientType.mockReturnValue(ClientType.Web); - - mockI18nService.t.mockImplementation((key: string) => { - switch (key) { - case "unlock": - return "Unlock"; - case "logOut": - return "Log Out"; - case "logOutConfirmation": - return "Confirm Log Out"; - case "masterPass": - return "Master Password"; - } - return ""; - }); - - // Trigger ngOnInit - fixture.detectChanges(); - - // Wait for html loading to complete - await firstValueFrom( - interval(10).pipe( - map(() => component["loading"]), - takeWhile((loading) => loading, true), - timeout(5000), - ), - ); - - // Wait for html to render - fixture.detectChanges(); - - form = fixture.debugElement.query(By.css("form")); - }); - - describe("form rendering", () => { - it("should render form with label", () => { - expect(form).toBeTruthy(); - expect(form.nativeElement).toBeInstanceOf(HTMLFormElement); - - const bitLabel = form.query(By.css("bit-label")); - expect(bitLabel).toBeTruthy(); - expect(bitLabel.nativeElement).toBeInstanceOf(HTMLElement); - expect((bitLabel.nativeElement as HTMLElement).textContent?.trim()).toBe("Master Password"); - }); - - it("should render master password input field", () => { - const input = form.query(By.css('input[formControlName="masterPassword"]')); - - expect(input).toBeTruthy(); - expect(input.nativeElement).toBeInstanceOf(HTMLInputElement); - const inputElement = input.nativeElement as HTMLInputElement; - expect(inputElement.type).toEqual("password"); - expect(inputElement.name).toEqual("masterPassword"); - expect(inputElement.required).toEqual(true); - expect(inputElement.attributes).toHaveProperty("bitInput"); - }); - - it("should render password toggle button", () => { - const toggleButton = form.query(By.css("button[bitPasswordInputToggle]")); - - expect(toggleButton).toBeTruthy(); - expect(toggleButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const toggleButtonElement = toggleButton.nativeElement as HTMLButtonElement; - expect(toggleButtonElement.type).toEqual("button"); - expect(toggleButtonElement.attributes).toHaveProperty("bitIconButton"); - }); - - it("should render unlock submit button", () => { - const submitButton = form.query(By.css('button[type="submit"]')); - - expect(submitButton).toBeTruthy(); - expect(submitButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const submitButtonElement = submitButton.nativeElement as HTMLButtonElement; - expect(submitButtonElement.type).toEqual("submit"); - expect(submitButtonElement.attributes).toHaveProperty("bitButton"); - expect(submitButtonElement.attributes).toHaveProperty("bitFormButton"); - expect(submitButtonElement.textContent?.trim()).toEqual("Unlock"); - }); - - it("should render logout button", () => { - const logoutButton = form.query( - By.css('button[type="button"]:not([bitPasswordInputToggle])'), - ); - - expect(logoutButton).toBeTruthy(); - expect(logoutButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const logoutButtonElement = logoutButton.nativeElement as HTMLButtonElement; - expect(logoutButtonElement.type).toEqual("button"); - expect(logoutButtonElement.textContent?.trim()).toEqual("Log Out"); - }); - }); - - describe("unlock", () => { - it("should unlock with master password when unlock button is clicked", async () => { - const unlockViaMasterPasswordFunction = jest - .spyOn(component, "unlockViaMasterPassword") - .mockImplementation(); - const submitButton = form.query(By.css('button[type="submit"]')); - expect(submitButton).toBeTruthy(); - expect(submitButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const submitButtonElement = submitButton.nativeElement as HTMLButtonElement; - submitButtonElement.click(); - - expect(unlockViaMasterPasswordFunction).toHaveBeenCalled(); - }); - }); - - describe("logout", () => { - it("should logout when logout button is clicked", async () => { - const logOut = jest.spyOn(component, "logOut").mockImplementation(); - const logoutButton = form.query( - By.css('button[type="button"]:not([bitPasswordInputToggle])'), - ); - - expect(logoutButton).toBeTruthy(); - expect(logoutButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const logoutButtonElement = logoutButton.nativeElement as HTMLButtonElement; - - logoutButtonElement.click(); - - expect(logOut).toHaveBeenCalled(); - }); - }); - - describe("password input", () => { - it("should bind form input to masterPassword form control", async () => { - const input = form.query(By.css('input[formControlName="masterPassword"]')); - expect(input).toBeTruthy(); - expect(input.nativeElement).toBeInstanceOf(HTMLInputElement); - expect(component.formGroup).toBeTruthy(); - const masterPasswordControl = component.formGroup!.get("masterPassword"); - expect(masterPasswordControl).toBeTruthy(); - - masterPasswordControl!.setValue("test-password"); - fixture.detectChanges(); - - const inputElement = input.nativeElement as HTMLInputElement; - expect(inputElement.value).toEqual("test-password"); - }); - - it("should validate required master password field", async () => { - const formGroup = component.formGroup; - - // Initially form should be invalid (empty required field) - expect(formGroup?.invalid).toEqual(true); - expect(formGroup?.get("masterPassword")?.hasError("required")).toBe(true); - - // Set a value - formGroup?.get("masterPassword")?.setValue("test-password"); - - expect(formGroup?.invalid).toEqual(false); - expect(formGroup?.get("masterPassword")?.hasError("required")).toBe(false); - }); - - it("should toggle password visibility when toggle button is clicked", async () => { - const toggleButton = form.query(By.css("button[bitPasswordInputToggle]")); - expect(toggleButton).toBeTruthy(); - expect(toggleButton.nativeElement).toBeInstanceOf(HTMLButtonElement); - const toggleButtonElement = toggleButton.nativeElement as HTMLButtonElement; - const input = form.query(By.css('input[formControlName="masterPassword"]')); - expect(input).toBeTruthy(); - expect(input.nativeElement).toBeInstanceOf(HTMLInputElement); - const inputElement = input.nativeElement as HTMLInputElement; - - // Initially password should be hidden - expect(component.showPassword).toEqual(false); - expect(inputElement.type).toEqual("password"); - - // Click toggle button - toggleButtonElement.click(); - fixture.detectChanges(); - - expect(component.showPassword).toEqual(true); - expect(inputElement.type).toEqual("text"); - - // Click toggle button again - toggleButtonElement.click(); - fixture.detectChanges(); - - expect(component.showPassword).toEqual(false); - expect(inputElement.type).toEqual("password"); - }); - }); - }); - describe("successfulMasterPasswordUnlock", () => { const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const masterPassword = "test-password"; @@ -519,317 +307,6 @@ describe("LockComponent", () => { } }); - describe("unlockViaMasterPassword", () => { - const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey; - const masterPasswordVerificationResponse: MasterPasswordVerificationResponse = { - masterKey: mockMasterKey, - email: "test-email@example.com", - policyOptions: null, - }; - const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; - const masterPassword = "test-password"; - - beforeEach(async () => { - mockI18nService.t.mockImplementation((key: string) => { - switch (key) { - case "errorOccurred": - return "Error Occurred"; - case "masterPasswordRequired": - return "Master Password is required"; - case "invalidMasterPassword": - return "Invalid Master Password"; - } - return ""; - }); - - component.buildMasterPasswordForm(); - component.formGroup!.controls.masterPassword.setValue(masterPassword); - component.activeAccount = await firstValueFrom(mockAccountService.activeAccount$); - mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue( - masterPasswordVerificationResponse, - ); - mockMasterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey); - }); - - it("should not unlock and show password invalid toast when master password is empty", async () => { - component.formGroup!.controls.masterPassword.setValue(""); - - await component.unlockViaMasterPassword(); - - expect(mockToastService.showToast).toHaveBeenCalledWith({ - variant: "error", - title: "Error Occurred", - message: "Master Password is required", - }); - expect(mockKeyService.setUserKey).not.toHaveBeenCalled(); - }); - - it("should not unlock when no active account", async () => { - component.activeAccount = null; - - await component.unlockViaMasterPassword(); - - expect(mockToastService.showToast).not.toHaveBeenCalled(); - expect(mockKeyService.setUserKey).not.toHaveBeenCalled(); - }); - - it("should not unlock when no form group", async () => { - component.formGroup = null; - - await component.unlockViaMasterPassword(); - - expect(mockToastService.showToast).not.toHaveBeenCalled(); - expect(mockKeyService.setUserKey).not.toHaveBeenCalled(); - }); - - it("should not unlock when input password verification failed due to invalid password", async () => { - mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce( - new Error("invalid password"), - ); - - await component.unlockViaMasterPassword(); - - expect(mockToastService.showToast).toHaveBeenCalledWith({ - variant: "error", - title: "Error Occurred", - message: "Invalid Master Password", - }); - expect(mockUserVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: masterPassword, - } as MasterPasswordVerification, - userId, - component.activeAccount!.email, - ); - expect(mockKeyService.setUserKey).not.toHaveBeenCalled(); - }); - - it("should not unlock when valid password but user have no user key", async () => { - mockMasterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null); - - await component.unlockViaMasterPassword(); - - expect(mockToastService.showToast).toHaveBeenCalledWith({ - variant: "error", - title: "Error Occurred", - message: "Invalid Master Password", - }); - expect(mockMasterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - userId, - ); - expect(mockKeyService.setUserKey).not.toHaveBeenCalled(); - }); - - it("should unlock and set user key and sync when valid password", async () => { - await component.unlockViaMasterPassword(); - - assertUnlocked(); - expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); - }); - - it.each([ - [false, undefined, false], - [false, { enforceOnLogin: false } as MasterPasswordPolicyOptions, false], - [false, { enforceOnLogin: false } as MasterPasswordPolicyOptions, true], - [true, { enforceOnLogin: true } as MasterPasswordPolicyOptions, false], - [false, { enforceOnLogin: true } as MasterPasswordPolicyOptions, true], - ])( - "should unlock and force set password change = %o when master password on login = %o and evaluated password against policy = %o and policy set during user verification by master password", - async (forceSetPassword, masterPasswordPolicyOptions, evaluatedMasterPassword) => { - jest.spyOn(component as any, "doContinue").mockImplementation(async () => { - await mockBiometricStateService.resetUserPromptCancelled(); - mockMessagingService.send("unlocked"); - - if (masterPasswordPolicyOptions?.enforceOnLogin) { - const passwordStrengthResult = mockPasswordStrengthService.getPasswordStrength( - masterPassword, - component.activeAccount!.email, - ); - const evaluated = mockPolicyService.evaluateMasterPassword( - passwordStrengthResult.score, - masterPassword, - masterPasswordPolicyOptions, - ); - if (!evaluated) { - await mockMasterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.WeakMasterPassword, - userId, - ); - } - } - - await mockSyncService.fullSync(false); - await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); - }); - - mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({ - ...masterPasswordVerificationResponse, - policyOptions: - masterPasswordPolicyOptions != null - ? new MasterPasswordPolicyResponse({ - EnforceOnLogin: masterPasswordPolicyOptions.enforceOnLogin, - }) - : null, - } as MasterPasswordVerificationResponse); - const passwordStrengthResult = { score: 1 } as ZXCVBNResult; - mockPasswordStrengthService.getPasswordStrength.mockReturnValue(passwordStrengthResult); - mockPolicyService.evaluateMasterPassword.mockReturnValue(evaluatedMasterPassword); - - await component.unlockViaMasterPassword(); - - assertUnlocked(); - if (masterPasswordPolicyOptions?.enforceOnLogin) { - expect(mockPasswordStrengthService.getPasswordStrength).toHaveBeenCalledWith( - masterPassword, - component.activeAccount!.email, - ); - expect(mockPolicyService.evaluateMasterPassword).toHaveBeenCalledWith( - passwordStrengthResult.score, - masterPassword, - masterPasswordPolicyOptions, - ); - } - if (forceSetPassword) { - expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( - ForceSetPasswordReason.WeakMasterPassword, - userId, - ); - } else { - expect(mockMasterPasswordService.setForceSetPasswordReason).not.toHaveBeenCalled(); - } - }, - ); - - it.each([ - [false, undefined, false], - [false, { enforceOnLogin: false } as MasterPasswordPolicyOptions, false], - [false, { enforceOnLogin: false } as MasterPasswordPolicyOptions, true], - [true, { enforceOnLogin: true } as MasterPasswordPolicyOptions, false], - [false, { enforceOnLogin: true } as MasterPasswordPolicyOptions, true], - ])( - "should unlock and force set password change = %o when master password on login = %o and evaluated password against policy = %o and policy loaded from policy service", - async (forceSetPassword, masterPasswordPolicyOptions, evaluatedMasterPassword) => { - mockPolicyService.masterPasswordPolicyOptions$.mockReturnValue( - of(masterPasswordPolicyOptions), - ); - const passwordStrengthResult = { score: 1 } as ZXCVBNResult; - mockPasswordStrengthService.getPasswordStrength.mockReturnValue(passwordStrengthResult); - mockPolicyService.evaluateMasterPassword.mockReturnValue(evaluatedMasterPassword); - - await component.unlockViaMasterPassword(); - - assertUnlocked(); - expect(mockPolicyService.masterPasswordPolicyOptions$).toHaveBeenCalledWith(userId); - if (masterPasswordPolicyOptions?.enforceOnLogin) { - expect(mockPasswordStrengthService.getPasswordStrength).toHaveBeenCalledWith( - masterPassword, - component.activeAccount!.email, - ); - expect(mockPolicyService.evaluateMasterPassword).toHaveBeenCalledWith( - passwordStrengthResult.score, - masterPassword, - masterPasswordPolicyOptions, - ); - } - if (forceSetPassword) { - expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( - ForceSetPasswordReason.WeakMasterPassword, - userId, - ); - } else { - expect(mockMasterPasswordService.setForceSetPasswordReason).not.toHaveBeenCalled(); - } - }, - ); - - it.each([ - [true, ClientType.Browser], - [false, ClientType.Cli], - [false, ClientType.Desktop], - [false, ClientType.Web], - ])( - "should unlock and navigate by url to previous url = %o when client type = %o and previous url was set", - async (shouldNavigate, clientType) => { - const previousUrl = "/test-url"; - component.clientType = clientType; - mockLockComponentService.getPreviousUrl.mockReturnValue(previousUrl); - - await component.unlockViaMasterPassword(); - - assertUnlocked(); - if (shouldNavigate) { - expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(previousUrl); - } else { - expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); - } - }, - ); - - it.each([ - ["/tabs/current", ClientType.Browser], - [undefined, ClientType.Cli], - ["vault", ClientType.Desktop], - ["vault", ClientType.Web], - ])( - "should unlock and navigate to success url = %o when client type = %o", - async (navigateUrl, clientType) => { - component.clientType = clientType; - mockLockComponentService.getPreviousUrl.mockReturnValue(null); - - jest.spyOn(component as any, "doContinue").mockImplementation(async () => { - await mockBiometricStateService.resetUserPromptCancelled(); - mockMessagingService.send("unlocked"); - await mockSyncService.fullSync(false); - await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); - await mockRouter.navigate([navigateUrl]); - }); - - await component.unlockViaMasterPassword(); - - assertUnlocked(); - expect(mockRouter.navigate).toHaveBeenCalledWith([navigateUrl]); - }, - ); - - it("should unlock and close browser extension popout on firefox extension", async () => { - component.shouldClosePopout = true; - mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); - - jest.spyOn(component as any, "doContinue").mockImplementation(async () => { - await mockBiometricStateService.resetUserPromptCancelled(); - mockMessagingService.send("unlocked"); - await mockSyncService.fullSync(false); - await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded( - component.activeAccount!.id, - ); - mockLockComponentService.closeBrowserExtensionPopout(); - }); - - await component.unlockViaMasterPassword(); - - assertUnlocked(); - expect(mockLockComponentService.closeBrowserExtensionPopout).toHaveBeenCalled(); - }); - - function assertUnlocked() { - expect(mockToastService.showToast).not.toHaveBeenCalled(); - expect(mockMasterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - userId, - ); - expect(mockKeyService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); - expect(mockDeviceTrustService.trustDeviceIfRequired).toHaveBeenCalledWith(userId); - expect(mockBiometricStateService.resetUserPromptCancelled).toHaveBeenCalled(); - expect(mockMessagingService.send).toHaveBeenCalledWith("unlocked"); - expect(mockSyncService.fullSync).toHaveBeenCalledWith(false); - expect(mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded).toHaveBeenCalledWith( - userId, - ); - } - }); - describe("logOut", () => { it("should log out user and redirect to login page when dialog confirmed", async () => { mockDialogService.openSimpleDialog.mockResolvedValue(true); diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 4b43f0ec784..03ab6033441 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -10,7 +10,6 @@ import { mergeMap, Subject, switchMap, - take, takeUntil, tap, } from "rxjs"; @@ -20,22 +19,14 @@ import { LogoutService } from "@bitwarden/auth/common"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - MasterPasswordVerification, - MasterPasswordVerificationResponse, -} from "@bitwarden/common/auth/types/verification"; import { ClientType, DeviceType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -115,10 +106,6 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); protected loading = true; - protected unlockWithMasterPasswordUnlockDataFlag$ = this.configService.getFeatureFlag$( - FeatureFlag.UnlockWithMasterPasswordUnlockData, - ); - activeAccount: Account | null = null; clientType?: ClientType; @@ -144,7 +131,6 @@ export class LockComponent implements OnInit, OnDestroy { biometricUnlockBtnText?: string; - // masterPassword = ""; showPassword = false; private enforcedMasterPasswordOptions?: MasterPasswordPolicyOptions = undefined; @@ -164,7 +150,6 @@ export class LockComponent implements OnInit, OnDestroy { constructor( private accountService: AccountService, private pinService: PinServiceAbstraction, - private userVerificationService: UserVerificationService, private keyService: KeyService, private platformUtilsService: PlatformUtilsService, private router: Router, @@ -189,7 +174,6 @@ export class LockComponent implements OnInit, OnDestroy { private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private encryptedMigrator: EncryptedMigrator, - private configService: ConfigService, // desktop deps private broadcasterService: BroadcasterService, ) {} @@ -246,21 +230,10 @@ export class LockComponent implements OnInit, OnDestroy { .subscribe((activeUnlockOption: UnlockOptionValue | null) => { if (activeUnlockOption === UnlockOption.Pin) { this.buildPinForm(); - } else if (activeUnlockOption === UnlockOption.MasterPassword) { - this.buildMasterPasswordForm(); } }); } - buildMasterPasswordForm() { - this.formGroup = this.formBuilder.group( - { - masterPassword: ["", [Validators.required]], - }, - { updateOn: "submit" }, - ); - } - private buildPinForm() { this.formGroup = this.formBuilder.group( { @@ -406,8 +379,6 @@ export class LockComponent implements OnInit, OnDestroy { if (this.activeUnlockOption === UnlockOption.Pin) { return await this.unlockViaPin(); } - - await this.unlockViaMasterPassword(); }; async logOut() { @@ -489,25 +460,6 @@ export class LockComponent implements OnInit, OnDestroy { } } - //TODO PM-25385 This code isn't used and should be removed when removing the UnlockWithMasterPasswordUnlockData feature flag. - togglePassword() { - this.showPassword = !this.showPassword; - const input = document.getElementById( - this.unlockOptions?.pin.enabled ? "pin" : "masterPassword", - ); - - if (input == null) { - return; - } - - if (this.ngZone.isStable) { - input.focus(); - } else { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); - } - } - private validatePin(): boolean { if (this.formGroup?.invalid) { this.toastService.showToast({ @@ -565,83 +517,6 @@ export class LockComponent implements OnInit, OnDestroy { } } - // TODO PM-25385 remove when removing the UnlockWithMasterPasswordUnlockData feature flag. - private validateMasterPassword(): boolean { - if (this.formGroup?.invalid) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return false; - } - - return true; - } - - // TODO PM-25385 remove when removing the UnlockWithMasterPasswordUnlockData feature flag. - async unlockViaMasterPassword() { - if (!this.validateMasterPassword() || this.formGroup == null || this.activeAccount == null) { - return; - } - - const masterPassword = this.formGroup.controls.masterPassword.value; - - const verification = { - type: VerificationType.MasterPassword, - secret: masterPassword, - } as MasterPasswordVerification; - - let passwordValid = false; - let masterPasswordVerificationResponse: MasterPasswordVerificationResponse | null = null; - try { - masterPasswordVerificationResponse = - await this.userVerificationService.verifyUserByMasterPassword( - verification, - this.activeAccount.id, - this.activeAccount.email, - ); - - if (masterPasswordVerificationResponse?.policyOptions != null) { - this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( - masterPasswordVerificationResponse.policyOptions, - ); - } else { - this.enforcedMasterPasswordOptions = undefined; - } - - passwordValid = true; - } catch (e) { - this.logService.error(e); - } - - if (!passwordValid) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidMasterPassword"), - }); - return; - } - - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - masterPasswordVerificationResponse!.masterKey, - this.activeAccount.id, - ); - if (userKey == null) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidMasterPassword"), - }); - return; - } - - await this.setUserKeyAndContinue(userKey, { - passwordEvaluation: { masterPassword }, - }); - } - async successfulMasterPasswordUnlock(event: { userKey: UserKey; masterPassword: string; diff --git a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.html b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.html index 185fb0666c4..4c7cdd48353 100644 --- a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.html +++ b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.html @@ -11,7 +11,13 @@ required appInputVerbatim /> - +
diff --git a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.spec.ts b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.spec.ts index 71287e7684c..dabab3e558a 100644 --- a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.spec.ts @@ -6,10 +6,12 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClientType } from "@bitwarden/client-type"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserKey } from "@bitwarden/common/types/key"; @@ -21,6 +23,7 @@ import { ToastService, } from "@bitwarden/components"; import { BiometricsStatus } from "@bitwarden/key-management"; +import { CommandDefinition, MessageListener } from "@bitwarden/messaging"; import { UserId } from "@bitwarden/user-core"; import { UnlockOption, UnlockOptions } from "../../services/lock-component.service"; @@ -36,6 +39,8 @@ describe("MasterPasswordLockComponent", () => { const i18nService = mock(); const toastService = mock(); const logService = mock(); + const platformUtilsService = mock(); + const messageListener = mock(); const mockMasterPassword = "testExample"; const activeAccount: Account = { @@ -103,6 +108,8 @@ describe("MasterPasswordLockComponent", () => { { provide: I18nService, useValue: i18nService }, { provide: ToastService, useValue: toastService }, { provide: LogService, useValue: logService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + { provide: MessageListener, useValue: messageListener }, ], }).compileComponents(); @@ -281,6 +288,29 @@ describe("MasterPasswordLockComponent", () => { }); }); + describe("ngOnInit", () => { + test.each([ClientType.Browser, ClientType.Web])( + "does nothing when client type is %s", + async (clientType) => { + platformUtilsService.getClientType.mockReturnValue(clientType); + messageListener.messages$.mockReturnValue(of({})); + + await component.ngOnInit(); + + expect(messageListener.messages$).not.toHaveBeenCalled(); + }, + ); + + it("subscribes to windowHidden messages when client type is Desktop", async () => { + platformUtilsService.getClientType.mockReturnValue(ClientType.Desktop); + messageListener.messages$.mockReturnValue(of({})); + + await component.ngOnInit(); + + expect(messageListener.messages$).toHaveBeenCalledWith(new CommandDefinition("windowHidden")); + }); + }); + describe("logout", () => { it("emits logOut event when logout button is clicked", () => { const setup = setupComponent(); diff --git a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.ts b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.ts index ff1e7f53e5f..1237869717f 100644 --- a/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.ts +++ b/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.ts @@ -1,12 +1,23 @@ -import { Component, computed, inject, input, model, output } from "@angular/core"; +import { + Component, + computed, + inject, + input, + model, + OnDestroy, + OnInit, + output, +} from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClientType } from "@bitwarden/client-type"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserKey } from "@bitwarden/common/types/key"; import { AsyncActionsModule, @@ -17,6 +28,7 @@ import { } from "@bitwarden/components"; import { BiometricsStatus } from "@bitwarden/key-management"; import { LogService } from "@bitwarden/logging"; +import { CommandDefinition, MessageListener } from "@bitwarden/messaging"; import { UserId } from "@bitwarden/user-core"; import { @@ -39,12 +51,14 @@ import { IconButtonModule, ], }) -export class MasterPasswordLockComponent { +export class MasterPasswordLockComponent implements OnInit, OnDestroy { private readonly accountService = inject(AccountService); private readonly masterPasswordUnlockService = inject(MasterPasswordUnlockService); private readonly i18nService = inject(I18nService); private readonly toastService = inject(ToastService); private readonly logService = inject(LogService); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly messageListener = inject(MessageListener); UnlockOption = UnlockOption; readonly activeUnlockOption = model.required(); @@ -64,6 +78,9 @@ export class MasterPasswordLockComponent { successfulUnlock = output<{ userKey: UserKey; masterPassword: string }>(); logOut = output(); + protected showPassword = false; + private destroy$ = new Subject(); + formGroup = new FormGroup({ masterPassword: new FormControl("", { validators: [Validators.required], @@ -71,6 +88,22 @@ export class MasterPasswordLockComponent { }), }); + async ngOnInit(): Promise { + if (this.platformUtilsService.getClientType() === ClientType.Desktop) { + this.messageListener + .messages$(new CommandDefinition("windowHidden")) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.showPassword = false; + }); + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + submit = async () => { this.formGroup.markAllAsTouched(); const masterPassword = this.formGroup.controls.masterPassword.value;