mirror of
https://github.com/bitwarden/clients.git
synced 2026-01-11 20:07:18 +00:00
[PM-25385] Remove unlock-with-master-password-unlock-data flag (#18010)
* remove feature flag from lock component * Add missing windowHidden desktop feature * Remove the flag from CLI unlock * Remove the flag from enum file
This commit is contained in:
parent
fa45110420
commit
0e2748784b
12 changed files with 129 additions and 953 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<AccountService>();
|
||||
const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
const keyService = mock<KeyService>();
|
||||
const userVerificationService = mock<UserVerificationService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<ConsoleLogService>();
|
||||
const keyConnectorService = mock<KeyConnectorService>();
|
||||
|
|
@ -44,7 +37,6 @@ describe("UnlockCommand", () => {
|
|||
const i18nService = mock<I18nService>();
|
||||
const encryptedMigrator = mock<EncryptedMigrator>();
|
||||
const masterPasswordUnlockService = mock<MasterPasswordUnlockService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, any>) {
|
||||
|
|
@ -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$)) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -121,11 +121,7 @@
|
|||
</ng-container>
|
||||
|
||||
<!-- MP Unlock -->
|
||||
@if (
|
||||
(unlockWithMasterPasswordUnlockDataFlag$ | async) &&
|
||||
unlockOptions.masterPassword.enabled &&
|
||||
activeUnlockOption === UnlockOption.MasterPassword
|
||||
) {
|
||||
@if (unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword) {
|
||||
<bit-master-password-lock
|
||||
[(activeUnlockOption)]="activeUnlockOption"
|
||||
[unlockOptions]="unlockOptions"
|
||||
|
|
@ -133,75 +129,5 @@
|
|||
(successfulUnlock)="successfulMasterPasswordUnlock($event)"
|
||||
(logOut)="logOut()"
|
||||
></bit-master-password-lock>
|
||||
} @else {
|
||||
<ng-container
|
||||
*ngIf="
|
||||
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
|
||||
"
|
||||
>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
|
||||
<!-- [attr.aria-pressed]="showPassword" -->
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -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<AnonLayoutWrapperDataService>();
|
||||
const mockBroadcasterService = mock<BroadcasterService>();
|
||||
const mockEncryptedMigrator = mock<EncryptedMigrator>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<void>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@
|
|||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
|
|
|
|||
|
|
@ -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<I18nService>();
|
||||
const toastService = mock<ToastService>();
|
||||
const logService = mock<LogService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
const messageListener = mock<MessageListener>();
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<UnlockOptionValue>();
|
||||
|
|
@ -64,6 +78,9 @@ export class MasterPasswordLockComponent {
|
|||
successfulUnlock = output<{ userKey: UserKey; masterPassword: string }>();
|
||||
logOut = output<void>();
|
||||
|
||||
protected showPassword = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
formGroup = new FormGroup({
|
||||
masterPassword: new FormControl("", {
|
||||
validators: [Validators.required],
|
||||
|
|
@ -71,6 +88,22 @@ export class MasterPasswordLockComponent {
|
|||
}),
|
||||
});
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue