Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
[disabled]="unlockingViaBiometrics || !biometricsAvailable"
[loading]="unlockingViaBiometrics"
block
[bitTooltip]="biometricUnavailabilityReason"
(click)="unlockViaBiometrics()"
>
<span> {{ biometricUnlockBtnText | i18n }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { firstValueFrom, interval, map, of, takeWhile, timeout } from "rxjs";
import { ZXCVBNResult } from "zxcvbn";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LogoutService } from "@bitwarden/auth/common";
import { LogoutService, UserDecryptionOptionsServiceAbstraction } 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
Expand Down Expand Up @@ -93,6 +93,7 @@ describe("LockComponent", () => {
const mockAnonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
const mockBroadcasterService = mock<BroadcasterService>();
const mockConfigService = mock<ConfigService>();
const mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();

beforeEach(async () => {
jest.resetAllMocks();
Expand All @@ -111,6 +112,7 @@ describe("LockComponent", () => {
mockDeviceTrustService.trustDeviceIfRequired.mockResolvedValue();
mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded.mockResolvedValue();
mockAnonLayoutWrapperDataService.setAnonLayoutWrapperData.mockImplementation(() => {});
mockUserDecryptionOptionsService.hasMasterPassword$ = of(true);

await TestBed.configureTestingModule({
imports: [
Expand Down Expand Up @@ -151,6 +153,10 @@ describe("LockComponent", () => {
{ provide: AnonLayoutWrapperDataService, useValue: mockAnonLayoutWrapperDataService },
{ provide: BroadcasterService, useValue: mockBroadcasterService },
{ provide: ConfigService, useValue: mockConfigService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
],
})
.overrideProvider(DialogService, { useValue: mockDialogService })
Expand Down
44 changes: 41 additions & 3 deletions libs/key-management-ui/src/lock/components/lock.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LogoutService } from "@bitwarden/auth/common";
import { LogoutService, UserDecryptionOptionsServiceAbstraction } 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";
Expand All @@ -43,6 +43,7 @@ import { SyncService } from "@bitwarden/common/platform/sync";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserKey } from "@bitwarden/common/types/key";
import {
TooltipDirective,
AsyncActionsModule,
AnonLayoutWrapperDataService,
ButtonModule,
Expand Down Expand Up @@ -100,6 +101,7 @@ const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
AsyncActionsModule,
IconButtonModule,
MasterPasswordLockComponent,
TooltipDirective,
],
})
export class LockComponent implements OnInit, OnDestroy {
Expand Down Expand Up @@ -178,6 +180,7 @@ export class LockComponent implements OnInit, OnDestroy {
private lockComponentService: LockComponentService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private configService: ConfigService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
// desktop deps
private broadcasterService: BroadcasterService,
) {}
Expand Down Expand Up @@ -276,7 +279,26 @@ export class LockComponent implements OnInit, OnDestroy {
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
);

this.setDefaultActiveUnlockOption(this.unlockOptions);
const canUsePassword = await firstValueFrom(
this.userDecryptionOptionsService.hasMasterPassword$,
);
const canUsePin =
(await this.pinService.isPinSet(activeAccount.id)) &&
(await this.pinService.isPinDecryptionAvailable(activeAccount.id));
const canUseBiometrics = [
BiometricsStatus.Available,
BiometricsStatus.HardwareUnavailable,
BiometricsStatus.DesktopDisconnected,
BiometricsStatus.NotEnabledInConnectedDesktopApp,
].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Inconsistent data source: This calls getBiometricsStatusForUser() directly after getAvailableUnlockOptions$() already fetched biometric status.

The biometric status is already available in this.unlockOptions.biometrics.biometricsStatus (line 283-285). Using a separate call could result in inconsistent state if the biometric status changes between the two calls.

Suggested change:

const canUseBiometrics = [
  BiometricsStatus.Available,
  ...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES,
].includes(this.unlockOptions?.biometrics.biometricsStatus ?? BiometricsStatus.PlatformUnsupported);

This was flagged in a previous review and appears unresolved.

if (!canUsePassword && !canUsePin && !canUseBiometrics) {
// User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set.
this.logService.warning("[LockComponent] User cannot unlock again. Logging out!");
await this.logoutService.logout(activeAccount.id);
return;
}

await this.setDefaultActiveUnlockOption(this.unlockOptions);

if (this.unlockOptions?.biometrics.enabled) {
await this.handleBiometricsUnlockEnabled();
Expand All @@ -299,14 +321,30 @@ export class LockComponent implements OnInit, OnDestroy {
});
}

private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
private async setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
const biometricsStatus: BiometricsStatus | null =
this.activeAccount != null
? await this.biometricService.getBiometricsStatusForUser(this.activeAccount.id)
: null;

// Priorities should be Biometrics > Pin > Master Password for speed
if (unlockOptions?.biometrics.enabled) {
this.activeUnlockOption = UnlockOption.Biometrics;
} else if (unlockOptions?.pin.enabled) {
this.activeUnlockOption = UnlockOption.Pin;
} else if (unlockOptions?.masterPassword.enabled) {
this.activeUnlockOption = UnlockOption.MasterPassword;
// If biometrics is temporarily unavailable for masterpassword-less users, but they have biometrics configured,
// then show the biometrics screen so the user knows why they can't unlock, and to give them the option to log out.
} else if (
biometricsStatus != null &&
[
BiometricsStatus.HardwareUnavailable,
BiometricsStatus.DesktopDisconnected,
BiometricsStatus.NotEnabledInConnectedDesktopApp,
].includes(biometricsStatus)
) {
this.activeUnlockOption = UnlockOption.Biometrics;
}
}

Expand Down
Loading