From 756c02cec269141279f78f19436f691eed933ca3 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:52:06 -0500 Subject: [PATCH] Auth/PM-4596 - Extract PIN and Biometrics unlock method logic into re-useable services for user verification (#7107) * PM-4596 - PinCryptoService first draft * PM-4596 - PinCryptoService - Refactor pinKeyEncryptedKey retrievals out into own method getPinKeyEncryptedKeys * PM-4596 - npm ci + npm run prettier to fix lint issues * PM-4596 - PinCryptoService - Add kdf types * PM-4596 - PinCryptoService - Refactor pin validation into own helper method. * PM-4596 - Rename pin-crypto.service.ts to pin-crypto.service.implementation.ts * PM-4596 - PinCryptoService - add additional logging for error states. * PM-4596 - JslibServicesModule - register new PinCryptoService and PinCryptoServiceAbstraction * PM-4596 - PinCryptoService - modify decryptUserKeyWithPin signature to not require email to match MP verification process in user verification service. * PM-4596 - Lock components - use new PinCryptoService.decryptUserKeyWithPin(...) to get user key + refactor base comp unlock with pin method to improve * PM-4596 - Lock component - if too many invalid attempts, added toast explaining that we were logging the user out due to excess PIN entry attempts * PM-4596 - UserVerificationService - (1) Refactor verifyUser(...) to use switch + separate methods for a cleaner parent method + better extensibility for PIN & biometrics which are TBD (2) Add PIN support to validateInput(...) * PM-4596 - UserVerificationService - add PIN and biometrics functions to verifyUser(...) * PM-4596 - PinCryptoService Spec - start test file - instantiates properly * PM-4596 - PinCryptoService tests - WIP * PM-4596 - PinCryptoService tests - WIP - got success cases working * PM-4596 - pin-crypto.service.implementation.spec.ts renamed to pin-crypto.service.spec.ts * PM-4596 - PinCryptoService.getPinKeyEncryptedKeys(...) - add comment + var name change for clarity * PM-4596 - PinCryptoService tests - test invalid, null return scenarios * PM-4596 - CLI - bw.ts - update UserVerificationService instantiation to include new pinCryptoService * PM-4596 - PinCryptoService - import VaultTimeoutSettingsServiceAbstraction instead of implementation for factory creation to get browser building * PM-4596 - (1) Create pinCryptoServiceFactory for browser background (2) Add it to the existing userVerificationServiceFactory * PM-4596 - Browser - Main.background.ts - Add pinCryptoService and add to userVerificationService dependencies * PM-4596 - UserVerificationService - per PR feedback simplify returns of verifyUserByPIN(...) and verifyUserByBiometrics(...) * PM-4596 - Messages.json on desktop & browser - per PR feedback, adjust tooManyInvalidPinEntryAttemptsLoggingOut translation text to remove "you" * PM-4596 - VerificationType enum - fix line copy mistake and give BIOMETRICS own, unique value. * PM-4596 - VerificationType - rename BIOMETRICS to Biometrics to match existing MasterPassword value case. * PM-4596 - Update verification type to consider whether or not a secret exists as we have added a new verification which doesn't have a type. Add new server and client side verification types. Update all relevant code to pass compilation checks. * PM-4596 - More verification type tweaking * PM-4596 - Verification - verificationHasSecret - tweak logic to be more dynamic and flexible for future verification types * PM-4596 - UpdateTempPasswordComp - use new MasterPasswordVerification * PM-4596 - Desktop - DeleteAcctComp - use VerificationWithSecret to solve compile error w/ accessing secret * PM-4596 - Per discussions with Andreas & Will, move new Pin Crypto services into libs/auth + added @bitwarden/auth path to CLI tsconfig + added new, required index.ts files for exporting service abstractions & implementations * PM-4596 - Fixed missed import fixes for lock components across clients for pin crypto service after moving into @bitwarden/auth * PM-4596 - More PinCryptoService import fixes to get browser & desktop building * PM-4596 - Update desktop lock comp tests to pass by providing new pin crypto service. * PM-4596 - User verification service -update todo * PM-4596 - PinCryptoService - per PR feedback, fix auto import wrong paths. * PM-4596 - PinCryptoService tests - fix imports per PR feedback * PM-4596 - UserVerificationSvc - rename method to validateSecretInput per PR feedback * Fix imports * PM-4596 - PinCryptoService - Refactor naming for clarity and move test cases into describes per PR feedback * reorg libs/auth; expose only libs/auth/core to cli app * PM-4596 - UserVerification - Resolve import issue with importing from libs/auth. Can't use @bitwarden/auth for whatever reason. * PM-4596 - Fix desktop build by fixing import * PM-4596 - Provide PinCryptoService to UserVerificationService * PM-4596 - PinCryptoServiceFactory - you cannot import services from @bitwarden/auth in the background b/c it brings along the libs/auth/components and introduces angular into the background context which doesn't have access to angular which causes random test failures. So, we must separate out the core services just like the CLI to only bring along the angular agnostic services from core. * PM-4596 - Refactor libs/auth to have angular / common + update all imports per discussion with Matt & Will. Introduced circular dep between PinCryptoService + VaultTimeoutSettingsService + UserVerificationService * PM-4596 - VaultTimeoutSettingsService - Refactor UserVerificationService out of the service and update all service instantiations and tests. The use of the UserVerificationService.hasMasterPassword method no longer needs to be used for backwards compatibility. This resolves the circular dependency between the PinCryptoService, the UserVerificationService, and the VaultTimeoutSettingsService. We will likely refactor the hasMasterPassword method out of the UserVerificationService in the future. * PM-4596 - Update CL tsconfig.libs.json to add new auth/common and auth/angular paths for jslib-services.module imports of pin crypto service to work and for test code coverage to run successfully. * PM-4596 - Address PR feedback * PM-4596 - Update root tsconfig (only used by storybook) to add new libs/auth paths to fix chromatic build pipeline. * PM-4596 - Actually update tsconfig with proper routes to fix storybook * PM-4596 - UserVerificationService - verifyUserByBiometrics - add error handling logic to convert failed or cancelled biometrics verification to a usable boolean * PM-4596 - Add missing await * PM-4596 - (1) Add log service and log to user verification service biometric flow to ensure errors are at least revealed to the console (2) Fix factory missing PinCryptoServiceInitOptions * PM-4596 - Use the correct log service abstraction * PM-4596 - Remove unused types per PR review --------- Co-authored-by: William Martin --- apps/browser/src/_locales/en/messages.json | 3 + .../pin-crypto-service.factory.ts | 49 +++++ .../user-verification-service.factory.ts | 11 +- apps/browser/src/auth/popup/lock.component.ts | 3 + .../browser/src/background/main.background.ts | 12 +- .../vault-timeout-settings-service.factory.ts | 8 +- .../src/popup/settings/settings.component.ts | 2 +- apps/browser/tsconfig.json | 3 +- apps/cli/src/bw.ts | 12 +- apps/cli/tsconfig.json | 2 + apps/desktop/src/app/app.component.ts | 2 +- .../src/auth/delete-account.component.ts | 4 +- apps/desktop/src/auth/lock.component.spec.ts | 5 + apps/desktop/src/auth/lock.component.ts | 3 + apps/desktop/src/locales/en/messages.json | 3 + apps/desktop/tsconfig.json | 3 +- .../organizations/members/members.module.ts | 2 +- .../services/rotateable-key-set.service.ts | 2 +- .../webauthn-login-admin.service.spec.ts | 2 +- .../webauthn-login-admin.service.ts | 2 +- apps/web/src/app/auth/lock.component.ts | 3 + .../register-form/register-form.module.ts | 2 +- .../src/app/auth/settings/settings.module.ts | 2 +- .../create-credential-dialog.component.ts | 2 +- .../src/app/shared/loose-components.module.ts | 2 +- apps/web/tsconfig.json | 3 +- .../src/auth/components/lock.component.ts | 82 +++----- .../update-temp-password.component.ts | 4 +- .../src/services/jslib-services.module.ts | 15 +- .../fingerprint-dialog.component.html | 0 .../fingerprint-dialog.component.ts | 0 libs/auth/src/angular/index.ts | 5 + .../password-callout.component.html | 0 .../password-callout.component.ts | 0 .../password-callout.stories.ts | 0 libs/auth/src/common/abstractions/index.ts | 1 + .../pin-crypto.service.abstraction.ts | 4 + libs/auth/src/common/index.ts | 6 + .../src/{ => common}/models/domain/index.ts | 0 .../models/domain/rotateable-key-set.ts | 0 libs/auth/src/{ => common}/models/index.ts | 0 libs/auth/src/common/services/index.ts | 1 + .../pin-crypto.service.implementation.ts | 106 ++++++++++ .../pin-crypto/pin-crypto.service.spec.ts | 191 ++++++++++++++++++ libs/auth/src/index.ts | 3 - .../src/auth/enums/verification-type.ts | 2 + .../user-verification.service.ts | 132 ++++++++---- libs/common/src/auth/types/verification.ts | 21 +- .../vault-timeout-settings.service.spec.ts | 21 +- .../vault-timeout-settings.service.ts | 16 +- libs/shared/tsconfig.libs.json | 3 +- tsconfig.json | 3 +- 52 files changed, 620 insertions(+), 143 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts rename libs/auth/src/{components => angular/fingerprint-dialog}/fingerprint-dialog.component.html (100%) rename libs/auth/src/{components => angular/fingerprint-dialog}/fingerprint-dialog.component.ts (100%) create mode 100644 libs/auth/src/angular/index.ts rename libs/auth/src/{ => angular}/password-callout/password-callout.component.html (100%) rename libs/auth/src/{ => angular}/password-callout/password-callout.component.ts (100%) rename libs/auth/src/{ => angular}/password-callout/password-callout.stories.ts (100%) create mode 100644 libs/auth/src/common/abstractions/index.ts create mode 100644 libs/auth/src/common/abstractions/pin-crypto.service.abstraction.ts create mode 100644 libs/auth/src/common/index.ts rename libs/auth/src/{ => common}/models/domain/index.ts (100%) rename libs/auth/src/{ => common}/models/domain/rotateable-key-set.ts (100%) rename libs/auth/src/{ => common}/models/index.ts (100%) create mode 100644 libs/auth/src/common/services/index.ts create mode 100644 libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts create mode 100644 libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts delete mode 100644 libs/auth/src/index.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 62d6f8df0d..a03d94270a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1493,6 +1493,9 @@ "invalidPin": { "message": "Invalid PIN code." }, + "tooManyInvalidPinEntryAttemptsLoggingOut": { + "message": "Too many invalid PIN entry attempts. Logging out." + }, "unlockWithBiometrics": { "message": "Unlock with biometrics" }, diff --git a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts new file mode 100644 index 0000000000..f5360f48fa --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts @@ -0,0 +1,49 @@ +import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common"; + +import { + VaultTimeoutSettingsServiceInitOptions, + vaultTimeoutSettingsServiceFactory, +} from "../../../background/service-factories/vault-timeout-settings-service.factory"; +import { + CryptoServiceInitOptions, + cryptoServiceFactory, +} from "../../../platform/background/service-factories/crypto-service.factory"; +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../../platform/background/service-factories/log-service.factory"; +import { + StateServiceInitOptions, + stateServiceFactory, +} from "../../../platform/background/service-factories/state-service.factory"; + +type PinCryptoServiceFactoryOptions = FactoryOptions; + +export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions & + StateServiceInitOptions & + CryptoServiceInitOptions & + VaultTimeoutSettingsServiceInitOptions & + LogServiceInitOptions; + +export function pinCryptoServiceFactory( + cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices, + opts: PinCryptoServiceInitOptions, +): Promise { + return factory( + cache, + "pinCryptoService", + opts, + async () => + new PinCryptoService( + await stateServiceFactory(cache, opts), + await cryptoServiceFactory(cache, opts), + await vaultTimeoutSettingsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), + ), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index d0cdfff993..9fa0f4069c 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -14,11 +14,16 @@ import { I18nServiceInitOptions, i18nServiceFactory, } from "../../../platform/background/service-factories/i18n-service.factory"; +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../../platform/background/service-factories/log-service.factory"; import { StateServiceInitOptions, stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { UserVerificationApiServiceInitOptions, userVerificationApiServiceFactory, @@ -30,7 +35,9 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO StateServiceInitOptions & CryptoServiceInitOptions & I18nServiceInitOptions & - UserVerificationApiServiceInitOptions; + UserVerificationApiServiceInitOptions & + PinCryptoServiceInitOptions & + LogServiceInitOptions; export function userVerificationServiceFactory( cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, @@ -46,6 +53,8 @@ export function userVerificationServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), + await pinCryptoServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 0379886bc2..18b07a1974 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -2,6 +2,7 @@ import { Component, NgZone } from "@angular/core"; import { Router } from "@angular/router"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -56,6 +57,7 @@ export class LockComponent extends BaseLockComponent { dialogService: DialogService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, userVerificationService: UserVerificationService, + pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, ) { super( @@ -77,6 +79,7 @@ export class LockComponent extends BaseLockComponent { dialogService, deviceTrustCryptoService, userVerificationService, + pinCryptoService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 438a0aa802..e9a992e9b0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,3 +1,4 @@ +import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common"; import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -246,6 +247,7 @@ export default class MainBackground { authRequestCryptoService: AuthRequestCryptoServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; + pinCryptoService: PinCryptoServiceAbstraction; singleUserStateProvider: SingleUserStateProvider; activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; @@ -482,11 +484,20 @@ export default class MainBackground { this.userVerificationApiService = new UserVerificationApiService(this.apiService); + this.pinCryptoService = new PinCryptoService( + this.stateService, + this.cryptoService, + this.vaultTimeoutSettingsService, + this.logService, + ); + this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, this.i18nService, this.userVerificationApiService, + this.pinCryptoService, + this.logService, ); this.configApiService = new ConfigApiService(this.apiService, this.authService); @@ -524,7 +535,6 @@ export default class MainBackground { this.tokenService, this.policyService, this.stateService, - this.userVerificationService, ); this.vaultFilterService = new VaultFilterService( diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index b2dfd96f5b..9313a27761 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -9,10 +9,6 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../auth/background/service-factories/token-service.factory"; -import { - userVerificationServiceFactory, - UserVerificationServiceInitOptions, -} from "../../auth/background/service-factories/user-verification-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -33,8 +29,7 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & - StateServiceInitOptions & - UserVerificationServiceInitOptions; + StateServiceInitOptions; export function vaultTimeoutSettingsServiceFactory( cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices, @@ -50,7 +45,6 @@ export function vaultTimeoutSettingsServiceFactory( await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), await stateServiceFactory(cache, opts), - await userVerificationServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index d237ca7ebc..d1fee44a3e 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -17,7 +17,7 @@ import { } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { FingerprintDialogComponent } from "@bitwarden/auth"; +import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index d7f44476ad..5e202c054e 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -12,7 +12,8 @@ "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], - "@bitwarden/auth": ["../../libs/auth/src"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], + "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/billing": ["../../libs/billing/src"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 333d6953ca..b4d62f952a 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -4,6 +4,7 @@ import * as path from "path"; import * as program from "commander"; import * as jsdom from "jsdom"; +import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -160,6 +161,7 @@ export class Main { cipherFileUploadService: CipherFileUploadService; keyConnectorService: KeyConnectorService; userVerificationService: UserVerificationService; + pinCryptoService: PinCryptoServiceAbstraction; stateService: StateService; organizationService: OrganizationService; providerService: ProviderService; @@ -433,11 +435,20 @@ export class Main { const lockedCallback = async (userId?: string) => await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); + this.pinCryptoService = new PinCryptoService( + this.stateService, + this.cryptoService, + this.vaultTimeoutSettingsService, + this.logService, + ); + this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, this.i18nService, this.userVerificationApiService, + this.pinCryptoService, + this.logService, ); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -445,7 +456,6 @@ export class Main { this.tokenService, this.policyService, this.stateService, - this.userVerificationService, ); this.vaultTimeoutService = new VaultTimeoutService( diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 395d91564a..7be2480a26 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -13,6 +13,8 @@ "baseUrl": ".", "paths": { "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], + "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/exporter/*": ["../../libs/exporter/src/*"], diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 898d4bb127..ba558152b0 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -15,7 +15,7 @@ import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { FingerprintDialogComponent } from "@bitwarden/auth"; +import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; diff --git a/apps/desktop/src/auth/delete-account.component.ts b/apps/desktop/src/auth/delete-account.component.ts index ad685f25df..a473310d38 100644 --- a/apps/desktop/src/auth/delete-account.component.ts +++ b/apps/desktop/src/auth/delete-account.component.ts @@ -4,7 +4,7 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { Verification } from "@bitwarden/common/auth/types/verification"; +import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -33,7 +33,7 @@ import { UserVerificationComponent } from "../app/components/user-verification.c }) export class DeleteAccountComponent { deleteForm = this.formBuilder.group({ - verification: undefined as Verification | undefined, + verification: undefined as VerificationWithSecret | undefined, }); constructor( diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index fa94731957..1ddbb6a4aa 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -6,6 +6,7 @@ import { of } from "rxjs"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -133,6 +134,10 @@ describe("LockComponent", () => { provide: UserVerificationService, useValue: mock(), }, + { + provide: PinCryptoServiceAbstraction, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 3f62df7dd1..98a2a0e28b 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { switchMap } from "rxjs"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -56,6 +57,7 @@ export class LockComponent extends BaseLockComponent { dialogService: DialogService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, userVerificationService: UserVerificationService, + pinCryptoService: PinCryptoServiceAbstraction, ) { super( router, @@ -76,6 +78,7 @@ export class LockComponent extends BaseLockComponent { dialogService, deviceTrustCryptoService, userVerificationService, + pinCryptoService, ); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 20c1cd7d31..47b31a57a4 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1392,6 +1392,9 @@ "invalidPin": { "message": "Invalid PIN code." }, + "tooManyInvalidPinEntryAttemptsLoggingOut": { + "message": "Too many invalid PIN entry attempts. Logging out." + }, "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 6de6305f92..e2be86acec 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -12,7 +12,8 @@ "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], - "@bitwarden/auth": ["../../libs/auth/src"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], + "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/billing": ["../../libs/billing/src"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 083fe354ff..39246010d5 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth"; +import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; diff --git a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts index cd67693091..04b24e0eb0 100644 --- a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts +++ b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from "@angular/core"; -import { RotateableKeySet } from "@bitwarden/auth"; +import { RotateableKeySet } from "@bitwarden/auth/common"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts index bc92114e87..80386d7f97 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts @@ -2,7 +2,7 @@ import { randomBytes } from "crypto"; import { mock, MockProxy } from "jest-mock-extended"; -import { RotateableKeySet } from "@bitwarden/auth"; +import { RotateableKeySet } from "@bitwarden/auth/common"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts index a59b2395e1..42b6981c21 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts @@ -1,7 +1,7 @@ import { Injectable, Optional } from "@angular/core"; import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; -import { PrfKeySet } from "@bitwarden/auth"; +import { PrfKeySet } from "@bitwarden/auth/common"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction"; import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 6b2aba4f2e..d04d7601fe 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -2,6 +2,7 @@ import { Component, NgZone } from "@angular/core"; import { Router } from "@angular/router"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -43,6 +44,7 @@ export class LockComponent extends BaseLockComponent { dialogService: DialogService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, userVerificationService: UserVerificationService, + pinCryptoService: PinCryptoServiceAbstraction, ) { super( router, @@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent { dialogService, deviceTrustCryptoService, userVerificationService, + pinCryptoService, ); } diff --git a/apps/web/src/app/auth/register-form/register-form.module.ts b/apps/web/src/app/auth/register-form/register-form.module.ts index ffe71715ab..b63cb18506 100644 --- a/apps/web/src/app/auth/register-form/register-form.module.ts +++ b/apps/web/src/app/auth/register-form/register-form.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth"; +import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { SharedModule } from "../../shared"; diff --git a/apps/web/src/app/auth/settings/settings.module.ts b/apps/web/src/app/auth/settings/settings.module.ts index 9d343cf948..2d1f64d1eb 100644 --- a/apps/web/src/app/auth/settings/settings.module.ts +++ b/apps/web/src/app/auth/settings/settings.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth"; +import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { SharedModule } from "../../shared"; import { EmergencyAccessModule } from "../emergency-access"; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index aca1cc482a..fd72cbbb71 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -3,7 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, map, Observable } from "rxjs"; -import { PrfKeySet } from "@bitwarden/auth"; +import { PrfKeySet } from "@bitwarden/auth/common"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 1f0bbe1659..262b52d8e4 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth"; +import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { OrganizationSwitcherComponent } from "../admin-console/components/organization-switcher.component"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 1670518318..00610326a0 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -7,7 +7,8 @@ "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], - "@bitwarden/auth": ["../../libs/auth/src"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], + "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/billing": ["../../libs/billing/src"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index f3398470cd..a8ff753cf9 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -3,6 +3,7 @@ import { Router } from "@angular/router"; import { firstValueFrom, Subject } from "rxjs"; import { concatMap, take, takeUntil } from "rxjs/operators"; +import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -23,7 +24,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -73,6 +73,7 @@ export class LockComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected userVerificationService: UserVerificationService, + protected pinCryptoService: PinCryptoServiceAbstraction, ) {} async ngOnInit() { @@ -150,78 +151,41 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithPin() { - let failed = true; + const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; + try { - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); - let userKeyPin: EncString; - let oldPinKey: EncString; - switch (this.pinStatus) { - case "PERSISTANT": { - userKeyPin = await this.stateService.getPinKeyEncryptedUserKey(); - const oldEncryptedPinKey = await this.stateService.getEncryptedPinProtected(); - oldPinKey = oldEncryptedPinKey ? new EncString(oldEncryptedPinKey) : undefined; - break; - } - case "TRANSIENT": { - userKeyPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral(); - oldPinKey = await this.stateService.getDecryptedPinProtected(); - break; - } - case "DISABLED": { - throw new Error("Pin is disabled"); - } - default: { - const _exhaustiveCheck: never = this.pinStatus; - return _exhaustiveCheck; - } - } + const userKey = await this.pinCryptoService.decryptUserKeyWithPin(this.pin); - let userKey: UserKey; - if (oldPinKey) { - userKey = await this.cryptoService.decryptAndMigrateOldPinKey( - this.pinStatus === "TRANSIENT", - this.pin, - this.email, - kdf, - kdfConfig, - oldPinKey, - ); - } else { - userKey = await this.cryptoService.decryptUserKeyWithPin( - this.pin, - this.email, - kdf, - kdfConfig, - userKeyPin, - ); - } - - const protectedPin = await this.stateService.getProtectedPin(); - const decryptedPin = await this.cryptoService.decryptToUtf8( - new EncString(protectedPin), - userKey, - ); - failed = decryptedPin !== this.pin; - - if (!failed) { + if (userKey) { await this.setUserKeyAndContinue(userKey); + return; // successfully unlocked } - } catch { - failed = true; - } - if (failed) { + // Failure state: invalid PIN or failed decryption this.invalidPinAttempts++; - if (this.invalidPinAttempts >= 5) { + + // Log user out if they have entered an invalid PIN too many times + if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + ); this.messagingService.send("logout"); return; } + this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), this.i18nService.t("invalidPin"), ); + } catch { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("unexpectedError"), + ); } } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index b8ebcdd1c2..d092a8e028 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -9,7 +9,7 @@ import { VerificationType } from "@bitwarden/common/auth/enums/verification-type import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; -import { Verification } from "@bitwarden/common/auth/types/verification"; +import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -31,7 +31,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { enforcedPolicyOptions: MasterPasswordPolicyOptions; showPassword = false; reason: ForceSetPasswordReason = ForceSetPasswordReason.None; - verification: Verification = { + verification: MasterPasswordVerification = { type: VerificationType.MasterPassword, secret: "", }; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b0d273c70f..3ed8d11dbb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,5 +1,6 @@ import { LOCALE_ID, NgModule } from "@angular/core"; +import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common"; import { AvatarUpdateService as AccountUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -478,7 +479,6 @@ import { ModalService } from "./modal.service"; TokenServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction, - UserVerificationServiceAbstraction, ], }, { @@ -628,6 +628,8 @@ import { ModalService } from "./modal.service"; CryptoServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, + PinCryptoServiceAbstraction, + LogService, ], }, { @@ -769,6 +771,17 @@ import { ModalService } from "./modal.service"; useClass: AuthRequestCryptoServiceImplementation, deps: [CryptoServiceAbstraction], }, + { + provide: PinCryptoServiceAbstraction, + useClass: PinCryptoService, + deps: [ + StateServiceAbstraction, + CryptoServiceAbstraction, + VaultTimeoutSettingsServiceAbstraction, + LogService, + ], + }, + { provide: WebAuthnLoginPrfCryptoServiceAbstraction, useClass: WebAuthnLoginPrfCryptoService, diff --git a/libs/auth/src/components/fingerprint-dialog.component.html b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html similarity index 100% rename from libs/auth/src/components/fingerprint-dialog.component.html rename to libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html diff --git a/libs/auth/src/components/fingerprint-dialog.component.ts b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts similarity index 100% rename from libs/auth/src/components/fingerprint-dialog.component.ts rename to libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts new file mode 100644 index 0000000000..7fd35ea8b7 --- /dev/null +++ b/libs/auth/src/angular/index.ts @@ -0,0 +1,5 @@ +/** + * This barrel file should only contain Angular exports + */ +export * from "./fingerprint-dialog/fingerprint-dialog.component"; +export * from "./password-callout/password-callout.component"; diff --git a/libs/auth/src/password-callout/password-callout.component.html b/libs/auth/src/angular/password-callout/password-callout.component.html similarity index 100% rename from libs/auth/src/password-callout/password-callout.component.html rename to libs/auth/src/angular/password-callout/password-callout.component.html diff --git a/libs/auth/src/password-callout/password-callout.component.ts b/libs/auth/src/angular/password-callout/password-callout.component.ts similarity index 100% rename from libs/auth/src/password-callout/password-callout.component.ts rename to libs/auth/src/angular/password-callout/password-callout.component.ts diff --git a/libs/auth/src/password-callout/password-callout.stories.ts b/libs/auth/src/angular/password-callout/password-callout.stories.ts similarity index 100% rename from libs/auth/src/password-callout/password-callout.stories.ts rename to libs/auth/src/angular/password-callout/password-callout.stories.ts diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts new file mode 100644 index 0000000000..5bd8a69d73 --- /dev/null +++ b/libs/auth/src/common/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./pin-crypto.service.abstraction"; diff --git a/libs/auth/src/common/abstractions/pin-crypto.service.abstraction.ts b/libs/auth/src/common/abstractions/pin-crypto.service.abstraction.ts new file mode 100644 index 0000000000..87f8a91411 --- /dev/null +++ b/libs/auth/src/common/abstractions/pin-crypto.service.abstraction.ts @@ -0,0 +1,4 @@ +import { UserKey } from "../../../../common/src/platform/models/domain/symmetric-crypto-key"; +export abstract class PinCryptoServiceAbstraction { + decryptUserKeyWithPin: (pin: string) => Promise; +} diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts new file mode 100644 index 0000000000..f70f8be215 --- /dev/null +++ b/libs/auth/src/common/index.ts @@ -0,0 +1,6 @@ +/** + * This barrel file should only contain non-Angular exports + */ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; diff --git a/libs/auth/src/models/domain/index.ts b/libs/auth/src/common/models/domain/index.ts similarity index 100% rename from libs/auth/src/models/domain/index.ts rename to libs/auth/src/common/models/domain/index.ts diff --git a/libs/auth/src/models/domain/rotateable-key-set.ts b/libs/auth/src/common/models/domain/rotateable-key-set.ts similarity index 100% rename from libs/auth/src/models/domain/rotateable-key-set.ts rename to libs/auth/src/common/models/domain/rotateable-key-set.ts diff --git a/libs/auth/src/models/index.ts b/libs/auth/src/common/models/index.ts similarity index 100% rename from libs/auth/src/models/index.ts rename to libs/auth/src/common/models/index.ts diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts new file mode 100644 index 0000000000..688eefffd8 --- /dev/null +++ b/libs/auth/src/common/services/index.ts @@ -0,0 +1 @@ +export * from "./pin-crypto/pin-crypto.service.implementation"; diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts new file mode 100644 index 0000000000..f43e546845 --- /dev/null +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts @@ -0,0 +1,106 @@ +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; + +import { PinCryptoServiceAbstraction } from "../../abstractions/pin-crypto.service.abstraction"; + +export class PinCryptoService implements PinCryptoServiceAbstraction { + constructor( + private stateService: StateService, + private cryptoService: CryptoService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private logService: LogService, + ) {} + async decryptUserKeyWithPin(pin: string): Promise { + try { + const pinLockType: PinLockType = await this.vaultTimeoutSettingsService.isPinLockSet(); + + const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } = + await this.getPinKeyEncryptedKeys(pinLockType); + + const kdf: KdfType = await this.stateService.getKdfType(); + const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + let userKey: UserKey; + const email = await this.stateService.getEmail(); + if (oldPinKeyEncryptedMasterKey) { + userKey = await this.cryptoService.decryptAndMigrateOldPinKey( + pinLockType === "TRANSIENT", + pin, + email, + kdf, + kdfConfig, + oldPinKeyEncryptedMasterKey, + ); + } else { + userKey = await this.cryptoService.decryptUserKeyWithPin( + pin, + email, + kdf, + kdfConfig, + pinKeyEncryptedUserKey, + ); + } + + if (!userKey) { + this.logService.error(`User key null after pin key decryption.`); + return null; + } + + if (!(await this.validatePin(userKey, pin))) { + this.logService.error(`Pin key decryption successful but pin validation failed.`); + return null; + } + + return userKey; + } catch (error) { + this.logService.error(`Error decrypting user key with pin: ${error}`); + return null; + } + } + + // Note: oldPinKeyEncryptedMasterKey is only used for migrating old pin keys + // and will be null for all migrated accounts + private async getPinKeyEncryptedKeys( + pinLockType: PinLockType, + ): Promise<{ pinKeyEncryptedUserKey: EncString; oldPinKeyEncryptedMasterKey?: EncString }> { + switch (pinLockType) { + case "PERSISTANT": { + const pinKeyEncryptedUserKey = await this.stateService.getPinKeyEncryptedUserKey(); + const oldPinKeyEncryptedMasterKey = await this.stateService.getEncryptedPinProtected(); + return { + pinKeyEncryptedUserKey, + oldPinKeyEncryptedMasterKey: oldPinKeyEncryptedMasterKey + ? new EncString(oldPinKeyEncryptedMasterKey) + : undefined, + }; + } + case "TRANSIENT": { + const pinKeyEncryptedUserKey = await this.stateService.getPinKeyEncryptedUserKeyEphemeral(); + const oldPinKeyEncryptedMasterKey = await this.stateService.getDecryptedPinProtected(); + return { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey }; + } + case "DISABLED": + throw new Error("Pin is disabled"); + default: { + // Compile-time check for exhaustive switch + const _exhaustiveCheck: never = pinLockType; + return _exhaustiveCheck; + } + } + } + + private async validatePin(userKey: UserKey, pin: string): Promise { + const protectedPin = await this.stateService.getProtectedPin(); + const decryptedPin = await this.cryptoService.decryptToUtf8( + new EncString(protectedPin), + userKey, + ); + return decryptedPin === pin; + } +} diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts new file mode 100644 index 0000000000..49ccb04983 --- /dev/null +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts @@ -0,0 +1,191 @@ +import { mock } from "jest-mock-extended"; + +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + VaultTimeoutSettingsService, + PinLockType, +} from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; + +import { PinCryptoService } from "./pin-crypto.service.implementation"; +describe("PinCryptoService", () => { + let pinCryptoService: PinCryptoService; + + const stateService = mock(); + const cryptoService = mock(); + const vaultTimeoutSettingsService = mock(); + const logService = mock(); + + beforeEach(() => { + jest.clearAllMocks(); + + pinCryptoService = new PinCryptoService( + stateService, + cryptoService, + vaultTimeoutSettingsService, + logService, + ); + }); + + it("instantiates", () => { + expect(pinCryptoService).not.toBeFalsy(); + }); + + describe("decryptUserKeyWithPin(...)", () => { + const mockPin = "1234"; + const mockProtectedPin = "protectedPin"; + const DEFAULT_PBKDF2_ITERATIONS = 600000; + const mockUserEmail = "user@example.com"; + const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey; + + function setupDecryptUserKeyWithPinMocks( + pinLockType: PinLockType, + migrationStatus: "PRE" | "POST" = "POST", + ) { + vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType); + + stateService.getKdfConfig.mockResolvedValue(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS)); + stateService.getEmail.mockResolvedValue(mockUserEmail); + + if (migrationStatus === "PRE") { + cryptoService.decryptAndMigrateOldPinKey.mockResolvedValue(mockUserKey); + } else { + cryptoService.decryptUserKeyWithPin.mockResolvedValue(mockUserKey); + } + + mockPinEncryptedKeyDataByPinLockType(pinLockType, migrationStatus); + + stateService.getProtectedPin.mockResolvedValue(mockProtectedPin); + cryptoService.decryptToUtf8.mockResolvedValue(mockPin); + } + + // Note: both pinKeyEncryptedUserKeys use encryptionType: 2 (AesCbc256_HmacSha256_B64) + const pinKeyEncryptedUserKeyEphemeral = new EncString( + "2.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=|DeUFkhIwgkGdZA08bDnDqMMNmZk21D+H5g8IostPKAY=", + ); + + const pinKeyEncryptedUserKeyPersistant = new EncString( + "2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=", + ); + + const oldPinKeyEncryptedMasterKeyPostMigration: any = null; + const oldPinKeyEncryptedMasterKeyPreMigrationPersistent = + "2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw="; + const oldPinKeyEncryptedMasterKeyPreMigrationEphemeral = new EncString( + "2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=", + ); + + function mockPinEncryptedKeyDataByPinLockType( + pinLockType: PinLockType, + migrationStatus: "PRE" | "POST" = "POST", + ) { + switch (pinLockType) { + case "PERSISTANT": + stateService.getPinKeyEncryptedUserKey.mockResolvedValue( + pinKeyEncryptedUserKeyPersistant, + ); + if (migrationStatus === "PRE") { + stateService.getEncryptedPinProtected.mockResolvedValue( + oldPinKeyEncryptedMasterKeyPreMigrationPersistent, + ); + } else { + stateService.getEncryptedPinProtected.mockResolvedValue( + oldPinKeyEncryptedMasterKeyPostMigration, + ); + } + break; + case "TRANSIENT": + stateService.getPinKeyEncryptedUserKeyEphemeral.mockResolvedValue( + pinKeyEncryptedUserKeyEphemeral, + ); + + if (migrationStatus === "PRE") { + stateService.getDecryptedPinProtected.mockResolvedValue( + oldPinKeyEncryptedMasterKeyPreMigrationEphemeral, + ); + } else { + stateService.getDecryptedPinProtected.mockResolvedValue( + oldPinKeyEncryptedMasterKeyPostMigration, + ); + } + break; + case "DISABLED": + // no mocking required. Error should be thrown + break; + } + } + + const testCases: { pinLockType: PinLockType; migrationStatus: "PRE" | "POST" }[] = [ + { pinLockType: "PERSISTANT", migrationStatus: "PRE" }, + { pinLockType: "PERSISTANT", migrationStatus: "POST" }, + { pinLockType: "TRANSIENT", migrationStatus: "PRE" }, + { pinLockType: "TRANSIENT", migrationStatus: "POST" }, + ]; + + testCases.forEach(({ pinLockType, migrationStatus }) => { + describe(`given a ${pinLockType} PIN (${migrationStatus} migration)`, () => { + it(`should successfully decrypt and return user key when using a valid PIN`, async () => { + // Arrange + setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus); + + // Act + const result = await pinCryptoService.decryptUserKeyWithPin(mockPin); + + // Assert + expect(result).toEqual(mockUserKey); + }); + + it(`should return null when PIN is incorrect and user key cannot be decrypted`, async () => { + // Arrange + setupDecryptUserKeyWithPinMocks("PERSISTANT"); + + cryptoService.decryptUserKeyWithPin.mockResolvedValue(null); + + // Act + const result = await pinCryptoService.decryptUserKeyWithPin(mockPin); + + // Assert + expect(result).toBeNull(); + }); + + // not sure if this is a realistic scenario but going to test it anyway + it(`should return null when PIN doesn't match after successful user key decryption`, async () => { + // Arrange + setupDecryptUserKeyWithPinMocks("PERSISTANT"); + + // non matching PIN + cryptoService.decryptToUtf8.mockResolvedValue("9999"); + + // Act + const result = await pinCryptoService.decryptUserKeyWithPin(mockPin); + + // Assert + expect(result).toBeNull(); + }); + }); + }); + + it(`should return null when pin is disabled`, async () => { + // Arrange + setupDecryptUserKeyWithPinMocks("DISABLED"); + + // Act + const result = await pinCryptoService.decryptUserKeyWithPin(mockPin); + + // Assert + expect(result).toBeNull(); + }); + }); +}); + +// Test helpers +function randomBytes(length: number): Uint8Array { + return new Uint8Array(Array.from({ length }, (_, k) => k % 255)); +} diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts deleted file mode 100644 index ddaaa97873..0000000000 --- a/libs/auth/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./components/fingerprint-dialog.component"; -export * from "./password-callout/password-callout.component"; -export * from "./models"; diff --git a/libs/common/src/auth/enums/verification-type.ts b/libs/common/src/auth/enums/verification-type.ts index 76a51ab7b5..c1991162f9 100644 --- a/libs/common/src/auth/enums/verification-type.ts +++ b/libs/common/src/auth/enums/verification-type.ts @@ -1,4 +1,6 @@ export enum VerificationType { MasterPassword = 0, OTP = 1, + PIN = 2, + Biometrics = 3, } diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 2d55a8402d..70fea196f0 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -1,12 +1,24 @@ +import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; +import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; +import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; +import { UserKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "../../enums/verification-type"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; -import { Verification } from "../../types/verification"; +import { + MasterPasswordVerification, + OtpVerification, + PinVerification, + ServerSideVerification, + Verification, + VerificationWithSecret, + verificationHasSecret, +} from "../../types/verification"; /** * Used for general-purpose user verification throughout the app. @@ -18,6 +30,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private cryptoService: CryptoService, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, + private pinCryptoService: PinCryptoServiceAbstraction, + private logService: LogService, ) {} /** @@ -27,11 +41,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti * @param alreadyHashed Whether the master password is already hashed */ async buildRequest( - verification: Verification, + verification: ServerSideVerification, requestClass?: new () => T, alreadyHashed?: boolean, ) { - this.validateInput(verification); + this.validateSecretInput(verification); const request = requestClass != null ? new requestClass() : (new SecretVerificationRequest() as T); @@ -57,42 +71,87 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } /** - * Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data) + * Used to verify Master Password, PIN, or biometrics client-side, or send the OTP to the server for verification (with no other data) * Generally used for client-side verification only. - * @param verification User-supplied verification data (Master Password or OTP) + * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) */ async verifyUser(verification: Verification): Promise { - this.validateInput(verification); + if (verificationHasSecret(verification)) { + this.validateSecretInput(verification); + } - if (verification.type === VerificationType.OTP) { - const request = new VerifyOTPRequest(verification.secret); - try { - await this.userVerificationApiService.postAccountVerifyOTP(request); - } catch (e) { - throw new Error(this.i18nService.t("invalidVerificationCode")); + switch (verification.type) { + case VerificationType.OTP: + return this.verifyUserByOTP(verification); + case VerificationType.MasterPassword: + return this.verifyUserByMasterPassword(verification); + case VerificationType.PIN: + return this.verifyUserByPIN(verification); + break; + case VerificationType.Biometrics: + return this.verifyUserByBiometrics(); + default: { + // Compile-time check for exhaustive switch + const _exhaustiveCheck: never = verification; + return _exhaustiveCheck; } - } else { - let masterKey = await this.cryptoService.getMasterKey(); - if (!masterKey) { - masterKey = await this.cryptoService.makeMasterKey( - verification.secret, - await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), - ); - } - const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( - verification.secret, - masterKey, - ); - if (!passwordValid) { - throw new Error(this.i18nService.t("invalidMasterPassword")); - } - this.cryptoService.setMasterKey(masterKey); + } + } + + private async verifyUserByOTP(verification: OtpVerification): Promise { + const request = new VerifyOTPRequest(verification.secret); + try { + await this.userVerificationApiService.postAccountVerifyOTP(request); + } catch (e) { + throw new Error(this.i18nService.t("invalidVerificationCode")); } return true; } + private async verifyUserByMasterPassword( + verification: MasterPasswordVerification, + ): Promise { + let masterKey = await this.cryptoService.getMasterKey(); + if (!masterKey) { + masterKey = await this.cryptoService.makeMasterKey( + verification.secret, + await this.stateService.getEmail(), + await this.stateService.getKdfType(), + await this.stateService.getKdfConfig(), + ); + } + const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( + verification.secret, + masterKey, + ); + if (!passwordValid) { + throw new Error(this.i18nService.t("invalidMasterPassword")); + } + // TODO: we should re-evaluate later on if user verification should have the side effect of modifying state. Probably not. + await this.cryptoService.setMasterKey(masterKey); + return true; + } + + private async verifyUserByPIN(verification: PinVerification): Promise { + const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret); + + return userKey != null; + } + + private async verifyUserByBiometrics(): Promise { + let userKey: UserKey; + // Biometrics crashes and doesn't return a value if the user cancels the prompt + try { + userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric); + } catch (e) { + this.logService.error(`Biometrics User Verification failed: ${e.message}`); + // So, any failures should be treated as a failed verification + return false; + } + + return userKey != null; + } + async requestOTP() { await this.userVerificationApiService.postAccountRequestOTP(); } @@ -121,12 +180,15 @@ export class UserVerificationService implements UserVerificationServiceAbstracti ); } - private validateInput(verification: Verification) { + private validateSecretInput(verification: VerificationWithSecret) { if (verification?.secret == null || verification.secret === "") { - if (verification.type === VerificationType.OTP) { - throw new Error(this.i18nService.t("verificationCodeRequired")); - } else { - throw new Error(this.i18nService.t("masterPasswordRequired")); + switch (verification.type) { + case VerificationType.OTP: + throw new Error(this.i18nService.t("verificationCodeRequired")); + case VerificationType.MasterPassword: + throw new Error(this.i18nService.t("masterPasswordRequired")); + case VerificationType.PIN: + throw new Error(this.i18nService.t("pinRequired")); } } } diff --git a/libs/common/src/auth/types/verification.ts b/libs/common/src/auth/types/verification.ts index 5a3ec9a6c7..8bb0813be7 100644 --- a/libs/common/src/auth/types/verification.ts +++ b/libs/common/src/auth/types/verification.ts @@ -1,6 +1,19 @@ import { VerificationType } from "../enums/verification-type"; -export type Verification = { - type: VerificationType; - secret: string; -}; +export type OtpVerification = { type: VerificationType.OTP; secret: string }; +export type MasterPasswordVerification = { type: VerificationType.MasterPassword; secret: string }; +export type PinVerification = { type: VerificationType.PIN; secret: string }; +export type BiometricsVerification = { type: VerificationType.Biometrics }; + +export type VerificationWithSecret = OtpVerification | MasterPasswordVerification | PinVerification; +export type VerificationWithoutSecret = BiometricsVerification; + +export type Verification = VerificationWithSecret | VerificationWithoutSecret; + +export function verificationHasSecret( + verification: Verification, +): verification is VerificationWithSecret { + return "secret" in verification; +} + +export type ServerSideVerification = OtpVerification | MasterPasswordVerification; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 4c7adb943d..46d3778348 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -4,10 +4,10 @@ import { firstValueFrom } from "rxjs"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; -import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { AccountDecryptionOptions } from "../../platform/models/domain/account"; import { EncString } from "../../platform/models/domain/enc-string"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; @@ -17,7 +17,6 @@ describe("VaultTimeoutSettingsService", () => { let tokenService: MockProxy; let policyService: MockProxy; let stateService: MockProxy; - let userVerificationService: MockProxy; let service: VaultTimeoutSettingsService; beforeEach(() => { @@ -25,13 +24,11 @@ describe("VaultTimeoutSettingsService", () => { tokenService = mock(); policyService = mock(); stateService = mock(); - userVerificationService = mock(); service = new VaultTimeoutSettingsService( cryptoService, tokenService, policyService, stateService, - userVerificationService, ); }); @@ -43,7 +40,9 @@ describe("VaultTimeoutSettingsService", () => { }); it("contains Lock when the user has a master password", async () => { - userVerificationService.hasMasterPassword.mockResolvedValue(true); + stateService.getAccountDecryptionOptions.mockResolvedValue( + new AccountDecryptionOptions({ hasMasterPassword: true }), + ); const result = await firstValueFrom(service.availableVaultTimeoutActions$()); @@ -75,7 +74,9 @@ describe("VaultTimeoutSettingsService", () => { }); it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => { - userVerificationService.hasMasterPassword.mockResolvedValue(false); + stateService.getAccountDecryptionOptions.mockResolvedValue( + new AccountDecryptionOptions({ hasMasterPassword: false }), + ); stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null); stateService.getProtectedPin.mockResolvedValue(null); stateService.getBiometricUnlock.mockResolvedValue(false); @@ -97,7 +98,9 @@ describe("VaultTimeoutSettingsService", () => { `( "returns $expected when policy is $policy, and user preference is $userPreference", async ({ policy, userPreference, expected }) => { - userVerificationService.hasMasterPassword.mockResolvedValue(true); + stateService.getAccountDecryptionOptions.mockResolvedValue( + new AccountDecryptionOptions({ hasMasterPassword: true }), + ); policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true); policyService.getAll.mockResolvedValue( policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[]), @@ -125,7 +128,9 @@ describe("VaultTimeoutSettingsService", () => { "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference", async ({ unlockMethod, policy, userPreference, expected }) => { stateService.getBiometricUnlock.mockResolvedValue(unlockMethod); - userVerificationService.hasMasterPassword.mockResolvedValue(false); + stateService.getAccountDecryptionOptions.mockResolvedValue( + new AccountDecryptionOptions({ hasMasterPassword: false }), + ); policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true); policyService.getAll.mockResolvedValue( policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[]), diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index e1b6fc2364..6bb7c73f6a 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -4,7 +4,6 @@ import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; import { TokenService } from "../../auth/abstractions/token.service"; -import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { StateService } from "../../platform/abstractions/state.service"; @@ -22,7 +21,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA private tokenService: TokenService, private policyService: PolicyService, private stateService: StateService, - private userVerificationService: UserVerificationService, ) {} async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise { @@ -134,7 +132,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA if (vaultTimeoutAction == null) { // Depends on whether or not the user has a master password - const defaultValue = (await this.userVerificationService.hasMasterPassword()) + const defaultValue = (await this.userHasMasterPassword(userId)) ? VaultTimeoutAction.Lock : VaultTimeoutAction.LogOut; // We really shouldn't need to set the value here, but multiple services relies on this value being correct. @@ -151,7 +149,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA const availableActions = [VaultTimeoutAction.LogOut]; const canLock = - (await this.userVerificationService.hasMasterPassword(userId)) || + (await this.userHasMasterPassword(userId)) || (await this.isPinLockSet(userId)) !== "DISABLED" || (await this.isBiometricLockSet(userId)); @@ -166,4 +164,14 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.cryptoService.clearPinKeys(userId); } + + private async userHasMasterPassword(userId: string): Promise { + const acctDecryptionOpts = await this.stateService.getAccountDecryptionOptions({ + userId: userId, + }); + + if (acctDecryptionOpts?.hasMasterPassword != undefined) { + return acctDecryptionOpts.hasMasterPassword; + } + } } diff --git a/libs/shared/tsconfig.libs.json b/libs/shared/tsconfig.libs.json index 59558c37cc..e90cf58c2d 100644 --- a/libs/shared/tsconfig.libs.json +++ b/libs/shared/tsconfig.libs.json @@ -4,7 +4,8 @@ "paths": { "@bitwarden/admin-console": ["../admin-console/src"], "@bitwarden/angular/*": ["../angular/src/*"], - "@bitwarden/auth": ["../auth/src"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/auth/angular": ["../auth/src/angular"], "@bitwarden/billing": ["../billing/src"], "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], diff --git a/tsconfig.json b/tsconfig.json index b04fa3fb0e..15f282a8c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "paths": { "@bitwarden/admin-console": ["./libs/admin-console/src"], "@bitwarden/angular/*": ["./libs/angular/src/*"], - "@bitwarden/auth": ["./libs/auth/src"], + "@bitwarden/auth/common": ["./libs/auth/src/common"], + "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"],