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"],