[PM-5363] PinService State Providers (#8244)

* move pinKeyEncryptedUserKey

* move pinKeyEncryptedUserKeyEphemeral

* remove comments, move docs

* cleanup

* use UserKeyDefinition

* refactor methods

* add migration

* fix browser dependency

* add tests for migration

* rename to pinService

* move state to PinService

* add PinService dep to CryptoService

* move protectedPin to state provider

* update service deps

* renaming

* move decryptUserKeyWithPin to pinService

* update service injection

* move more methods our of crypto service

* remove CryptoService dep from PinService and update service injection

* remove cryptoService reference

* add method to FakeMasterPasswordService

* fix circular dependency

* fix desktop service injection

* update browser dependencies

* add protectedPin to migrations

* move storePinKey to pinService

* update and clarify documentation

* more jsdoc updates

* update import paths

* refactor isPinLockSet method

* update state definitions

* initialize service before injecting into other services

* initialize service before injecting into other services (bw.ts)

* update clearOn and do additional cleanup

* clarify docs and naming

* assign abstract & private methods, add clarity to decryptAndMigrateOldPinKeyEncryptedMasterKey() method

* derived state (attempt)

* fix typos

* use accountService to get active user email

* use constant userId

* add derived state

* add get and clear for oldPinKeyEncryptedMasterKey

* require userId

* move pinProtected

* add clear methods

* remove pinProtected from account.ts and replace methods

* add methods to create and store pinKeyEncryptedUserKey

* add pinProtected/oldPinKeyEncrypterMasterKey to migration

* update migration tests

* update migration rollback tests

* update to systemService and decryptAndMigrate... method

* remove old test

* increase length of state definition name to meet test requirements

* rename 'TRANSIENT' to 'EPHEMERAL' for consistency

* fix tests for login strategies, vault-export, and fake MP service

* more updates to login-strategy tests

* write new tests for core pinKeyEncrypterUserKey methods and isPinSet

* write new tests for pinProtected and oldPinKeyEncryptedMasterKey methods

* minor test reformatting

* update test for decryptUserKeyWithPin()

* fix bug with oldPinKeyEncryptedMasterKey

* fix tests for vault-timeout-settings.service

* fix bitwarden-password-protected-importer test

* fix login strategy tests and auth-request.service test

* update pinService tests

* fix crypto service tests

* add jsdoc

* fix test file import

* update jsdocs for decryptAndMigrateOldPinKeyEncryptedMasterKey()

* update error messages and jsdocs

* add null checks, move userId retrievals

* update migration tests

* update stateService calls to require userId

* update test for decryptUserKeyWithPin()

* update oldPinKeyEncryptedMasterKey migration tests

* more test updates

* fix factory import

* update tests for isPinSet() and createProtectedPin()

* add test for makePinKey()

* add test for createPinKeyEncryptedUserKey()

* add tests for getPinLockType()

* consolidate userId verification tests

* add tests for storePinKeyEncryptedUserKey()

* fix service dep

* get email based on userId

* use MasterPasswordService instead of internal

* rename protectedPin to userKeyEncryptedPin

* rename to pinKeyEncryptedUserKeyPersistent

* update method params

* fix CryptoService tests

* jsdoc update

* use EncString for userKeyEncryptedPin

* remove comment

* use cryptoFunctionService.compareFast()

* update tests

* cleanup, remove comments

* resolve merge conflict

* fix DI of MasterPasswordService

* more DI fixes
This commit is contained in:
rr-bw 2024-05-08 11:34:47 -07:00 committed by GitHub
parent c2812fc21d
commit a42de41587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 2182 additions and 998 deletions

View File

@ -4,20 +4,35 @@ import {
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import {
encryptServiceFactory,
EncryptServiceInitOptions,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
keyGenerationServiceFactory,
KeyGenerationServiceInitOptions,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
type MasterPasswordServiceFactoryOptions = FactoryOptions;
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
StateProviderInitOptions;
StateProviderInitOptions &
StateServiceInitOptions &
KeyGenerationServiceInitOptions &
EncryptServiceInitOptions;
export function internalMasterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
cache,
"masterPasswordService",
opts,
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)),
async () =>
new MasterPasswordService(
await stateProviderFactory(cache, opts),
await stateServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
),
);
}

View File

@ -1,53 +0,0 @@
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";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
type PinCryptoServiceFactoryOptions = FactoryOptions;
export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions &
StateServiceInitOptions &
CryptoServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
LogServiceInitOptions &
KdfConfigServiceInitOptions;
export function pinCryptoServiceFactory(
cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices,
opts: PinCryptoServiceInitOptions,
): Promise<PinCryptoServiceAbstraction> {
return factory(
cache,
"pinCryptoService",
opts,
async () =>
new PinCryptoService(
await stateServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -0,0 +1,74 @@
import { PinServiceAbstraction, PinService } from "@bitwarden/auth/common";
import {
CryptoFunctionServiceInitOptions,
cryptoFunctionServiceFactory,
} from "../../../platform/background/service-factories/crypto-function-service.factory";
import {
EncryptServiceInitOptions,
encryptServiceFactory,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
KeyGenerationServiceInitOptions,
keyGenerationServiceFactory,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import {
LogServiceInitOptions,
logServiceFactory,
} from "../../../platform/background/service-factories/log-service.factory";
import {
StateProviderInitOptions,
stateProviderFactory,
} from "../../../platform/background/service-factories/state-provider.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
import {
MasterPasswordServiceInitOptions,
masterPasswordServiceFactory,
} from "./master-password-service.factory";
type PinServiceFactoryOptions = FactoryOptions;
export type PinServiceInitOptions = PinServiceFactoryOptions &
AccountServiceInitOptions &
CryptoFunctionServiceInitOptions &
EncryptServiceInitOptions &
KdfConfigServiceInitOptions &
KeyGenerationServiceInitOptions &
LogServiceInitOptions &
MasterPasswordServiceInitOptions &
StateProviderInitOptions &
StateServiceInitOptions;
export function pinServiceFactory(
cache: { pinService?: PinServiceAbstraction } & CachedServices,
opts: PinServiceInitOptions,
): Promise<PinServiceAbstraction> {
return factory(
cache,
"pinService",
opts,
async () =>
new PinService(
await accountServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await masterPasswordServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await stateServiceFactory(cache, opts),
),
);
}

View File

@ -37,7 +37,7 @@ import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "./master-password-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
I18nServiceInitOptions &
UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions &
PinServiceInitOptions &
LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions &
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
<label
class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>

View File

@ -3,7 +3,7 @@ import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } 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";
@ -63,7 +63,7 @@ export class LockComponent extends BaseLockComponent {
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction,
pinService: PinServiceAbstraction,
private routerService: BrowserRouterService,
biometricStateService: BiometricStateService,
accountService: AccountService,
@ -89,7 +89,7 @@ export class LockComponent extends BaseLockComponent {
dialogService,
deviceTrustService,
userVerificationService,
pinCryptoService,
pinService,
biometricStateService,
accountService,
authService,

View File

@ -16,6 +16,7 @@ import {
} from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
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";
@ -71,6 +72,7 @@ export class AccountSecurityComponent implements OnInit {
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
@ -131,7 +133,6 @@ export class AccountSecurityComponent implements OnInit {
if (timeout === -2 && !showOnLocked) {
timeout = -1;
}
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.form.controls.vaultTimeout.valueChanges
.pipe(
@ -153,12 +154,14 @@ export class AccountSecurityComponent implements OnInit {
)
.subscribe();
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
),
pin: pinStatus !== "DISABLED",
pin: await this.pinService.isPinSet(userId),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$,

View File

@ -1,8 +1,8 @@
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
import {
PinCryptoServiceAbstraction,
PinCryptoService,
PinServiceAbstraction,
PinService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
AuthRequestServiceAbstraction,
@ -318,7 +318,7 @@ export default class MainBackground {
authRequestService: AuthRequestServiceAbstraction;
accountService: AccountServiceAbstraction;
globalStateProvider: GlobalStateProvider;
pinCryptoService: PinCryptoServiceAbstraction;
pinService: PinServiceAbstraction;
singleUserStateProvider: SingleUserStateProvider;
activeUserStateProvider: ActiveUserStateProvider;
derivedStateProvider: DerivedStateProvider;
@ -542,13 +542,31 @@ export default class MainBackground {
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new BrowserCryptoService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
this.cryptoFunctionService,
@ -693,6 +711,8 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
@ -701,14 +721,6 @@ export default class MainBackground {
this.biometricStateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
@ -717,7 +729,7 @@ export default class MainBackground {
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.pinService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
@ -839,11 +851,13 @@ export default class MainBackground {
this.i18nService,
this.collectionService,
this.cryptoService,
this.pinService,
);
this.individualVaultExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.kdfConfigService,
@ -852,6 +866,7 @@ export default class MainBackground {
this.organizationVaultExportService = new OrganizationVaultExportService(
this.cipherService,
this.apiService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.collectionService,
@ -902,6 +917,7 @@ export default class MainBackground {
};
this.systemService = new SystemService(
this.pinService,
this.messagingService,
this.platformUtilsService,
systemUtilsServiceReloadCallback,

View File

@ -356,7 +356,7 @@ export class NativeMessagingBackground {
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.keyB64),
) as MasterKey;
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
encUserKey,
);

View File

@ -5,6 +5,14 @@ import {
policyServiceFactory,
PolicyServiceInitOptions,
} from "../../admin-console/background/service-factories/policy-service.factory";
import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../auth/background/service-factories/account-service.factory";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../auth/background/service-factories/pin-service.factory";
import {
tokenServiceFactory,
TokenServiceInitOptions,
@ -34,6 +42,8 @@ import {
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
AccountServiceInitOptions &
PinServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
CryptoServiceInitOptions &
TokenServiceInitOptions &
@ -51,6 +61,8 @@ export function vaultTimeoutSettingsServiceFactory(
opts,
async () =>
new VaultTimeoutSettingsService(
await accountServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts),

View File

@ -12,6 +12,10 @@ import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "../../../auth/background/service-factories/master-password-service.factory";
import {
PinServiceInitOptions,
pinServiceFactory,
} from "../../../auth/background/service-factories/pin-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@ -45,6 +49,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
type CryptoServiceFactoryOptions = FactoryOptions;
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
PinServiceInitOptions &
MasterPasswordServiceInitOptions &
KeyGenerationServiceInitOptions &
CryptoFunctionServiceInitOptions &
@ -67,6 +72,7 @@ export function cryptoServiceFactory(
opts,
async () =>
new BrowserCryptoService(
await pinServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts),

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@ -19,6 +20,7 @@ import { UserKey } from "@bitwarden/common/types/key";
export class BrowserCryptoService extends CryptoService {
constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
@ -32,6 +34,7 @@ export class BrowserCryptoService extends CryptoService {
kdfConfigService: KdfConfigService,
) {
super(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,

View File

@ -16,7 +16,7 @@ import {
CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -209,6 +209,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CryptoService,
useFactory: (
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
@ -222,6 +223,7 @@ const safeProviders: SafeProvider[] = [
kdfConfigService: KdfConfigService,
) => {
const cryptoService = new BrowserCryptoService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
@ -238,6 +240,7 @@ const safeProviders: SafeProvider[] = [
return cryptoService;
},
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyGenerationService,
CryptoFunctionService,

View File

@ -1,5 +1,9 @@
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../../auth/background/service-factories/pin-service.factory";
import {
cryptoServiceFactory,
CryptoServiceInitOptions,
@ -36,7 +40,8 @@ export type ImportServiceInitOptions = ImportServiceFactoryOptions &
ImportApiServiceInitOptions &
I18nServiceInitOptions &
CollectionServiceInitOptions &
CryptoServiceInitOptions;
CryptoServiceInitOptions &
PinServiceInitOptions;
export function importServiceFactory(
cache: {
@ -56,6 +61,7 @@ export function importServiceFactory(
await i18nServiceFactory(cache, opts),
await collectionServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
),
);
}

View File

@ -86,7 +86,7 @@ export class UnlockCommand {
if (passwordValid) {
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

@ -10,8 +10,8 @@ import {
AuthRequestService,
LoginStrategyService,
LoginStrategyServiceAbstraction,
PinCryptoService,
PinCryptoServiceAbstraction,
PinService,
PinServiceAbstraction,
UserDecryptionOptionsService,
} from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
@ -208,7 +208,7 @@ export class Main {
cipherFileUploadService: CipherFileUploadService;
keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
pinCryptoService: PinCryptoServiceAbstraction;
pinService: PinServiceAbstraction;
stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
@ -360,11 +360,29 @@ export class Main {
migrationRunner,
);
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new CryptoService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
this.cryptoFunctionService,
@ -575,6 +593,8 @@ export class Main {
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
@ -583,14 +603,6 @@ export class Main {
this.biometricStateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
@ -599,7 +611,7 @@ export class Main {
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.pinService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
@ -662,11 +674,13 @@ export class Main {
this.i18nService,
this.collectionService,
this.cryptoService,
this.pinService,
);
this.individualExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.kdfConfigService,
@ -675,6 +689,7 @@ export class Main {
this.organizationExportService = new OrganizationVaultExportService(
this.cipherService,
this.apiService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.collectionService,

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@ -19,7 +20,7 @@ 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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private i18nService: I18nService,
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private logService: LogService,
private nativeMessagingManifestService: NativeMessagingManifestService,
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
}),
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Load initial values
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.userHasPinSet = pinStatus !== "DISABLED";
this.userHasPinSet = await this.pinService.isPinSet(userId);
const initialValues = {
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),

View File

@ -59,6 +59,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components";
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
provide: SystemServiceAbstraction,
useClass: SystemService,
deps: [
PinServiceAbstraction,
MessagingServiceAbstraction,
PlatformUtilsServiceAbstraction,
RELOAD_CALLBACK,
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
<label
class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>

View File

@ -6,7 +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 { PinServiceAbstraction } 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";
@ -155,8 +155,8 @@ describe("LockComponent", () => {
useValue: mock<UserVerificationService>(),
},
{
provide: PinCryptoServiceAbstraction,
useValue: mock<PinCryptoServiceAbstraction>(),
provide: PinServiceAbstraction,
useValue: mock<PinServiceAbstraction>(),
},
{
provide: BiometricStateService,

View File

@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } 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";
@ -62,7 +62,7 @@ export class LockComponent extends BaseLockComponent {
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction,
pinService: PinServiceAbstraction,
biometricStateService: BiometricStateService,
accountService: AccountService,
authService: AuthService,
@ -88,7 +88,7 @@ export class LockComponent extends BaseLockComponent {
dialogService,
deviceTrustService,
userVerificationService,
pinCryptoService,
pinService,
biometricStateService,
accountService,
authService,

View File

@ -1,6 +1,7 @@
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { mock } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -26,6 +27,7 @@ import { ElectronCryptoService } from "./electron-crypto.service";
describe("electronCryptoService", () => {
let sut: ElectronCryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
@ -46,6 +48,7 @@ describe("electronCryptoService", () => {
stateProvider = new FakeStateProvider(accountService);
sut = new ElectronCryptoService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@ -22,6 +23,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
export class ElectronCryptoService extends CryptoService {
constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
@ -35,6 +37,7 @@ export class ElectronCryptoService extends CryptoService {
kdfConfigService: KdfConfigService,
) {
super(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
@ -174,7 +177,10 @@ export class ElectronCryptoService extends CryptoService {
if (!encUserKey) {
throw new Error("No user key found during biometric migration");
}
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
encUserKey,
);
// migrate
await this.storeBiometricKey(userKey, userId);
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });

View File

@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -50,6 +51,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
private userVerificationService: UserVerificationService,
private keyRotationService: UserKeyRotationService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {
super(
i18nService,
@ -61,6 +63,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
stateService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}
@ -171,7 +174,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
await this.kdfConfigService.getKdfConfig(),
);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
if (userKey == null) {
this.platformUtilsService.showToast(
"error",

View File

@ -6,6 +6,7 @@ import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
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";
@ -60,6 +61,7 @@ export class EmergencyAccessTakeoverComponent
dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {
super(
i18nService,
@ -71,6 +73,7 @@ export class EmergencyAccessTakeoverComponent
stateService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}

View File

@ -5,6 +5,7 @@ import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitward
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -34,6 +35,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
userVerificationService: UserVerificationService,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {
super(
router,
@ -49,6 +51,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
logService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}
}

View File

@ -4,6 +4,7 @@ import { Subject, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -45,6 +46,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
protected stateService: StateService,
protected dialogService: DialogService,
protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {}
async ngOnInit() {

View File

@ -3,7 +3,7 @@ import { Router } from "@angular/router";
import { firstValueFrom, Subject } from "rxjs";
import { concatMap, map, take, takeUntil } from "rxjs/operators";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction, PinLockType } 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";
@ -30,7 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
@ -55,7 +54,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected onSuccessfulSubmit: () => Promise<void>;
private invalidPinAttempts = 0;
private pinStatus: PinLockType;
private pinLockType: PinLockType;
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
@ -81,7 +80,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
protected deviceTrustService: DeviceTrustServiceAbstraction,
protected userVerificationService: UserVerificationService,
protected pinCryptoService: PinCryptoServiceAbstraction,
protected pinService: PinServiceAbstraction,
protected biometricStateService: BiometricStateService,
protected accountService: AccountService,
protected authService: AuthService,
@ -168,7 +167,8 @@ export class LockComponent implements OnInit, OnDestroy {
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
try {
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(this.pin);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
if (userKey) {
await this.setUserKeyAndContinue(userKey);
@ -272,7 +272,7 @@ export class LockComponent implements OnInit, OnDestroy {
return;
}
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.setUserKeyAndContinue(userKey, true);
}
@ -358,12 +358,13 @@ export class LockComponent implements OnInit, OnDestroy {
return await this.vaultTimeoutService.logOut(userId);
}
this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.pinLockType = await this.pinService.getPinLockType(userId);
const ephemeralPinSet = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
let ephemeralPinSet = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
ephemeralPinSet ||= await this.stateService.getDecryptedPinProtected();
this.pinEnabled =
(this.pinStatus === "TRANSIENT" && !!ephemeralPinSet) || this.pinStatus === "PERSISTANT";
(this.pinLockType === "EPHEMERAL" && !!ephemeralPinSet) || this.pinLockType === "PERSISTENT";
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();

View File

@ -52,7 +52,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
@ -82,6 +82,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}

View File

@ -1,63 +1,66 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Directive, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@Directive()
export class SetPinComponent implements OnInit {
showMasterPassOnRestart = true;
showMasterPasswordOnClientRestartOption = true;
setPinForm = this.formBuilder.group({
pin: ["", [Validators.required]],
masterPassOnRestart: true,
requireMasterPasswordOnClientRestart: true,
});
constructor(
private dialogRef: DialogRef,
private accountService: AccountService,
private cryptoService: CryptoService,
private userVerificationService: UserVerificationService,
private stateService: StateService,
private dialogRef: DialogRef,
private formBuilder: FormBuilder,
private kdfConfigService: KdfConfigService,
private pinService: PinServiceAbstraction,
private userVerificationService: UserVerificationService,
) {}
async ngOnInit() {
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.setPinForm.controls.masterPassOnRestart.setValue(hasMasterPassword);
this.showMasterPassOnRestart = hasMasterPassword;
this.setPinForm.controls.requireMasterPasswordOnClientRestart.setValue(hasMasterPassword);
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
}
submit = async () => {
const pin = this.setPinForm.get("pin").value;
const masterPassOnRestart = this.setPinForm.get("masterPassOnRestart").value;
const requireMasterPasswordOnClientRestart = this.setPinForm.get(
"requireMasterPasswordOnClientRestart",
).value;
if (Utils.isNullOrWhitespace(pin)) {
this.dialogRef.close(false);
return;
}
const pinKey = await this.cryptoService.makePinKey(
pin,
await this.stateService.getEmail(),
await this.kdfConfigService.getKdfConfig(),
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const userKey = await this.cryptoService.getUserKey();
const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey);
const encPin = await this.cryptoService.encrypt(pin, userKey);
await this.stateService.setProtectedPin(encPin.encryptedString);
const userKeyEncryptedPin = await this.pinService.createUserKeyEncryptedPin(pin, userKey);
await this.pinService.setUserKeyEncryptedPin(userKeyEncryptedPin, userId);
if (masterPassOnRestart) {
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
} else {
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
}
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
userKey,
userId,
);
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
requireMasterPasswordOnClientRestart,
userId,
);
this.dialogRef.close(true);
};

View File

@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
@ -46,6 +47,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
private logService: LogService,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {
super(
i18nService,
@ -57,6 +59,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
stateService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}

View File

@ -62,7 +62,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
dialogService: DialogService,
kdfConfigService: KdfConfigService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {
super(
i18nService,
@ -74,6 +74,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
stateService,
dialogService,
kdfConfigService,
masterPasswordService,
);
}

View File

@ -4,8 +4,8 @@ import { Subject } from "rxjs";
import {
AuthRequestServiceAbstraction,
AuthRequestService,
PinCryptoServiceAbstraction,
PinCryptoService,
PinServiceAbstraction,
PinService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
LoginEmailServiceAbstraction,
@ -537,6 +537,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,
@ -639,6 +640,8 @@ const safeProviders: SafeProvider[] = [
provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService,
deps: [
AccountServiceAbstraction,
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction,
TokenServiceAbstraction,
@ -706,6 +709,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction,
CollectionServiceAbstraction,
CryptoServiceAbstraction,
PinServiceAbstraction,
],
}),
safeProvider({
@ -714,6 +718,7 @@ const safeProviders: SafeProvider[] = [
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
KdfConfigServiceAbstraction,
@ -725,6 +730,7 @@ const safeProviders: SafeProvider[] = [
deps: [
CipherServiceAbstraction,
ApiServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
CollectionServiceAbstraction,
@ -800,7 +806,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: InternalMasterPasswordServiceAbstraction,
useClass: MasterPasswordService,
deps: [StateProvider],
deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
}),
safeProvider({
provide: MasterPasswordServiceAbstraction,
@ -833,7 +839,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction,
UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction,
PinServiceAbstraction,
LogService,
VaultTimeoutSettingsServiceAbstraction,
PlatformUtilsServiceAbstraction,
@ -983,14 +989,18 @@ const safeProviders: SafeProvider[] = [
],
}),
safeProvider({
provide: PinCryptoServiceAbstraction,
useClass: PinCryptoService,
provide: PinServiceAbstraction,
useClass: PinService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
VaultTimeoutSettingsServiceAbstraction,
LogService,
AccountServiceAbstraction,
CryptoFunctionServiceAbstraction,
EncryptService,
KdfConfigServiceAbstraction,
KeyGenerationServiceAbstraction,
LogService,
MasterPasswordServiceAbstraction,
StateProvider,
StateServiceAbstraction,
],
}),
safeProvider({

View File

@ -1,4 +1,4 @@
export * from "./pin-crypto.service.abstraction";
export * from "./pin.service.abstraction";
export * from "./login-email.service";
export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction";

View File

@ -1,5 +0,0 @@
import { UserKey } from "@bitwarden/common/types/key";
export abstract class PinCryptoServiceAbstraction {
decryptUserKeyWithPin: (pin: string) => Promise<UserKey | null>;
}

View File

@ -0,0 +1,129 @@
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { EncString, EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserId } from "@bitwarden/common/types/guid";
import { PinKey, UserKey } from "@bitwarden/common/types/key";
import { PinLockType } from "../services";
/**
* The PinService is used for PIN-based unlocks. Below is a very basic overview of the PIN flow:
*
* -- Setting the PIN via {@link SetPinComponent} --
*
* When the user submits the setPinForm:
* 1. We encrypt the PIN with the UserKey and store it on disk as `userKeyEncryptedPin`.
*
* 2. We create a PinKey from the PIN, and then use that PinKey to encrypt the UserKey, resulting in
* a `pinKeyEncryptedUserKey`, which can be stored in one of two ways depending on what the user selects
* for the `requireMasterPasswordOnClientReset` checkbox.
*
* If `requireMasterPasswordOnClientReset` is:
* - TRUE, store in memory as `pinKeyEncryptedUserKeyEphemeral` (does NOT persist through a client reset)
* - FALSE, store on disk as `pinKeyEncryptedUserKeyPersistent` (persists through a client reset)
*
* -- Unlocking with the PIN via {@link LockComponent} --
*
* When the user enters their PIN, we decrypt their UserKey with the PIN and set that UserKey to state.
*/
export abstract class PinServiceAbstraction {
/**
* Gets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*/
abstract getPinKeyEncryptedUserKeyPersistent: (userId: UserId) => Promise<EncString>;
/**
* Clears the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*/
abstract clearPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<void>;
/**
* Gets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
*/
abstract getPinKeyEncryptedUserKeyEphemeral: (userId: UserId) => Promise<EncString>;
/**
* Clears the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
*/
abstract clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<void>;
/**
* Creates a pinKeyEncryptedUserKey from the provided PIN and UserKey.
*/
abstract createPinKeyEncryptedUserKey: (
pin: string,
userKey: UserKey,
userId: UserId,
) => Promise<EncString>;
/**
* Stores the UserKey, encrypted by the PinKey.
* @param storeEphemeralVersion If true, stores an ephemeral version via the private {@link setPinKeyEncryptedUserKeyEphemeral} method.
* If false, stores a persistent version via the private {@link setPinKeyEncryptedUserKeyPersistent} method.
*/
abstract storePinKeyEncryptedUserKey: (
pinKeyEncryptedUserKey: EncString,
storeEphemeralVersion: boolean,
userId: UserId,
) => Promise<void>;
/**
* Gets the user's PIN, encrypted by the UserKey.
*/
abstract getUserKeyEncryptedPin: (userId: UserId) => Promise<EncString>;
/**
* Sets the user's PIN, encrypted by the UserKey.
*/
abstract setUserKeyEncryptedPin: (
userKeyEncryptedPin: EncString,
userId: UserId,
) => Promise<void>;
/**
* Creates a PIN, encrypted by the UserKey.
*/
abstract createUserKeyEncryptedPin: (pin: string, userKey: UserKey) => Promise<EncString>;
/**
* Clears the user's PIN, encrypted by the UserKey.
*/
abstract clearUserKeyEncryptedPin(userId: UserId): Promise<void>;
/**
* Gets the old MasterKey, encrypted by the PinKey (formerly called `pinProtected`).
* Deprecated and used for migration purposes only.
*/
abstract getOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<EncryptedString>;
/**
* Clears the old MasterKey, encrypted by the PinKey.
*/
abstract clearOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<void>;
/**
* Makes a PinKey from the provided PIN.
*/
abstract makePinKey: (pin: string, salt: string, kdfConfig: KdfConfig) => Promise<PinKey>;
/**
* Gets the user's PinLockType {@link PinLockType}.
*/
abstract getPinLockType: (userId: UserId) => Promise<PinLockType>;
/**
* Declares whether or not the user has a PIN set (either persistent or ephemeral).
*/
abstract isPinSet: (userId: UserId) => Promise<boolean>;
/**
* Decrypts the UserKey with the provided PIN.
*
* @remarks - If the user has an old pinKeyEncryptedMasterKey (formerly called `pinProtected`), the UserKey
* will be obtained via the private {@link decryptAndMigrateOldPinKeyEncryptedMasterKey} method.
* - If the user does not have an old pinKeyEncryptedMasterKey, the UserKey will be obtained via the
* private {@link decryptUserKey} method.
* @returns UserKey
*/
abstract decryptUserKeyWithPin: (pin: string, userId: UserId) => Promise<UserKey | null>;
}

View File

@ -127,7 +127,7 @@ describe("AuthRequestLoginStrategy", () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
await authRequestLoginStrategy.logIn(credentials);

View File

@ -156,7 +156,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}

View File

@ -262,7 +262,7 @@ describe("LoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
const result = await passwordLoginStrategy.logIn(credentials);
@ -281,7 +281,7 @@ describe("LoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLoginStrategy.logIn(credentials);

View File

@ -163,7 +163,7 @@ describe("PasswordLoginStrategy", () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
await passwordLoginStrategy.logIn(credentials);

View File

@ -228,7 +228,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey, userId);
}
}

View File

@ -445,11 +445,15 @@ describe("SsoLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLoginStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});
@ -497,11 +501,15 @@ describe("SsoLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLoginStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});

View File

@ -350,7 +350,7 @@ export class SsoLoginStrategy extends LoginStrategy {
return;
}
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}

View File

@ -190,11 +190,15 @@ describe("UserApiLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
});
});

View File

@ -110,7 +110,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey, userId);
}
}

View File

@ -172,7 +172,9 @@ describe("AuthRequestService", () => {
masterPasswordService.masterKeySubject.next(undefined);
masterPasswordService.masterKeyHashSubject.next(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(
mockDecryptedUserKey,
);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
@ -192,8 +194,10 @@ describe("AuthRequestService", () => {
mockDecryptedMasterKeyHash,
mockUserId,
);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
mockDecryptedMasterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey);
});

View File

@ -178,7 +178,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
);
// Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;

View File

@ -1,4 +1,4 @@
export * from "./pin-crypto/pin-crypto.service.implementation";
export * from "./pin/pin.service.implementation";
export * from "./login-email/login-email.service";
export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.service";

View File

@ -1,104 +0,0 @@
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { UserKey } from "@bitwarden/common/types/key";
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,
private kdfConfigService: KdfConfigService,
) {}
async decryptUserKeyWithPin(pin: string): Promise<UserKey | null> {
try {
const pinLockType: PinLockType = await this.vaultTimeoutSettingsService.isPinLockSet();
const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } =
await this.getPinKeyEncryptedKeys(pinLockType);
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig();
let userKey: UserKey;
const email = await this.stateService.getEmail();
if (oldPinKeyEncryptedMasterKey) {
userKey = await this.cryptoService.decryptAndMigrateOldPinKey(
pinLockType === "TRANSIENT",
pin,
email,
kdfConfig,
oldPinKeyEncryptedMasterKey,
);
} else {
userKey = await this.cryptoService.decryptUserKeyWithPin(
pin,
email,
kdfConfig,
pinKeyEncryptedUserKey,
);
}
if (!userKey) {
this.logService.warning(`User key null after pin key decryption.`);
return null;
}
if (!(await this.validatePin(userKey, pin))) {
this.logService.warning(`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<boolean> {
const protectedPin = await this.stateService.getProtectedPin();
const decryptedPin = await this.cryptoService.decryptToUtf8(
new EncString(protectedPin),
userKey,
);
return decryptedPin === pin;
}
}

View File

@ -1,192 +0,0 @@
import { mock } from "jest-mock-extended";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
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 { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
VaultTimeoutSettingsService,
PinLockType,
} from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { UserKey } from "@bitwarden/common/types/key";
import { PinCryptoService } from "./pin-crypto.service.implementation";
describe("PinCryptoService", () => {
let pinCryptoService: PinCryptoService;
const stateService = mock<StateService>();
const cryptoService = mock<CryptoService>();
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
const logService = mock<LogService>();
const kdfConfigService = mock<KdfConfigService>();
beforeEach(() => {
jest.clearAllMocks();
pinCryptoService = new PinCryptoService(
stateService,
cryptoService,
vaultTimeoutSettingsService,
logService,
kdfConfigService,
);
});
it("instantiates", () => {
expect(pinCryptoService).not.toBeFalsy();
});
describe("decryptUserKeyWithPin(...)", () => {
const mockPin = "1234";
const mockProtectedPin = "protectedPin";
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);
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
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));
}

View File

@ -0,0 +1,501 @@
import { firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncString, EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
PIN_DISK,
PIN_MEMORY,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, PinKey, UserKey } from "@bitwarden/common/types/key";
import { PinServiceAbstraction } from "../../abstractions/pin.service.abstraction";
/**
* - DISABLED : No PIN set.
* - PERSISTENT : PIN is set and persists through client reset.
* - EPHEMERAL : PIN is set, but does NOT persist through client reset. This means that
* after client reset the master password is required to unlock.
*/
export type PinLockType = "DISABLED" | "PERSISTENT" | "EPHEMERAL";
/**
* The persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*
* @remarks Persists through a client reset. Used when `requireMasterPasswordOnClientRestart` is disabled.
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
*/
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT = new UserKeyDefinition<EncryptedString>(
PIN_DISK,
"pinKeyEncryptedUserKeyPersistent",
{
deserializer: (jsonValue) => jsonValue,
clearOn: ["logout"],
},
);
/**
* The ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
*
* @remarks Does NOT persist through a client reset. Used when `requireMasterPasswordOnClientRestart` is enabled.
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
*/
export const PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL = new UserKeyDefinition<EncryptedString>(
PIN_MEMORY,
"pinKeyEncryptedUserKeyEphemeral",
{
deserializer: (jsonValue) => jsonValue,
clearOn: ["logout"],
},
);
/**
* The PIN, encrypted by the UserKey.
*/
export const USER_KEY_ENCRYPTED_PIN = new UserKeyDefinition<EncryptedString>(
PIN_DISK,
"userKeyEncryptedPin",
{
deserializer: (jsonValue) => jsonValue,
clearOn: ["logout"],
},
);
/**
* The old MasterKey, encrypted by the PinKey (formerly called `pinProtected`).
* Deprecated and used for migration purposes only.
*/
export const OLD_PIN_KEY_ENCRYPTED_MASTER_KEY = new UserKeyDefinition<EncryptedString>(
PIN_DISK,
"oldPinKeyEncryptedMasterKey",
{
deserializer: (jsonValue) => jsonValue,
clearOn: ["logout"],
},
);
export class PinService implements PinServiceAbstraction {
constructor(
private accountService: AccountService,
private cryptoFunctionService: CryptoFunctionService,
private encryptService: EncryptService,
private kdfConfigService: KdfConfigService,
private keyGenerationService: KeyGenerationService,
private logService: LogService,
private masterPasswordService: MasterPasswordServiceAbstraction,
private stateProvider: StateProvider,
private stateService: StateService,
) {}
async getPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString> {
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyPersistent.");
return EncString.fromJSON(
await firstValueFrom(
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, userId),
),
);
}
/**
* Sets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*/
private async setPinKeyEncryptedUserKeyPersistent(
pinKeyEncryptedUserKey: EncString,
userId: UserId,
): Promise<void> {
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyPersistent.");
if (pinKeyEncryptedUserKey == null) {
throw new Error(
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyPersistent.",
);
}
await this.stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
pinKeyEncryptedUserKey?.encryptedString,
userId,
);
}
async clearPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<void> {
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyPersistent.");
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
}
async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<EncString> {
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyEphemeral.");
return EncString.fromJSON(
await firstValueFrom(
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, userId),
),
);
}
/**
* Sets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
*/
private async setPinKeyEncryptedUserKeyEphemeral(
pinKeyEncryptedUserKey: EncString,
userId: UserId,
): Promise<void> {
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyEphemeral.");
if (pinKeyEncryptedUserKey == null) {
throw new Error(
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyEphemeral.",
);
}
await this.stateProvider.setUserState(
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
pinKeyEncryptedUserKey?.encryptedString,
userId,
);
}
async clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<void> {
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyEphemeral.");
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, null, userId);
}
async createPinKeyEncryptedUserKey(
pin: string,
userKey: UserKey,
userId: UserId,
): Promise<EncString> {
this.validateUserId(userId, "Cannot create pinKeyEncryptedUserKey.");
if (!userKey) {
throw new Error("No UserKey provided. Cannot create pinKeyEncryptedUserKey.");
}
const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
);
const kdfConfig = await this.kdfConfigService.getKdfConfig();
const pinKey = await this.makePinKey(pin, email, kdfConfig);
return await this.encryptService.encrypt(userKey.key, pinKey);
}
async storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey: EncString,
storeAsEphemeral: boolean,
userId: UserId,
): Promise<void> {
this.validateUserId(userId, "Cannot store pinKeyEncryptedUserKey.");
if (storeAsEphemeral) {
await this.setPinKeyEncryptedUserKeyEphemeral(pinKeyEncryptedUserKey, userId);
} else {
await this.setPinKeyEncryptedUserKeyPersistent(pinKeyEncryptedUserKey, userId);
}
}
async getUserKeyEncryptedPin(userId: UserId): Promise<EncString> {
this.validateUserId(userId, "Cannot get userKeyEncryptedPin.");
return EncString.fromJSON(
await firstValueFrom(this.stateProvider.getUserState$(USER_KEY_ENCRYPTED_PIN, userId)),
);
}
async setUserKeyEncryptedPin(userKeyEncryptedPin: EncString, userId: UserId): Promise<void> {
this.validateUserId(userId, "Cannot set userKeyEncryptedPin.");
await this.stateProvider.setUserState(
USER_KEY_ENCRYPTED_PIN,
userKeyEncryptedPin?.encryptedString,
userId,
);
}
async clearUserKeyEncryptedPin(userId: UserId): Promise<void> {
this.validateUserId(userId, "Cannot clear userKeyEncryptedPin.");
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, userId);
}
async createUserKeyEncryptedPin(pin: string, userKey: UserKey): Promise<EncString> {
if (!userKey) {
throw new Error("No UserKey provided. Cannot create userKeyEncryptedPin.");
}
return await this.encryptService.encrypt(pin, userKey);
}
async getOldPinKeyEncryptedMasterKey(userId: UserId): Promise<EncryptedString> {
this.validateUserId(userId, "Cannot get oldPinKeyEncryptedMasterKey.");
return await firstValueFrom(
this.stateProvider.getUserState$(OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, userId),
);
}
async clearOldPinKeyEncryptedMasterKey(userId: UserId): Promise<void> {
this.validateUserId(userId, "Cannot clear oldPinKeyEncryptedMasterKey.");
await this.stateProvider.setUserState(OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, null, userId);
}
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
}
async getPinLockType(userId: UserId): Promise<PinLockType> {
this.validateUserId(userId, "Cannot get PinLockType.");
/**
* We can't check the `userKeyEncryptedPin` (formerly called `protectedPin`) for both because old
* accounts only used it for MP on Restart
*/
const aUserKeyEncryptedPinIsSet = !!(await this.getUserKeyEncryptedPin(userId));
const aPinKeyEncryptedUserKeyPersistentIsSet =
!!(await this.getPinKeyEncryptedUserKeyPersistent(userId));
const anOldPinKeyEncryptedMasterKeyIsSet =
!!(await this.getOldPinKeyEncryptedMasterKey(userId));
if (aPinKeyEncryptedUserKeyPersistentIsSet || anOldPinKeyEncryptedMasterKeyIsSet) {
return "PERSISTENT";
} else if (
aUserKeyEncryptedPinIsSet &&
!aPinKeyEncryptedUserKeyPersistentIsSet &&
!anOldPinKeyEncryptedMasterKeyIsSet
) {
return "EPHEMERAL";
} else {
return "DISABLED";
}
}
async isPinSet(userId: UserId): Promise<boolean> {
this.validateUserId(userId, "Cannot determine if PIN is set.");
return (await this.getPinLockType(userId)) !== "DISABLED";
}
async decryptUserKeyWithPin(pin: string, userId: UserId): Promise<UserKey | null> {
this.validateUserId(userId, "Cannot decrypt user key with PIN.");
try {
const pinLockType = await this.getPinLockType(userId);
const requireMasterPasswordOnClientRestart = pinLockType === "EPHEMERAL";
const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } =
await this.getPinKeyEncryptedKeys(pinLockType, userId);
const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
);
const kdfConfig = await this.kdfConfigService.getKdfConfig();
let userKey: UserKey;
if (oldPinKeyEncryptedMasterKey) {
userKey = await this.decryptAndMigrateOldPinKeyEncryptedMasterKey(
userId,
pin,
email,
kdfConfig,
requireMasterPasswordOnClientRestart,
oldPinKeyEncryptedMasterKey,
);
} else {
userKey = await this.decryptUserKey(userId, pin, email, kdfConfig, pinKeyEncryptedUserKey);
}
if (!userKey) {
this.logService.warning(`User key null after pin key decryption.`);
return null;
}
if (!(await this.validatePin(userKey, pin, userId))) {
this.logService.warning(`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;
}
}
/**
* Decrypts the UserKey with the provided PIN.
*/
private async decryptUserKey(
userId: UserId,
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinKeyEncryptedUserKey?: EncString,
): Promise<UserKey> {
this.validateUserId(userId, "Cannot decrypt user key.");
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyPersistent(userId);
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyEphemeral(userId);
if (!pinKeyEncryptedUserKey) {
throw new Error("No pinKeyEncryptedUserKey found.");
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.decryptToBytes(pinKeyEncryptedUserKey, pinKey);
return new SymmetricCryptoKey(userKey) as UserKey;
}
/**
* Creates a new `pinKeyEncryptedUserKey` and clears the `oldPinKeyEncryptedMasterKey`.
* @returns UserKey
*/
private async decryptAndMigrateOldPinKeyEncryptedMasterKey(
userId: UserId,
pin: string,
email: string,
kdfConfig: KdfConfig,
requireMasterPasswordOnClientRestart: boolean,
oldPinKeyEncryptedMasterKey: EncString,
): Promise<UserKey> {
this.validateUserId(userId, "Cannot decrypt and migrate oldPinKeyEncryptedMasterKey.");
const masterKey = await this.decryptMasterKeyWithPin(
userId,
pin,
email,
kdfConfig,
oldPinKeyEncryptedMasterKey,
);
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId });
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
new EncString(encUserKey),
);
const pinKeyEncryptedUserKey = await this.createPinKeyEncryptedUserKey(pin, userKey, userId);
await this.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
requireMasterPasswordOnClientRestart,
userId,
);
const userKeyEncryptedPin = await this.createUserKeyEncryptedPin(pin, userKey);
await this.setUserKeyEncryptedPin(userKeyEncryptedPin, userId);
await this.clearOldPinKeyEncryptedMasterKey(userId);
// This also clears the old Biometrics key since the new Biometrics key will be created when the user key is set.
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
return userKey;
}
// Only for migration purposes
private async decryptMasterKeyWithPin(
userId: UserId,
pin: string,
salt: string,
kdfConfig: KdfConfig,
oldPinKeyEncryptedMasterKey?: EncString,
): Promise<MasterKey> {
this.validateUserId(userId, "Cannot decrypt master key with PIN.");
if (!oldPinKeyEncryptedMasterKey) {
const oldPinKeyEncryptedMasterKeyString = await this.getOldPinKeyEncryptedMasterKey(userId);
if (oldPinKeyEncryptedMasterKeyString == null) {
throw new Error("No oldPinKeyEncrytedMasterKey found.");
}
oldPinKeyEncryptedMasterKey = new EncString(oldPinKeyEncryptedMasterKeyString);
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const masterKey = await this.encryptService.decryptToBytes(oldPinKeyEncryptedMasterKey, pinKey);
return new SymmetricCryptoKey(masterKey) as MasterKey;
}
/**
* Gets the user's `pinKeyEncryptedUserKey` (persistent or ephemeral) and `oldPinKeyEncryptedMasterKey`
* (if one exists) based on the user's PinLockType.
*
* @remarks The `oldPinKeyEncryptedMasterKey` (formerly `pinProtected`) is only used for migration and
* will be null for all migrated accounts.
* @throws If PinLockType is 'DISABLED' or if userId is not provided
*/
private async getPinKeyEncryptedKeys(
pinLockType: PinLockType,
userId: UserId,
): Promise<{ pinKeyEncryptedUserKey: EncString; oldPinKeyEncryptedMasterKey?: EncString }> {
this.validateUserId(userId, "Cannot get PinKey encrypted keys.");
switch (pinLockType) {
case "PERSISTENT": {
const pinKeyEncryptedUserKey = await this.getPinKeyEncryptedUserKeyPersistent(userId);
const oldPinKeyEncryptedMasterKey = await this.getOldPinKeyEncryptedMasterKey(userId);
return {
pinKeyEncryptedUserKey,
oldPinKeyEncryptedMasterKey: oldPinKeyEncryptedMasterKey
? new EncString(oldPinKeyEncryptedMasterKey)
: undefined,
};
}
case "EPHEMERAL": {
const pinKeyEncryptedUserKey = await this.getPinKeyEncryptedUserKeyEphemeral(userId);
return {
pinKeyEncryptedUserKey,
oldPinKeyEncryptedMasterKey: undefined, // Going forward, we only migrate non-ephemeral version
};
}
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, userId: UserId): Promise<boolean> {
this.validateUserId(userId, "Cannot validate PIN.");
const userKeyEncryptedPin = await this.getUserKeyEncryptedPin(userId);
const decryptedPin = await this.encryptService.decryptToUtf8(userKeyEncryptedPin, userKey);
const isPinValid = this.cryptoFunctionService.compareFast(decryptedPin, pin);
return isPinValid;
}
/**
* Throws a custom error message if user ID is not provided.
*/
private validateUserId(userId: UserId, errorMessage: string = "") {
if (!userId) {
throw new Error(`User ID is required. ${errorMessage}`);
}
}
}

View File

@ -0,0 +1,597 @@
import { mock } from "jest-mock-extended";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, PinKey, UserKey } from "@bitwarden/common/types/key";
import {
PinService,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
USER_KEY_ENCRYPTED_PIN,
PinLockType,
} from "./pin.service.implementation";
describe("PinService", () => {
let sut: PinService;
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
let stateProvider: FakeStateProvider;
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const kdfConfigService = mock<KdfConfigService>();
const keyGenerationService = mock<KeyGenerationService>();
const logService = mock<LogService>();
const stateService = mock<StateService>();
const mockUserId = Utils.newGuid() as UserId;
const mockUserKey = new SymmetricCryptoKey(randomBytes(64)) as UserKey;
const mockMasterKey = new SymmetricCryptoKey(randomBytes(32)) as MasterKey;
const mockPinKey = new SymmetricCryptoKey(randomBytes(32)) as PinKey;
const mockUserEmail = "user@example.com";
const mockPin = "1234";
const mockUserKeyEncryptedPin = new EncString("userKeyEncryptedPin");
// 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=";
beforeEach(() => {
jest.clearAllMocks();
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
masterPasswordService = new FakeMasterPasswordService();
stateProvider = new FakeStateProvider(accountService);
sut = new PinService(
accountService,
cryptoFunctionService,
encryptService,
kdfConfigService,
keyGenerationService,
logService,
masterPasswordService,
stateProvider,
stateService,
);
});
it("should instantiate the PinService", () => {
expect(sut).not.toBeFalsy();
});
describe("userId validation", () => {
it("should throw an error if a userId is not provided", async () => {
await expect(sut.getPinKeyEncryptedUserKeyPersistent(undefined)).rejects.toThrow(
"User ID is required. Cannot get pinKeyEncryptedUserKeyPersistent.",
);
await expect(sut.getPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow(
"User ID is required. Cannot get pinKeyEncryptedUserKeyEphemeral.",
);
await expect(sut.clearPinKeyEncryptedUserKeyPersistent(undefined)).rejects.toThrow(
"User ID is required. Cannot clear pinKeyEncryptedUserKeyPersistent.",
);
await expect(sut.clearPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow(
"User ID is required. Cannot clear pinKeyEncryptedUserKeyEphemeral.",
);
await expect(
sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, undefined),
).rejects.toThrow("User ID is required. Cannot create pinKeyEncryptedUserKey.");
await expect(sut.getUserKeyEncryptedPin(undefined)).rejects.toThrow(
"User ID is required. Cannot get userKeyEncryptedPin.",
);
await expect(sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, undefined)).rejects.toThrow(
"User ID is required. Cannot set userKeyEncryptedPin.",
);
await expect(sut.clearUserKeyEncryptedPin(undefined)).rejects.toThrow(
"User ID is required. Cannot clear userKeyEncryptedPin.",
);
await expect(sut.getOldPinKeyEncryptedMasterKey(undefined)).rejects.toThrow(
"User ID is required. Cannot get oldPinKeyEncryptedMasterKey.",
);
await expect(sut.clearOldPinKeyEncryptedMasterKey(undefined)).rejects.toThrow(
"User ID is required. Cannot clear oldPinKeyEncryptedMasterKey.",
);
await expect(
sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, undefined),
).rejects.toThrow("User ID is required. Cannot create pinKeyEncryptedUserKey.");
await expect(sut.getPinLockType(undefined)).rejects.toThrow("Cannot get PinLockType.");
await expect(sut.isPinSet(undefined)).rejects.toThrow(
"User ID is required. Cannot determine if PIN is set.",
);
});
});
describe("get/clear/create/store pinKeyEncryptedUserKey methods", () => {
describe("getPinKeyEncryptedUserKeyPersistent()", () => {
it("should get the pinKeyEncryptedUserKey of the specified userId", async () => {
await sut.getPinKeyEncryptedUserKeyPersistent(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
mockUserId,
);
});
});
describe("clearPinKeyEncryptedUserKeyPersistent()", () => {
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
await sut.clearPinKeyEncryptedUserKeyPersistent(mockUserId);
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
null,
mockUserId,
);
});
});
describe("getPinKeyEncryptedUserKeyEphemeral()", () => {
it("should get the pinKeyEncrypterUserKeyEphemeral of the specified userId", async () => {
await sut.getPinKeyEncryptedUserKeyEphemeral(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
mockUserId,
);
});
});
describe("clearPinKeyEncryptedUserKeyEphemeral()", () => {
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
await sut.clearPinKeyEncryptedUserKeyEphemeral(mockUserId);
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
null,
mockUserId,
);
});
});
describe("createPinKeyEncryptedUserKey()", () => {
it("should throw an error if a userKey is not provided", async () => {
await expect(
sut.createPinKeyEncryptedUserKey(mockPin, undefined, mockUserId),
).rejects.toThrow("No UserKey provided. Cannot create pinKeyEncryptedUserKey.");
});
it("should create a pinKeyEncryptedUserKey", async () => {
// Arrange
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
// Act
await sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, mockUserId);
// Assert
expect(encryptService.encrypt).toHaveBeenCalledWith(mockUserKey.key, mockPinKey);
});
});
describe("storePinKeyEncryptedUserKey", () => {
it("should store a pinKeyEncryptedUserKey (persistent version) when 'storeAsEphemeral' is false", async () => {
// Arrange
const storeAsEphemeral = false;
// Act
await sut.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKeyPersistant,
storeAsEphemeral,
mockUserId,
);
// Assert
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
pinKeyEncryptedUserKeyPersistant.encryptedString,
mockUserId,
);
});
it("should store a pinKeyEncryptedUserKeyEphemeral when 'storeAsEphemeral' is true", async () => {
// Arrange
const storeAsEphemeral = true;
// Act
await sut.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKeyEphemeral,
storeAsEphemeral,
mockUserId,
);
// Assert
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
pinKeyEncryptedUserKeyEphemeral.encryptedString,
mockUserId,
);
});
});
});
describe("userKeyEncryptedPin methods", () => {
describe("getUserKeyEncryptedPin()", () => {
it("should get the userKeyEncryptedPin of the specified userId", async () => {
await sut.getUserKeyEncryptedPin(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
USER_KEY_ENCRYPTED_PIN,
mockUserId,
);
});
});
describe("setUserKeyEncryptedPin()", () => {
it("should set the userKeyEncryptedPin of the specified userId", async () => {
await sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, mockUserId);
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
USER_KEY_ENCRYPTED_PIN,
mockUserKeyEncryptedPin.encryptedString,
mockUserId,
);
});
});
describe("clearUserKeyEncryptedPin()", () => {
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
await sut.clearUserKeyEncryptedPin(mockUserId);
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
USER_KEY_ENCRYPTED_PIN,
null,
mockUserId,
);
});
});
describe("createUserKeyEncryptedPin()", () => {
it("should throw an error if a userKey is not provided", async () => {
await expect(sut.createUserKeyEncryptedPin(mockPin, undefined)).rejects.toThrow(
"No UserKey provided. Cannot create userKeyEncryptedPin.",
);
});
it("should create a userKeyEncryptedPin from the provided PIN and userKey", async () => {
encryptService.encrypt.mockResolvedValue(mockUserKeyEncryptedPin);
const result = await sut.createUserKeyEncryptedPin(mockPin, mockUserKey);
expect(encryptService.encrypt).toHaveBeenCalledWith(mockPin, mockUserKey);
expect(result).toEqual(mockUserKeyEncryptedPin);
});
});
});
describe("oldPinKeyEncryptedMasterKey methods", () => {
describe("getOldPinKeyEncryptedMasterKey()", () => {
it("should get the oldPinKeyEncryptedMasterKey of the specified userId", async () => {
await sut.getOldPinKeyEncryptedMasterKey(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
mockUserId,
);
});
});
describe("clearOldPinKeyEncryptedMasterKey()", () => {
it("should clear the oldPinKeyEncryptedMasterKey of the specified userId", async () => {
await sut.clearOldPinKeyEncryptedMasterKey(mockUserId);
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
null,
mockUserId,
);
});
});
});
describe("makePinKey()", () => {
it("should make a PinKey", async () => {
// Arrange
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
// Act
await sut.makePinKey(mockPin, mockUserEmail, DEFAULT_KDF_CONFIG);
// Assert
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
mockPin,
mockUserEmail,
DEFAULT_KDF_CONFIG,
);
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(mockPinKey);
});
});
describe("getPinLockType()", () => {
it("should return 'PERSISTENT' if a pinKeyEncryptedUserKey (persistent version) is found", async () => {
// Arrange
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
sut.getPinKeyEncryptedUserKeyPersistent = jest
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
// Act
const result = await sut.getPinLockType(mockUserId);
// Assert
expect(result).toBe("PERSISTENT");
});
it("should return 'PERSISTENT' if an old oldPinKeyEncryptedMasterKey is found", async () => {
// Arrange
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
sut.getOldPinKeyEncryptedMasterKey = jest
.fn()
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPreMigrationPersistent);
// Act
const result = await sut.getPinLockType(mockUserId);
// Assert
expect(result).toBe("PERSISTENT");
});
it("should return 'EPHEMERAL' if neither a pinKeyEncryptedUserKey (persistent version) nor an old oldPinKeyEncryptedMasterKey are found, but a userKeyEncryptedPin is found", async () => {
// Arrange
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
sut.getOldPinKeyEncryptedMasterKey = jest.fn().mockResolvedValue(null);
// Act
const result = await sut.getPinLockType(mockUserId);
// Assert
expect(result).toBe("EPHEMERAL");
});
it("should return 'DISABLED' if ALL three of these are NOT found: userKeyEncryptedPin, pinKeyEncryptedUserKey (persistent version), oldPinKeyEncryptedMasterKey", async () => {
// Arrange
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
sut.getOldPinKeyEncryptedMasterKey = jest.fn().mockResolvedValue(null);
// Act
const result = await sut.getPinLockType(mockUserId);
// Assert
expect(result).toBe("DISABLED");
});
});
describe("isPinSet()", () => {
it.each(["PERSISTENT", "EPHEMERAL"])(
"should return true if the user PinLockType is '%s'",
async () => {
// Arrange
sut.getPinLockType = jest.fn().mockResolvedValue("PERSISTENT");
// Act
const result = await sut.isPinSet(mockUserId);
// Assert
expect(result).toEqual(true);
},
);
it("should return false if the user PinLockType is 'DISABLED'", async () => {
// Arrange
sut.getPinLockType = jest.fn().mockResolvedValue("DISABLED");
// Act
const result = await sut.isPinSet(mockUserId);
// Assert
expect(result).toEqual(false);
});
});
describe("decryptUserKeyWithPin()", () => {
async function setupDecryptUserKeyWithPinMocks(
pinLockType: PinLockType,
migrationStatus: "PRE" | "POST" = "POST",
) {
sut.getPinLockType = jest.fn().mockResolvedValue(pinLockType);
mockPinEncryptedKeyDataByPinLockType(pinLockType, migrationStatus);
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
if (pinLockType === "PERSISTENT" && migrationStatus === "PRE") {
await mockDecryptAndMigrateOldPinKeyEncryptedMasterKeyFn();
} else {
mockDecryptUserKeyFn();
}
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
encryptService.decryptToUtf8.mockResolvedValue(mockPin);
cryptoFunctionService.compareFast.calledWith(mockPin, "1234").mockResolvedValue(true);
}
async function mockDecryptAndMigrateOldPinKeyEncryptedMasterKeyFn() {
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
encryptService.decryptToBytes.mockResolvedValue(mockMasterKey.key);
stateService.getEncryptedCryptoSymmetricKey.mockResolvedValue(mockUserKey.keyB64);
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey);
sut.createPinKeyEncryptedUserKey = jest
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
await sut.storePinKeyEncryptedUserKey(pinKeyEncryptedUserKeyPersistant, false, mockUserId);
sut.createUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
await sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, mockUserId);
await sut.clearOldPinKeyEncryptedMasterKey(mockUserId);
await stateService.setCryptoMasterKeyBiometric(null, { userId: mockUserId });
}
function mockDecryptUserKeyFn() {
sut.getPinKeyEncryptedUserKeyPersistent = jest
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
encryptService.decryptToBytes.mockResolvedValue(mockUserKey.key);
}
function mockPinEncryptedKeyDataByPinLockType(
pinLockType: PinLockType,
migrationStatus: "PRE" | "POST" = "POST",
) {
switch (pinLockType) {
case "PERSISTENT":
sut.getPinKeyEncryptedUserKeyPersistent = jest
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
if (migrationStatus === "PRE") {
sut.getOldPinKeyEncryptedMasterKey = jest
.fn()
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPreMigrationPersistent);
} else {
sut.getOldPinKeyEncryptedMasterKey = jest
.fn()
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPostMigration); // null
}
break;
case "EPHEMERAL":
sut.getPinKeyEncryptedUserKeyEphemeral = jest
.fn()
.mockResolvedValue(pinKeyEncryptedUserKeyEphemeral);
break;
case "DISABLED":
// no mocking required. Error should be thrown
break;
}
}
const testCases: { pinLockType: PinLockType; migrationStatus: "PRE" | "POST" }[] = [
{ pinLockType: "PERSISTENT", migrationStatus: "PRE" },
{ pinLockType: "PERSISTENT", migrationStatus: "POST" },
{ pinLockType: "EPHEMERAL", migrationStatus: "POST" },
];
testCases.forEach(({ pinLockType, migrationStatus }) => {
describe(`given a ${pinLockType} PIN (${migrationStatus} migration)`, () => {
if (pinLockType === "PERSISTENT" && migrationStatus === "PRE") {
it("should clear the oldPinKeyEncryptedMasterKey from state", async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
// Act
await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
null,
mockUserId,
);
});
it("should set the new pinKeyEncrypterUserKeyPersistent to state", async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
// Act
await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
pinKeyEncryptedUserKeyPersistant.encryptedString,
mockUserId,
);
});
}
it(`should successfully decrypt and return user key when using a valid PIN`, async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(result).toEqual(mockUserKey);
});
it(`should return null when PIN is incorrect and user key cannot be decrypted`, async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
sut.decryptUserKeyWithPin = jest.fn().mockResolvedValue(null);
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// 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
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
encryptService.decryptToUtf8.mockResolvedValue("9999"); // non matching PIN
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(result).toBeNull();
});
});
});
it(`should return null when pin is disabled`, async () => {
// Arrange
await setupDecryptUserKeyWithPinMocks("DISABLED");
// Act
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
// Assert
expect(result).toBeNull();
});
});
});
// Test helpers
function randomBytes(length: number): Uint8Array {
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
}

View File

@ -1,7 +1,6 @@
import { Observable } from "rxjs";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
export abstract class VaultTimeoutSettingsService {
/**
@ -38,13 +37,6 @@ export abstract class VaultTimeoutSettingsService {
*/
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
/**
* Has the user enabled unlock with Pin.
* @param userId The user id to check. If not provided, the current user is used
* @returns PinLockType
*/
isPinLockSet: (userId?: string) => Promise<PinLockType>;
/**
* Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used

View File

@ -2,7 +2,7 @@ import { Observable } from "rxjs";
import { EncString } from "../../platform/models/domain/enc-string";
import { UserId } from "../../types/guid";
import { MasterKey } from "../../types/key";
import { MasterKey, UserKey } from "../../types/key";
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
export abstract class MasterPasswordServiceAbstraction {
@ -30,6 +30,20 @@ export abstract class MasterPasswordServiceAbstraction {
* @throws If the user ID is missing.
*/
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type
* is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64
* @returns The user key
*/
abstract decryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
) => Promise<UserKey>;
}
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {

View File

@ -3,7 +3,7 @@ import { ReplaySubject, Observable } from "rxjs";
import { EncString } from "../../../platform/models/domain/enc-string";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { MasterKey, UserKey } from "../../../types/key";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
@ -61,4 +61,12 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
return this.mock.setForceSetPasswordReason(reason, userId);
}
decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
): Promise<UserKey> {
return this.mock.decryptUserKeyWithMasterKey(masterKey, userKey, userId);
}
}

View File

@ -1,5 +1,9 @@
import { firstValueFrom, map, Observable } from "rxjs";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import {
@ -9,7 +13,7 @@ import {
UserKeyDefinition,
} from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { MasterKey, UserKey } from "../../../types/key";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
@ -46,7 +50,12 @@ const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
);
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
constructor(private stateProvider: StateProvider) {}
constructor(
private stateProvider: StateProvider,
private stateService: StateService,
private keyGenerationService: KeyGenerationService,
private encryptService: EncryptService,
) {}
masterKey$(userId: UserId): Observable<MasterKey> {
if (userId == null) {
@ -137,4 +146,48 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
}
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason);
}
async decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: UserId,
): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
userKey ??= await this.getMasterKeyEncryptedUserKey(userId);
masterKey ??= await firstValueFrom(this.masterKey$(userId));
if (masterKey == null) {
throw new Error("No master key found.");
}
// Try one more way to get the user key if it still wasn't found.
if (userKey == null) {
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
if (deprecatedKey == null) {
throw new Error("No encrypted user key found.");
}
userKey = new EncString(deprecatedKey);
}
let decUserKey: Uint8Array;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.keyGenerationService.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
} else {
throw new Error("Unsupported encryption type.");
}
if (decUserKey == null) {
return null;
}
return new SymmetricCryptoKey(decUserKey) as UserKey;
}
}

View File

@ -2,7 +2,7 @@ import { firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction";
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
@ -44,7 +44,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinCryptoService: PinCryptoServiceAbstraction,
private pinService: PinServiceAbstraction,
private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
@ -55,10 +55,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
verificationType: keyof UserVerificationOptions,
): Promise<UserVerificationOptions> {
if (verificationType === "client") {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(),
this.vaultTimeoutSettingsService.isPinLockSet(),
this.pinService.getPinLockType(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(),
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric),
]);
@ -137,6 +138,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
*/
async verifyUser(verification: Verification): Promise<boolean> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationHasSecret(verification)) {
this.validateSecretInput(verification);
}
@ -145,9 +148,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
case VerificationType.OTP:
return this.verifyUserByOTP(verification);
case VerificationType.MasterPassword:
return this.verifyUserByMasterPassword(verification);
return this.verifyUserByMasterPassword(verification, userId);
case VerificationType.PIN:
return this.verifyUserByPIN(verification);
return this.verifyUserByPIN(verification, userId);
case VerificationType.Biometrics:
return this.verifyUserByBiometrics();
default: {
@ -170,8 +173,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private async verifyUserByMasterPassword(
verification: MasterPasswordVerification,
userId: UserId,
): Promise<boolean> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (!userId) {
throw new Error("User ID is required. Cannot verify user by master password.");
}
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey(
@ -192,8 +199,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
return true;
}
private async verifyUserByPIN(verification: PinVerification): Promise<boolean> {
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret);
private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
if (!userId) {
throw new Error("User ID is required. Cannot verify user by PIN.");
}
const userKey = await this.pinService.decryptUserKeyWithPin(verification.secret, userId);
return userKey != null;
}

View File

@ -16,6 +16,7 @@ export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServ
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
}
// TODO: use keyGenerationService.stretchKey
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256");

View File

@ -5,7 +5,7 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key";
import { KeySuffixOptions, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
@ -139,18 +139,6 @@ export abstract class CryptoService {
masterKey: MasterKey,
userKey?: UserKey,
): Promise<[UserKey, EncString]>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @returns The user key
*/
abstract decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
): Promise<UserKey>;
/**
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
@ -268,13 +256,6 @@ export abstract class CryptoService {
* @throws If the provided key is a null-ish value.
*/
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* @param pin The user's pin
* @param salt The user's salt
* @param kdfConfig The user's kdf config
* @returns A key derived from the user's pin
*/
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
/**
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
@ -282,39 +263,6 @@ export abstract class CryptoService {
* @param userId The desired user
*/
abstract clearPinKeys(userId?: string): Promise<void>;
/**
* Decrypts the user key with their pin
* @param pin The user's PIN
* @param salt The user's salt
* @param kdfConfig The user's KDF config
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
* it will be retrieved from storage
* @returns The decrypted user key
*/
abstract decryptUserKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<UserKey>;
/**
* Creates a new Pin key that encrypts the user key instead of the
* master key. Clears the old Pin key from state.
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
* @param pin User's PIN
* @param email User's email
* @param kdfConfig User's KdfConfig
* @param oldPinKey The old Pin key from state (retrieved from different
* places depending on if Master Password on Restart was enabled)
* @returns The user key
*/
abstract decryptAndMigrateOldPinKey(
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey>;
/**
* @param keyMaterial The key material to derive the send key from
* @returns A new send key
@ -358,16 +306,6 @@ export abstract class CryptoService {
publicKey: string;
privateKey: EncString;
}>;
/**
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
*/
abstract decryptMasterKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<MasterKey>;
/**
* Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state

View File

@ -53,4 +53,11 @@ export abstract class KeyGenerationService {
salt: string | Uint8Array,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>;
/**
* Derives a 64 byte key from a 32 byte key using a key derivation function.
* @param key 32 byte key.
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
}

View File

@ -4,7 +4,6 @@ import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/
import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid";
import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options";
/**
@ -47,26 +46,6 @@ export abstract class StateService<T extends Account = Account> {
* Sets the user's biometric key
*/
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* Gets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
*/
@ -101,14 +80,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions,
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
*/
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
/**
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
@ -127,14 +98,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions,
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
*/
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getLastSync: (options?: StorageOptions) => Promise<string>;
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
@ -154,14 +117,6 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's Pin, encrypted by the user key
*/
getProtectedPin: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's Pin, encrypted by the user key
*/
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;

View File

@ -1,24 +1,9 @@
import { AccountSettings, EncryptionPair } from "./account";
import { EncString } from "./enc-string";
import { AccountSettings } from "./account";
describe("AccountSettings", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
});
it("should deserialize pinProtected", () => {
const accountSettings = new AccountSettings();
accountSettings.pinProtected = EncryptionPair.fromJSON<string, EncString>({
encrypted: "encrypted",
decrypted: "3.data",
});
const jsonObj = JSON.parse(JSON.stringify(accountSettings));
const actual = AccountSettings.fromJSON(jsonObj);
expect(actual.pinProtected).toBeInstanceOf(EncryptionPair);
expect(actual.pinProtected.encrypted).toEqual("encrypted");
expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data");
});
});
});

View File

@ -11,7 +11,6 @@ import { DeepJsonify } from "../../../types/deep-jsonify";
import { KdfType } from "../../enums";
import { Utils } from "../../misc/utils";
import { EncryptedString, EncString } from "./enc-string";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
export class EncryptionPair<TEncrypted, TDecrypted> {
@ -148,26 +147,15 @@ export class AccountSettings {
passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions;
generatorOptions?: GeneratorOptions;
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
/** @deprecated July 2023, left for migration purposes*/
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) {
return null;
}
return Object.assign(new AccountSettings(), obj, {
pinProtected: EncryptionPair.fromJSON<string, EncString>(
obj?.pinProtected,
EncString.fromJSON,
),
});
return Object.assign(new AccountSettings(), obj);
}
}

View File

@ -1,6 +1,7 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, of, tap } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
@ -8,7 +9,7 @@ import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";
import { UserKey, MasterKey, PinKey } from "../../types/key";
import { UserKey, MasterKey } from "../../types/key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
@ -32,6 +33,7 @@ import {
describe("cryptoService", () => {
let cryptoService: CryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
@ -51,6 +53,7 @@ describe("cryptoService", () => {
stateProvider = new FakeStateProvider(accountService);
cryptoService = new CryptoService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
@ -251,60 +254,50 @@ describe("cryptoService", () => {
});
describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance;
const protectedPin =
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
let encPin: EncString;
const mockPinKeyEncryptedUserKey = new EncString(
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
const mockUserKeyEncryptedPin = new EncString(
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
beforeEach(() => {
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey);
encPin = new EncString(
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
encryptService.encrypt.mockResolvedValue(encPin);
});
it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
new EncString(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A=",
),
it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
mockPinKeyEncryptedUserKey,
);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), {
userId: mockUserId,
});
});
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(
expect.any(EncString),
{
userId: mockUserId,
},
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
false,
mockUserId,
);
});
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null);
it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
true,
mockUserId,
);
});
it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => {
pinService.getUserKeyEncryptedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
});
});
});

View File

@ -1,6 +1,7 @@
import * as bigInt from "big-integer";
import { Observable, filter, firstValueFrom, map } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
@ -17,7 +18,6 @@ import {
UserKey,
MasterKey,
ProviderKey,
PinKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
readonly everHadUserKey$: Observable<boolean>;
constructor(
protected pinService: PinServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Pin) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
}
// TODO: move to master password service
async decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: UserId,
): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey == null) {
throw new Error("No master key found.");
}
// Try one more way to get the user key if it still wasn't found.
if (userKey == null) {
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
if (deprecatedKey == null) {
throw new Error("No encrypted user key found.");
}
userKey = new EncString(deprecatedKey);
}
let decUserKey: Uint8Array;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
} else {
throw new Error("Unsupported encryption type.");
}
if (decUserKey == null) {
return null;
}
return new SymmetricCryptoKey(decUserKey) as UserKey;
}
// TODO: move to MasterPasswordService
async hashMasterKey(
password: string,
@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
return (await this.stretchKey(pinKey)) as PinKey;
}
async clearPinKeys(userId?: UserId): Promise<void> {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot clear PIN keys, no user Id resolved.");
}
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
await this.pinService.clearUserKeyEncryptedPin(userId);
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
async decryptUserKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinProtectedUserKey?: EncString,
): Promise<UserKey> {
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKey();
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
if (!pinProtectedUserKey) {
throw new Error("No PIN protected key found.");
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey);
return new SymmetricCryptoKey(userKey) as UserKey;
}
// only for migration purposes
async decryptMasterKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinProtectedMasterKey?: EncString,
): Promise<MasterKey> {
if (!pinProtectedMasterKey) {
const pinProtectedMasterKeyString = await this.stateService.getEncryptedPinProtected();
if (pinProtectedMasterKeyString == null) {
throw new Error("No PIN protected key found.");
}
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey);
return new SymmetricCryptoKey(masterKey) as MasterKey;
}
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial,
@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
* @param userId The desired user
*/
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot store additional keys, no user Id resolved.");
}
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
@ -808,37 +741,31 @@ export class CryptoService implements CryptoServiceAbstraction {
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) {
await this.storePinKey(key, userId);
// Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId),
key,
);
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
key,
userId,
);
const noPreExistingPersistentKey =
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
noPreExistingPersistentKey,
userId,
);
// We can't always clear deprecated keys because the pin is only
// migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} else {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
}
}
/**
* Stores the pin key if needed. If MP on Reset is enabled, stores the
* ephemeral version.
* @param key The user key
*/
protected async storePinKey(key: UserKey, userId?: UserId) {
const pin = await this.encryptService.decryptToUtf8(
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
key,
);
const pinKey = await this.makePinKey(
pin,
await this.stateService.getEmail({ userId: userId }),
await this.kdfConfigService.getKdfConfig(),
);
const encPin = await this.encryptService.encrypt(key.key, pinKey);
if ((await this.stateService.getPinKeyEncryptedUserKey({ userId: userId })) != null) {
await this.stateService.setPinKeyEncryptedUserKey(encPin, { userId: userId });
} else {
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(encPin, { userId: userId });
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
}
@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
break;
}
case KeySuffixOptions.Pin: {
const protectedPin = await this.stateService.getProtectedPin({ userId: userId });
shouldStoreKey = !!protectedPin;
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
shouldStoreKey = !!userKeyEncryptedPin;
break;
}
}
@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
}
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
): Promise<[T, EncString]> {
let protectedSymKey: EncString = null;
if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.stretchKey(encryptionKey);
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
} else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) {
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
}
}
async decryptAndMigrateOldPinKey(
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey> {
// Decrypt
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey);
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
// Migrate
const pinKey = await this.makePinKey(pin, email, kdfConfig);
const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey);
if (masterPasswordOnRestart) {
await this.stateService.setDecryptedPinProtected(null);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
} else {
await this.stateService.setEncryptedPinProtected(null);
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
// We previously only set the protected pin if MP on Restart was enabled
// now we set it regardless
const encPin = await this.encryptService.encrypt(pin, userKey);
await this.stateService.setProtectedPin(encPin.encryptedString);
}
// This also clears the old Biometrics key since the new Biometrics key will
// be created when the user key is set.
await this.stateService.setCryptoMasterKeyBiometric(null);
return userKey;
}
// --DEPRECATED METHODS--
/**

View File

@ -81,4 +81,15 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
}
return new SymmetricCryptoKey(key);
}
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
}
}

View File

@ -19,7 +19,6 @@ import { HtmlStorageLocation, StorageLocation } from "../enums";
import { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils";
import { Account, AccountData, AccountSettings } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storage-options";
@ -220,45 +219,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
}
async getPinKeyEncryptedUserKey(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.settings?.pinKeyEncryptedUserKey,
);
}
async setPinKeyEncryptedUserKey(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.pinKeyEncryptedUserKey = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getPinKeyEncryptedUserKeyEphemeral(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
?.settings?.pinKeyEncryptedUserKeyEphemeral,
);
}
async setPinKeyEncryptedUserKeyEphemeral(
value: EncString,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.settings.pinKeyEncryptedUserKeyEphemeral = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
/**
* @deprecated Use UserKeyAuto instead
*/
@ -369,29 +329,6 @@ export class StateService<
);
}
/**
* @deprecated Use getPinKeyEncryptedUserKeyEphemeral instead
*/
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.settings?.pinProtected?.decrypted;
}
/**
* @deprecated Use setPinKeyEncryptedUserKeyEphemeral instead
*/
async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.settings.pinProtected.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
@ -512,23 +449,6 @@ export class StateService<
);
}
async getEncryptedPinProtected(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.pinProtected?.encrypted;
}
async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.pinProtected.encrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
return (
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
@ -645,23 +565,6 @@ export class StateService<
);
}
async getProtectedPin(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.protectedPin;
}
async setProtectedPin(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.protectedPin = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getUserId(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

@ -1,5 +1,6 @@
import { firstValueFrom, map, timeout } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountService } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
@ -20,6 +21,7 @@ export class SystemService implements SystemServiceAbstraction {
private clearClipboardTimeoutFunction: () => Promise<any> = null;
constructor(
private pinService: PinServiceAbstraction,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null,
@ -50,10 +52,13 @@ export class SystemService implements SystemServiceAbstraction {
return;
}
// User has set a PIN, with ask for master password on restart, to protect their vault
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
if (ephemeralPin != null) {
return;
// If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (userId != null) {
const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
if (ephemeralPin != null) {
return;
}
}
this.cancelProcessReload();

View File

@ -35,31 +35,33 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
// Auth
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
web: "disk-local",
});
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
web: "disk-local",
});
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const ROUTER_DISK = new StateDefinition("router", "disk");
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local",
});
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
web: "disk-local",
});
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
export const PIN_DISK = new StateDefinition("pinUnlock", "disk");
export const PIN_MEMORY = new StateDefinition("pinUnlock", "memory");
export const ROUTER_DISK = new StateDefinition("router", "disk");
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const TOKEN_DISK = new StateDefinition("token", "disk");
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
web: "disk-local",
});
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
web: "disk-local",
});
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
// Autofill

View File

@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
import {
PinServiceAbstraction,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
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";
@ -13,11 +17,12 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => {
let accountService: FakeAccountService;
let pinService: MockProxy<PinServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>;
@ -28,7 +33,11 @@ describe("VaultTimeoutSettingsService", () => {
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
pinService = mock<PinServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>();
@ -45,6 +54,8 @@ describe("VaultTimeoutSettingsService", () => {
);
service = new VaultTimeoutSettingsService(
accountService,
pinService,
userDecryptionOptionsService,
cryptoService,
tokenService,
@ -75,16 +86,8 @@ describe("VaultTimeoutSettingsService", () => {
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a persistent PIN configured", async () => {
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(createEncString());
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a transient/ephemeral PIN configured", async () => {
stateService.getProtectedPin.mockResolvedValue("some-key");
it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => {
pinService.isPinSet.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
@ -93,6 +96,7 @@ describe("VaultTimeoutSettingsService", () => {
it("contains Lock when the user has biometrics configured", async () => {
biometricStateService.biometricUnlockEnabled$ = of(true);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
@ -101,8 +105,7 @@ describe("VaultTimeoutSettingsService", () => {
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
stateService.getProtectedPin.mockResolvedValue(null);
pinService.isPinSet.mockResolvedValue(false);
biometricStateService.biometricUnlockEnabled$ = of(false);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
@ -149,6 +152,8 @@ describe("VaultTimeoutSettingsService", () => {
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
async ({ unlockMethod, policy, userPreference, expected }) => {
biometricStateService.biometricUnlockEnabled$ = of(unlockMethod);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(unlockMethod);
userDecryptionOptionsSubject.next(
new UserDecryptionOptions({ hasMasterPassword: false }),
);
@ -165,7 +170,3 @@ describe("VaultTimeoutSettingsService", () => {
});
});
});
function createEncString() {
return Symbol() as unknown as EncString;
}

View File

@ -1,10 +1,14 @@
import { defer, firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
import { AccountService } from "../../auth/abstractions/account.service";
import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
@ -12,15 +16,10 @@ import { StateService } from "../../platform/abstractions/state.service";
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
import { UserId } from "../../types/guid";
/**
* - DISABLED: No Pin set
* - PERSISTENT: Pin is set and survives client reset
* - TRANSIENT: Pin is set and requires password unlock after client reset
*/
export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT";
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private cryptoService: CryptoService,
private tokenService: TokenService,
@ -64,22 +63,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return defer(() => this.getAvailableVaultTimeoutActions(userId));
}
async isPinLockSet(userId?: string): Promise<PinLockType> {
// we can't check the protected pin for both because old accounts only
// used it for MP on Restart
const pinIsEnabled = !!(await this.stateService.getProtectedPin({ userId }));
const aUserKeyPinIsSet = !!(await this.stateService.getPinKeyEncryptedUserKey({ userId }));
const anOldUserKeyPinIsSet = !!(await this.stateService.getEncryptedPinProtected({ userId }));
if (aUserKeyPinIsSet || anOldUserKeyPinIsSet) {
return "PERSISTANT";
} else if (pinIsEnabled && !aUserKeyPinIsSet && !anOldUserKeyPinIsSet) {
return "TRANSIENT";
} else {
return "DISABLED";
}
}
async isBiometricLockSet(userId?: string): Promise<boolean> {
const biometricUnlockPromise =
userId == null
@ -157,11 +140,13 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
}
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const availableActions = [VaultTimeoutAction.LogOut];
const canLock =
(await this.userHasMasterPassword(userId)) ||
(await this.isPinLockSet(userId)) !== "DISABLED" ||
(await this.pinService.isPinSet(userId as UserId)) ||
(await this.isBiometricLockSet(userId));
if (canLock) {

View File

@ -58,13 +58,14 @@ import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-r
import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 60;
export const CURRENT_VERSION = 61;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@ -126,7 +127,8 @@ export function createMigrationBuilder() {
.with(CipherServiceMigrator, 56, 57)
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58)
.with(KdfConfigMigrator, 58, 59)
.with(KnownAccountsMigrator, 59, CURRENT_VERSION);
.with(KnownAccountsMigrator, 59, 60)
.with(PinStateMigrator, 60, CURRENT_VERSION);
}
export async function currentVersion(

View File

@ -0,0 +1,176 @@
import { MockProxy } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
USER_KEY_ENCRYPTED_PIN,
PinStateMigrator,
} from "./61-move-pin-state-to-providers";
function preMigrationState() {
return {
global: {
otherStuff: "otherStuff1",
},
global_account_accounts: {
// prettier-ignore
"AccountOne": {
email: "account-one@email.com",
name: "Account One",
},
// prettier-ignore
"AccountTwo": {
email: "account-two@email.com",
name: "Account Two",
},
},
// prettier-ignore
"AccountOne": {
settings: {
pinKeyEncryptedUserKey: "AccountOne_pinKeyEncryptedUserKeyPersistent",
protectedPin: "AccountOne_userKeyEncryptedPin", // note the name change
pinProtected: {
encrypted: "AccountOne_oldPinKeyEncryptedMasterKey", // note the name change
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
// prettier-ignore
"AccountTwo": {
settings: {
otherStuff: "otherStuff4",
},
},
};
}
function postMigrationState() {
return {
user_AccountOne_pinUnlock_pinKeyEncryptedUserKeyPersistent:
"AccountOne_pinKeyEncryptedUserKeyPersistent",
user_AccountOne_pinUnlock_userKeyEncryptedPin: "AccountOne_userKeyEncryptedPin",
user_AccountOne_pinUnlock_oldPinKeyEncryptedMasterKey: "AccountOne_oldPinKeyEncryptedMasterKey",
authenticatedAccounts: ["AccountOne", "AccountTwo"],
global: {
otherStuff: "otherStuff1",
},
global_account_accounts: {
// prettier-ignore
"AccountOne": {
email: "account-one@email.com",
name: "Account One",
},
// prettier-ignore
"AccountTwo": {
email: "account-two@email.com",
name: "Account Two",
},
},
// prettier-ignore
"AccountOne": {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
// prettier-ignore
"AccountTwo": {
settings: {
otherStuff: "otherStuff4",
},
},
};
}
describe("PinStateMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: PinStateMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(preMigrationState(), 61);
sut = new PinStateMigrator(60, 61);
});
it("should remove properties (pinKeyEncryptedUserKey, protectedPin, pinProtected) from existing accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("AccountOne", {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).not.toHaveBeenCalledWith("AccountTwo");
});
it("should set the properties (pinKeyEncryptedUserKeyPersistent, userKeyEncryptedPin, oldPinKeyEncryptedMasterKey) under the new key definitions", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith(
"AccountOne",
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
"AccountOne_pinKeyEncryptedUserKeyPersistent",
);
expect(helper.setToUser).toHaveBeenCalledWith(
"AccountOne",
USER_KEY_ENCRYPTED_PIN,
"AccountOne_userKeyEncryptedPin",
);
expect(helper.setToUser).toHaveBeenCalledWith(
"AccountOne",
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
"AccountOne_oldPinKeyEncryptedMasterKey",
);
expect(helper.setToUser).not.toHaveBeenCalledWith("AccountTwo");
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(postMigrationState(), 61);
sut = new PinStateMigrator(60, 61);
});
it("should null out the previously migrated values (pinKeyEncryptedUserKeyPersistent, userKeyEncryptedPin, oldPinKeyEncryptedMasterKey)", async () => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(
"AccountOne",
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
null,
);
expect(helper.setToUser).toHaveBeenCalledWith("AccountOne", USER_KEY_ENCRYPTED_PIN, null);
expect(helper.setToUser).toHaveBeenCalledWith(
"AccountOne",
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
null,
);
});
it("should set back the original account properties (pinKeyEncryptedUserKey, protectedPin, pinProtected)", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(1);
expect(helper.set).toHaveBeenCalledWith("AccountOne", {
settings: {
pinKeyEncryptedUserKey: "AccountOne_pinKeyEncryptedUserKeyPersistent",
protectedPin: "AccountOne_userKeyEncryptedPin",
pinProtected: {
encrypted: "AccountOne_oldPinKeyEncryptedMasterKey",
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
});
});

View File

@ -0,0 +1,129 @@
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedAccountState = {
settings?: {
pinKeyEncryptedUserKey?: string; // EncryptedString
protectedPin?: string; // EncryptedString
pinProtected?: {
encrypted?: string;
};
};
};
export const PIN_STATE: StateDefinitionLike = { name: "pinUnlock" };
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT: KeyDefinitionLike = {
stateDefinition: PIN_STATE,
key: "pinKeyEncryptedUserKeyPersistent",
};
export const USER_KEY_ENCRYPTED_PIN: KeyDefinitionLike = {
stateDefinition: PIN_STATE,
key: "userKeyEncryptedPin",
};
export const OLD_PIN_KEY_ENCRYPTED_MASTER_KEY: KeyDefinitionLike = {
stateDefinition: PIN_STATE,
key: "oldPinKeyEncryptedMasterKey",
};
export class PinStateMigrator extends Migrator<60, 61> {
async migrate(helper: MigrationHelper): Promise<void> {
const legacyAccounts = await helper.getAccounts<ExpectedAccountState>();
let updatedAccount = false;
async function migrateAccount(userId: string, account: ExpectedAccountState) {
// Migrate pinKeyEncryptedUserKey (to `pinKeyEncryptedUserKeyPersistent`)
if (account?.settings?.pinKeyEncryptedUserKey != null) {
await helper.setToUser(
userId,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
account.settings.pinKeyEncryptedUserKey,
);
delete account.settings.pinKeyEncryptedUserKey;
updatedAccount = true;
}
// Migrate protectedPin (to `userKeyEncryptedPin`)
if (account?.settings?.protectedPin != null) {
await helper.setToUser(userId, USER_KEY_ENCRYPTED_PIN, account.settings.protectedPin);
delete account.settings.protectedPin;
updatedAccount = true;
}
// Migrate pinProtected (to `oldPinKeyEncryptedMasterKey`)
if (account?.settings?.pinProtected?.encrypted != null) {
await helper.setToUser(
userId,
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
account.settings.pinProtected.encrypted,
);
delete account.settings.pinProtected;
updatedAccount = true;
}
if (updatedAccount) {
await helper.set(userId, account);
}
}
await Promise.all([
...legacyAccounts.map(({ userId, account }) => migrateAccount(userId, account)),
]);
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountState>();
async function rollbackAccount(userId: string, account: ExpectedAccountState) {
let updatedAccount = false;
const accountPinKeyEncryptedUserKeyPersistent = await helper.getFromUser<string>(
userId,
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
);
const accountUserKeyEncryptedPin = await helper.getFromUser<string>(
userId,
USER_KEY_ENCRYPTED_PIN,
);
const accountOldPinKeyEncryptedMasterKey = await helper.getFromUser<string>(
userId,
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
);
if (!account) {
account = {};
}
if (accountPinKeyEncryptedUserKeyPersistent != null) {
account.settings.pinKeyEncryptedUserKey = accountPinKeyEncryptedUserKeyPersistent;
await helper.setToUser(userId, PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null);
updatedAccount = true;
}
if (accountUserKeyEncryptedPin != null) {
account.settings.protectedPin = accountUserKeyEncryptedPin;
await helper.setToUser(userId, USER_KEY_ENCRYPTED_PIN, null);
updatedAccount = true;
}
if (accountOldPinKeyEncryptedMasterKey != null) {
account.settings = Object.assign(account.settings ?? {}, {
pinProtected: {
encrypted: accountOldPinKeyEncryptedMasterKey,
},
});
await helper.setToUser(userId, OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, null);
updatedAccount = true;
}
if (updatedAccount) {
await helper.set(userId, account);
}
}
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
}
}

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KdfType } from "@bitwarden/common/platform/enums";
@ -19,6 +20,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
let cryptoService: MockProxy<CryptoService>;
let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
const password = Utils.newGuid();
const promptForPassword_callback = async () => {
return password;
@ -28,11 +30,13 @@ describe("BitwardenPasswordProtectedImporter", () => {
cryptoService = mock<CryptoService>();
i18nService = mock<I18nService>();
cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
importer = new BitwardenPasswordProtectedImporter(
cryptoService,
i18nService,
cipherService,
pinService,
promptForPassword_callback,
);
});

View File

@ -1,3 +1,4 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import {
CipherWithIdExport,
CollectionWithIdExport,
@ -28,6 +29,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
protected cryptoService: CryptoService,
protected i18nService: I18nService,
protected cipherService: CipherService,
protected pinService: PinServiceAbstraction,
) {
super();
}

View File

@ -1,3 +1,4 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import {
Argon2KdfConfig,
KdfConfig,
@ -23,9 +24,10 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
cryptoService: CryptoService,
i18nService: I18nService,
cipherService: CipherService,
pinService: PinServiceAbstraction,
private promptForPassword_callback: () => Promise<string>,
) {
super(cryptoService, i18nService, cipherService);
super(cryptoService, i18nService, cipherService, pinService);
}
async parse(data: string): Promise<ImportResult> {
@ -78,7 +80,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
? new PBKDF2KdfConfig(jdoc.kdfIterations)
: new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism);
this.key = await this.cryptoService.makePinKey(password, jdoc.salt, kdfConfig);
this.key = await this.pinService.makePinKey(password, jdoc.salt, kdfConfig);
const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT);

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -25,6 +26,7 @@ describe("ImportService", () => {
let i18nService: MockProxy<I18nService>;
let collectionService: MockProxy<CollectionService>;
let cryptoService: MockProxy<CryptoService>;
let pinService: MockProxy<PinServiceAbstraction>;
beforeEach(() => {
cipherService = mock<CipherService>();
@ -33,6 +35,7 @@ describe("ImportService", () => {
i18nService = mock<I18nService>();
collectionService = mock<CollectionService>();
cryptoService = mock<CryptoService>();
pinService = mock<PinServiceAbstraction>();
importService = new ImportService(
cipherService,
@ -41,6 +44,7 @@ describe("ImportService", () => {
i18nService,
collectionService,
cryptoService,
pinService,
);
});

View File

@ -1,3 +1,4 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
@ -100,6 +101,7 @@ export class ImportService implements ImportServiceAbstraction {
private i18nService: I18nService,
private collectionService: CollectionService,
private cryptoService: CryptoService,
private pinService: PinServiceAbstraction,
) {}
getImportOptions(): ImportOption[] {
@ -203,6 +205,7 @@ export class ImportService implements ImportServiceAbstraction {
this.cryptoService,
this.i18nService,
this.cipherService,
this.pinService,
promptForPassword_callback,
);
case "lastpasscsv":

View File

@ -1,3 +1,4 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -10,6 +11,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { BitwardenCsvExportType, BitwardenPasswordProtectedFileFormat } from "../types";
export class BaseVaultExportService {
constructor(
protected pinService: PinServiceAbstraction,
protected cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private kdfConfigService: KdfConfigService,
@ -19,7 +21,7 @@ export class BaseVaultExportService {
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig();
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
const key = await this.cryptoService.makePinKey(password, salt, kdfConfig);
const key = await this.pinService.makePinKey(password, salt, kdfConfig);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key);
const encText = await this.cryptoService.encrypt(clearText, key);

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -141,6 +142,7 @@ describe("VaultExportService", () => {
let exportService: IndividualVaultExportService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
let folderService: MockProxy<FolderService>;
let cryptoService: MockProxy<CryptoService>;
let kdfConfigService: MockProxy<KdfConfigService>;
@ -148,6 +150,7 @@ describe("VaultExportService", () => {
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
folderService = mock<FolderService>();
cryptoService = mock<CryptoService>();
kdfConfigService = mock<KdfConfigService>();
@ -160,6 +163,7 @@ describe("VaultExportService", () => {
exportService = new IndividualVaultExportService(
folderService,
cipherService,
pinService,
cryptoService,
cryptoFunctionService,
kdfConfigService,

View File

@ -1,5 +1,6 @@
import * as papa from "papaparse";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -30,11 +31,12 @@ export class IndividualVaultExportService
constructor(
private folderService: FolderService,
private cipherService: CipherService,
pinService: PinServiceAbstraction,
cryptoService: CryptoService,
cryptoFunctionService: CryptoFunctionService,
kdfConfigService: KdfConfigService,
) {
super(cryptoService, cryptoFunctionService, kdfConfigService);
super(pinService, cryptoService, cryptoFunctionService, kdfConfigService);
}
async getExport(format: ExportFormat = "csv"): Promise<string> {

View File

@ -1,5 +1,6 @@
import * as papa from "papaparse";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
@ -34,12 +35,13 @@ export class OrganizationVaultExportService
constructor(
private cipherService: CipherService,
private apiService: ApiService,
pinService: PinServiceAbstraction,
cryptoService: CryptoService,
cryptoFunctionService: CryptoFunctionService,
private collectionService: CollectionService,
kdfConfigService: KdfConfigService,
) {
super(cryptoService, cryptoFunctionService, kdfConfigService);
super(pinService, cryptoService, cryptoFunctionService, kdfConfigService);
}
async getPasswordProtectedExport(

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -141,6 +142,7 @@ describe("VaultExportService", () => {
let exportService: IndividualVaultExportService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
let folderService: MockProxy<FolderService>;
let cryptoService: MockProxy<CryptoService>;
let kdfConfigService: MockProxy<KdfConfigService>;
@ -148,6 +150,7 @@ describe("VaultExportService", () => {
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
folderService = mock<FolderService>();
cryptoService = mock<CryptoService>();
kdfConfigService = mock<KdfConfigService>();
@ -160,6 +163,7 @@ describe("VaultExportService", () => {
exportService = new IndividualVaultExportService(
folderService,
cipherService,
pinService,
cryptoService,
cryptoFunctionService,
kdfConfigService,