[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"; } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import {
encryptServiceFactory,
EncryptServiceInitOptions,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
FactoryOptions, FactoryOptions,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import {
keyGenerationServiceFactory,
KeyGenerationServiceInitOptions,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import { import {
stateProviderFactory, stateProviderFactory,
StateProviderInitOptions, StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory"; } from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
type MasterPasswordServiceFactoryOptions = FactoryOptions; type MasterPasswordServiceFactoryOptions = FactoryOptions;
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions & export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
StateProviderInitOptions; StateProviderInitOptions &
StateServiceInitOptions &
KeyGenerationServiceInitOptions &
EncryptServiceInitOptions;
export function internalMasterPasswordServiceFactory( export function internalMasterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
cache, cache,
"masterPasswordService", "masterPasswordService",
opts, 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, internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions, MasterPasswordServiceInitOptions,
} from "./master-password-service.factory"; } from "./master-password-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
import { import {
userDecryptionOptionsServiceFactory, userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions, UserDecryptionOptionsServiceInitOptions,
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
I18nServiceInitOptions & I18nServiceInitOptions &
UserVerificationApiServiceInitOptions & UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions & UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions & PinServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions & VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions & PlatformUtilsServiceInitOptions &
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts), await pinServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ export class UnlockCommand {
if (passwordValid) { if (passwordValid) {
await this.masterPasswordService.setMasterKey(masterKey, userId); 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); await this.cryptoService.setUserKey(userKey);
if (await this.keyConnectorService.getConvertAccountRequired()) { if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

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

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; 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 { 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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; 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 { 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 { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
private accountService: AccountService,
private policyService: PolicyService, private policyService: PolicyService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
private desktopSettingsService: DesktopSettingsService, private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private logService: LogService, private logService: LogService,
private nativeMessagingManifestService: NativeMessagingManifestService, private nativeMessagingManifestService: NativeMessagingManifestService,
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
}), }),
); );
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Load initial values // Load initial values
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); this.userHasPinSet = await this.pinService.isPinSet(userId);
this.userHasPinSet = pinStatus !== "DISABLED";
const initialValues = { const initialValues = {
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), 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 { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { Account } from "../../models/account"; import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
provide: SystemServiceAbstraction, provide: SystemServiceAbstraction,
useClass: SystemService, useClass: SystemService,
deps: [ deps: [
PinServiceAbstraction,
MessagingServiceAbstraction, MessagingServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
RELOAD_CALLBACK, RELOAD_CALLBACK,
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService, useClass: ElectronCryptoService,
deps: [ deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,

View File

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

View File

@ -6,7 +6,7 @@ import { of } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -155,8 +155,8 @@ describe("LockComponent", () => {
useValue: mock<UserVerificationService>(), useValue: mock<UserVerificationService>(),
}, },
{ {
provide: PinCryptoServiceAbstraction, provide: PinServiceAbstraction,
useValue: mock<PinCryptoServiceAbstraction>(), useValue: mock<PinServiceAbstraction>(),
}, },
{ {
provide: BiometricStateService, provide: BiometricStateService,

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; 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 { export class ElectronCryptoService extends CryptoService {
constructor( constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
@ -35,6 +37,7 @@ export class ElectronCryptoService extends CryptoService {
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) { ) {
super( super(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -174,7 +177,10 @@ export class ElectronCryptoService extends CryptoService {
if (!encUserKey) { if (!encUserKey) {
throw new Error("No user key found during biometric migration"); 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 // migrate
await this.storeBiometricKey(userKey, userId); await this.storeBiometricKey(userKey, userId);
await this.stateService.setCryptoMasterKeyBiometric(null, { 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 { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -50,6 +51,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private keyRotationService: UserKeyRotationService, private keyRotationService: UserKeyRotationService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -61,6 +63,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }
@ -171,7 +174,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
await this.kdfConfigService.getKdfConfig(), await this.kdfConfigService.getKdfConfig(),
); );
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
if (userKey == null) { if (userKey == null) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",

View File

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

View File

@ -5,6 +5,7 @@ import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitward
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -34,6 +35,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
dialogService: DialogService, dialogService: DialogService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
router, router,
@ -49,6 +51,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
logService, logService,
dialogService, dialogService,
kdfConfigService, 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 { 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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 { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -45,6 +46,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
protected stateService: StateService, protected stateService: StateService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected kdfConfigService: KdfConfigService, protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {} ) {}
async ngOnInit() { async ngOnInit() {

View File

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

View File

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

View File

@ -1,63 +1,66 @@
import { DialogRef } from "@angular/cdk/dialog"; import { DialogRef } from "@angular/cdk/dialog";
import { Directive, OnInit } from "@angular/core"; import { Directive, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; 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"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@Directive() @Directive()
export class SetPinComponent implements OnInit { export class SetPinComponent implements OnInit {
showMasterPassOnRestart = true; showMasterPasswordOnClientRestartOption = true;
setPinForm = this.formBuilder.group({ setPinForm = this.formBuilder.group({
pin: ["", [Validators.required]], pin: ["", [Validators.required]],
masterPassOnRestart: true, requireMasterPasswordOnClientRestart: true,
}); });
constructor( constructor(
private dialogRef: DialogRef, private accountService: AccountService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private userVerificationService: UserVerificationService, private dialogRef: DialogRef,
private stateService: StateService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private kdfConfigService: KdfConfigService, private pinService: PinServiceAbstraction,
private userVerificationService: UserVerificationService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.setPinForm.controls.masterPassOnRestart.setValue(hasMasterPassword); this.setPinForm.controls.requireMasterPasswordOnClientRestart.setValue(hasMasterPassword);
this.showMasterPassOnRestart = hasMasterPassword; this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
} }
submit = async () => { submit = async () => {
const pin = this.setPinForm.get("pin").value; 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)) { if (Utils.isNullOrWhitespace(pin)) {
this.dialogRef.close(false); this.dialogRef.close(false);
return; return;
} }
const pinKey = await this.cryptoService.makePinKey( const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
pin,
await this.stateService.getEmail(),
await this.kdfConfigService.getKdfConfig(),
);
const userKey = await this.cryptoService.getUserKey(); 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) { const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey); pin,
} else { userKey,
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey); userId,
} );
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
requireMasterPasswordOnClientRestart,
userId,
);
this.dialogRef.close(true); 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 { 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
@ -46,6 +47,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
private logService: LogService, private logService: LogService,
dialogService: DialogService, dialogService: DialogService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -57,6 +59,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }

View File

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

View File

@ -4,8 +4,8 @@ import { Subject } from "rxjs";
import { import {
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
AuthRequestService, AuthRequestService,
PinCryptoServiceAbstraction, PinServiceAbstraction,
PinCryptoService, PinService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
LoginStrategyService, LoginStrategyService,
LoginEmailServiceAbstraction, LoginEmailServiceAbstraction,
@ -537,6 +537,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: CryptoService, useClass: CryptoService,
deps: [ deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
@ -639,6 +640,8 @@ const safeProviders: SafeProvider[] = [
provide: VaultTimeoutSettingsServiceAbstraction, provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService, useClass: VaultTimeoutSettingsService,
deps: [ deps: [
AccountServiceAbstraction,
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
TokenServiceAbstraction, TokenServiceAbstraction,
@ -706,6 +709,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction, I18nServiceAbstraction,
CollectionServiceAbstraction, CollectionServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
PinServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({
@ -714,6 +718,7 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
FolderServiceAbstraction, FolderServiceAbstraction,
CipherServiceAbstraction, CipherServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
KdfConfigServiceAbstraction, KdfConfigServiceAbstraction,
@ -725,6 +730,7 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
CipherServiceAbstraction, CipherServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
CollectionServiceAbstraction, CollectionServiceAbstraction,
@ -800,7 +806,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: InternalMasterPasswordServiceAbstraction, provide: InternalMasterPasswordServiceAbstraction,
useClass: MasterPasswordService, useClass: MasterPasswordService,
deps: [StateProvider], deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
}), }),
safeProvider({ safeProvider({
provide: MasterPasswordServiceAbstraction, provide: MasterPasswordServiceAbstraction,
@ -833,7 +839,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction, I18nServiceAbstraction,
UserVerificationApiServiceAbstraction, UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction, PinServiceAbstraction,
LogService, LogService,
VaultTimeoutSettingsServiceAbstraction, VaultTimeoutSettingsServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
@ -983,14 +989,18 @@ const safeProviders: SafeProvider[] = [
], ],
}), }),
safeProvider({ safeProvider({
provide: PinCryptoServiceAbstraction, provide: PinServiceAbstraction,
useClass: PinCryptoService, useClass: PinService,
deps: [ deps: [
StateServiceAbstraction, AccountServiceAbstraction,
CryptoServiceAbstraction, CryptoFunctionServiceAbstraction,
VaultTimeoutSettingsServiceAbstraction, EncryptService,
LogService,
KdfConfigServiceAbstraction, KdfConfigServiceAbstraction,
KeyGenerationServiceAbstraction,
LogService,
MasterPasswordServiceAbstraction,
StateProvider,
StateServiceAbstraction,
], ],
}), }),
safeProvider({ 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-email.service";
export * from "./login-strategy.service"; export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction"; 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; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
await authRequestLoginStrategy.logIn(credentials); await authRequestLoginStrategy.logIn(credentials);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -178,7 +178,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
); );
// Decrypt and set user key in state // 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) // Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; 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-email/login-email.service";
export * from "./login-strategies/login-strategy.service"; export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.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 { Observable } from "rxjs";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
export abstract class VaultTimeoutSettingsService { export abstract class VaultTimeoutSettingsService {
/** /**
@ -38,13 +37,6 @@ export abstract class VaultTimeoutSettingsService {
*/ */
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>; 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. * Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used * @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 { EncString } from "../../platform/models/domain/enc-string";
import { UserId } from "../../types/guid"; 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"; import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
export abstract class MasterPasswordServiceAbstraction { export abstract class MasterPasswordServiceAbstraction {
@ -30,6 +30,20 @@ export abstract class MasterPasswordServiceAbstraction {
* @throws If the user ID is missing. * @throws If the user ID is missing.
*/ */
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>; 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 { 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 { EncString } from "../../../platform/models/domain/enc-string";
import { UserId } from "../../../types/guid"; 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 { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; 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> { setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
return this.mock.setForceSetPasswordReason(reason, userId); 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 { 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 { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { import {
@ -9,7 +13,7 @@ import {
UserKeyDefinition, UserKeyDefinition,
} from "../../../platform/state"; } from "../../../platform/state";
import { UserId } from "../../../types/guid"; 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 { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; 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 { 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> { masterKey$(userId: UserId): Observable<MasterKey> {
if (userId == null) { if (userId == null) {
@ -137,4 +146,48 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
} }
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); 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 { 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 { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
@ -44,7 +44,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private i18nService: I18nService, private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction, private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinCryptoService: PinCryptoServiceAbstraction, private pinService: PinServiceAbstraction,
private logService: LogService, private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -55,10 +55,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
verificationType: keyof UserVerificationOptions, verificationType: keyof UserVerificationOptions,
): Promise<UserVerificationOptions> { ): Promise<UserVerificationOptions> {
if (verificationType === "client") { if (verificationType === "client") {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] = const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
await Promise.all([ await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(), this.hasMasterPasswordAndMasterKeyHash(),
this.vaultTimeoutSettingsService.isPinLockSet(), this.pinService.getPinLockType(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(), this.vaultTimeoutSettingsService.isBiometricLockSet(),
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric), 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) * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
*/ */
async verifyUser(verification: Verification): Promise<boolean> { async verifyUser(verification: Verification): Promise<boolean> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationHasSecret(verification)) { if (verificationHasSecret(verification)) {
this.validateSecretInput(verification); this.validateSecretInput(verification);
} }
@ -145,9 +148,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
case VerificationType.OTP: case VerificationType.OTP:
return this.verifyUserByOTP(verification); return this.verifyUserByOTP(verification);
case VerificationType.MasterPassword: case VerificationType.MasterPassword:
return this.verifyUserByMasterPassword(verification); return this.verifyUserByMasterPassword(verification, userId);
case VerificationType.PIN: case VerificationType.PIN:
return this.verifyUserByPIN(verification); return this.verifyUserByPIN(verification, userId);
case VerificationType.Biometrics: case VerificationType.Biometrics:
return this.verifyUserByBiometrics(); return this.verifyUserByBiometrics();
default: { default: {
@ -170,8 +173,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private async verifyUserByMasterPassword( private async verifyUserByMasterPassword(
verification: MasterPasswordVerification, verification: MasterPasswordVerification,
userId: UserId,
): Promise<boolean> { ): 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)); let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (!masterKey) { if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey( masterKey = await this.cryptoService.makeMasterKey(
@ -192,8 +199,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
return true; return true;
} }
private async verifyUserByPIN(verification: PinVerification): Promise<boolean> { private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret); 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; return userKey != null;
} }

View File

@ -16,6 +16,7 @@ export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServ
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey; return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
} }
// TODO: use keyGenerationService.stretchKey
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> { private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64); const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256"); 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 { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid"; 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 { KeySuffixOptions, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
@ -139,18 +139,6 @@ export abstract class CryptoService {
masterKey: MasterKey, masterKey: MasterKey,
userKey?: UserKey, userKey?: UserKey,
): Promise<[UserKey, EncString]>; ): 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 * Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending * 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. * @throws If the provided key is a null-ish value.
*/ */
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>; 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 * Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result, * Note: This will remove the stored pin and as a result,
@ -282,39 +263,6 @@ export abstract class CryptoService {
* @param userId The desired user * @param userId The desired user
*/ */
abstract clearPinKeys(userId?: string): Promise<void>; 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 * @param keyMaterial The key material to derive the send key from
* @returns A new send key * @returns A new send key
@ -358,16 +306,6 @@ export abstract class CryptoService {
publicKey: string; publicKey: string;
privateKey: EncString; 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. * 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 * 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, salt: string | Uint8Array,
kdfConfig: KdfConfig, kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>; ): 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 { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { Account } from "../models/domain/account"; import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options"; 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 * Sets the user's biometric key
*/ */
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>; 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 * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
*/ */
@ -101,14 +80,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[], value: GeneratedPasswordHistory[],
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => 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>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>; getEmail: (options?: StorageOptions) => Promise<string>;
@ -127,14 +98,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[], value: GeneratedPasswordHistory[],
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => 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>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getLastSync: (options?: StorageOptions) => Promise<string>; getLastSync: (options?: StorageOptions) => Promise<string>;
setLastSync: (value: string, options?: StorageOptions) => Promise<void>; setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
@ -154,14 +117,6 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>; ) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>; getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>; 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>; getUserId: (options?: StorageOptions) => Promise<string>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>; getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;

View File

@ -1,24 +1,9 @@
import { AccountSettings, EncryptionPair } from "./account"; import { AccountSettings } from "./account";
import { EncString } from "./enc-string";
describe("AccountSettings", () => { describe("AccountSettings", () => {
describe("fromJSON", () => { describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => { it("should deserialize to an instance of itself", () => {
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings); 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 { KdfType } from "../../enums";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
import { EncryptedString, EncString } from "./enc-string";
import { SymmetricCryptoKey } from "./symmetric-crypto-key"; import { SymmetricCryptoKey } from "./symmetric-crypto-key";
export class EncryptionPair<TEncrypted, TDecrypted> { export class EncryptionPair<TEncrypted, TDecrypted> {
@ -148,26 +147,15 @@ export class AccountSettings {
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions;
generatorOptions?: GeneratorOptions; generatorOptions?: GeneratorOptions;
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string = "lock"; vaultTimeoutAction?: string = "lock";
/** @deprecated July 2023, left for migration purposes*/
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings { static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) { if (obj == null) {
return null; return null;
} }
return Object.assign(new AccountSettings(), obj, { return Object.assign(new AccountSettings(), obj);
pinProtected: EncryptionPair.fromJSON<string, EncString>(
obj?.pinProtected,
EncString.fromJSON,
),
});
} }
} }

View File

@ -1,6 +1,7 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { firstValueFrom, of, tap } from "rxjs"; import { firstValueFrom, of, tap } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider"; 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 { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { CsprngArray } from "../../types/csprng"; import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid"; 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 { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service"; import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service"; import { KeyGenerationService } from "../abstractions/key-generation.service";
@ -32,6 +33,7 @@ import {
describe("cryptoService", () => { describe("cryptoService", () => {
let cryptoService: CryptoService; let cryptoService: CryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>(); const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>(); const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
@ -51,6 +53,7 @@ describe("cryptoService", () => {
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
cryptoService = new CryptoService( cryptoService = new CryptoService(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -251,60 +254,50 @@ describe("cryptoService", () => {
}); });
describe("Pin Key refresh", () => { describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance; const mockPinKeyEncryptedUserKey = new EncString(
const protectedPin = "2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg="; );
let encPin: EncString; 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 () => { it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin); pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue( pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
new EncString( pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A=", mockPinKeyEncryptedUserKey,
),
); );
await cryptoService.setUserKey(mockUserKey, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), { expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
userId: mockUserId, mockPinKeyEncryptedUserKey,
}); false,
}); 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,
},
); );
}); });
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => { it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null); pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, { expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
userId: mockUserId, mockPinKeyEncryptedUserKey,
}); true,
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, { mockUserId,
userId: 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 * as bigInt from "big-integer";
import { Observable, filter, firstValueFrom, map } from "rxjs"; 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 { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
@ -17,7 +18,6 @@ import {
UserKey, UserKey,
MasterKey, MasterKey,
ProviderKey, ProviderKey,
PinKey,
CipherKey, CipherKey,
UserPrivateKey, UserPrivateKey,
UserPublicKey, UserPublicKey,
@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
readonly everHadUserKey$: Observable<boolean>; readonly everHadUserKey$: Observable<boolean>;
constructor( constructor(
protected pinService: PinServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected keyGenerationService: KeyGenerationService, protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService, protected cryptoFunctionService: CryptoFunctionService,
@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Pin) { 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. // 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 // 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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.buildProtectedSymmetricKey(masterKey, userKey.key); 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 // TODO: move to MasterPasswordService
async hashMasterKey( async hashMasterKey(
password: string, password: string,
@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); 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> { async clearPinKeys(userId?: UserId): Promise<void> {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId }); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId }); 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); 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> { async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial( return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial, keyMaterial,
@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
* @param userId The desired user * @param userId The desired user
*/ */
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) { 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); const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) { if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId }); 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); const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) { 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 // We can't always clear deprecated keys because the pin is only
// migrated once used to unlock // migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} else { } else {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(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 });
} }
} }
@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
break; break;
} }
case KeySuffixOptions.Pin: { case KeySuffixOptions.Pin: {
const protectedPin = await this.stateService.getProtectedPin({ userId: userId }); const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
shouldStoreKey = !!protectedPin; shouldStoreKey = !!userKeyEncryptedPin;
break; break;
} }
} }
@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> { protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(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);
} }
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) { private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
): Promise<[T, EncString]> { ): Promise<[T, EncString]> {
let protectedSymKey: EncString = null; let protectedSymKey: EncString = null;
if (encryptionKey.key.byteLength === 32) { 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); protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
} else if (encryptionKey.key.byteLength === 64) { } else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey); protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Auto) { if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) { } else if (keySuffix === KeySuffixOptions.Pin) {
await this.stateService.setEncryptedPinProtected(null, { userId: userId }); await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
await this.stateService.setDecryptedPinProtected(null, { userId: 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-- // --DEPRECATED METHODS--
/** /**

View File

@ -81,4 +81,15 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
} }
return new SymmetricCryptoKey(key); 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 { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { Account, AccountData, AccountSettings } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state"; import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state"; import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
@ -220,45 +219,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options); 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 * @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> { async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) { 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> { async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null && (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> { async getUserId(options?: StorageOptions): Promise<string> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

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

View File

@ -35,31 +35,33 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
// Auth // 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 KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "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", { export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", { export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
web: "disk-local", 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 SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const TOKEN_DISK = new StateDefinition("token", "disk"); export const TOKEN_DISK = new StateDefinition("token", "disk");
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TOKEN_MEMORY = new StateDefinition("token", "memory");
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", { export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
web: "disk-local",
});
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
// Autofill // Autofill

View File

@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of } from "rxjs"; import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
import { import {
PinServiceAbstraction,
FakeUserDecryptionOptions as UserDecryptionOptions, FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } 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 { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy"; import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service"; 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 { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { BiometricStateService } from "../../platform/biometrics/biometric-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"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => { describe("VaultTimeoutSettingsService", () => {
let accountService: FakeAccountService;
let pinService: MockProxy<PinServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>; let tokenService: MockProxy<TokenService>;
@ -28,7 +33,11 @@ describe("VaultTimeoutSettingsService", () => {
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>; let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => { beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
pinService = mock<PinServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>(); tokenService = mock<TokenService>();
@ -45,6 +54,8 @@ describe("VaultTimeoutSettingsService", () => {
); );
service = new VaultTimeoutSettingsService( service = new VaultTimeoutSettingsService(
accountService,
pinService,
userDecryptionOptionsService, userDecryptionOptionsService,
cryptoService, cryptoService,
tokenService, tokenService,
@ -75,16 +86,8 @@ describe("VaultTimeoutSettingsService", () => {
expect(result).toContain(VaultTimeoutAction.Lock); expect(result).toContain(VaultTimeoutAction.Lock);
}); });
it("contains Lock when the user has a persistent PIN configured", async () => { it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => {
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(createEncString()); pinService.isPinSet.mockResolvedValue(true);
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");
const result = await firstValueFrom(service.availableVaultTimeoutActions$()); const result = await firstValueFrom(service.availableVaultTimeoutActions$());
@ -93,6 +96,7 @@ describe("VaultTimeoutSettingsService", () => {
it("contains Lock when the user has biometrics configured", async () => { it("contains Lock when the user has biometrics configured", async () => {
biometricStateService.biometricUnlockEnabled$ = of(true); biometricStateService.biometricUnlockEnabled$ = of(true);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$()); 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 () => { it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false })); userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null); pinService.isPinSet.mockResolvedValue(false);
stateService.getProtectedPin.mockResolvedValue(null);
biometricStateService.biometricUnlockEnabled$ = of(false); biometricStateService.biometricUnlockEnabled$ = of(false);
const result = await firstValueFrom(service.availableVaultTimeoutActions$()); 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", "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
async ({ unlockMethod, policy, userPreference, expected }) => { async ({ unlockMethod, policy, userPreference, expected }) => {
biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); biometricStateService.biometricUnlockEnabled$ = of(unlockMethod);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(unlockMethod);
userDecryptionOptionsSubject.next( userDecryptionOptionsSubject.next(
new UserDecryptionOptions({ hasMasterPassword: false }), 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 { 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 { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums"; import { PolicyType } from "../../admin-console/enums";
import { AccountService } from "../../auth/abstractions/account.service";
import { TokenService } from "../../auth/abstractions/token.service"; import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service"; 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 { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
import { UserId } from "../../types/guid"; 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 { export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor( constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private tokenService: TokenService, private tokenService: TokenService,
@ -64,22 +63,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return defer(() => this.getAvailableVaultTimeoutActions(userId)); 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> { async isBiometricLockSet(userId?: string): Promise<boolean> {
const biometricUnlockPromise = const biometricUnlockPromise =
userId == null userId == null
@ -157,11 +140,13 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
} }
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> { private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const availableActions = [VaultTimeoutAction.LogOut]; const availableActions = [VaultTimeoutAction.LogOut];
const canLock = const canLock =
(await this.userHasMasterPassword(userId)) || (await this.userHasMasterPassword(userId)) ||
(await this.isPinLockSet(userId)) !== "DISABLED" || (await this.pinService.isPinSet(userId as UserId)) ||
(await this.isBiometricLockSet(userId)); (await this.isBiometricLockSet(userId));
if (canLock) { 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 { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; 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 { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 60; export const CURRENT_VERSION = 61;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -126,7 +127,8 @@ export function createMigrationBuilder() {
.with(CipherServiceMigrator, 56, 57) .with(CipherServiceMigrator, 56, 57)
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58)
.with(KdfConfigMigrator, 58, 59) .with(KdfConfigMigrator, 58, 59)
.with(KnownAccountsMigrator, 59, CURRENT_VERSION); .with(KnownAccountsMigrator, 59, 60)
.with(PinStateMigrator, 60, CURRENT_VERSION);
} }
export async function currentVersion( 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 { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KdfType } from "@bitwarden/common/platform/enums"; import { KdfType } from "@bitwarden/common/platform/enums";
@ -19,6 +20,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>; let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
const password = Utils.newGuid(); const password = Utils.newGuid();
const promptForPassword_callback = async () => { const promptForPassword_callback = async () => {
return password; return password;
@ -28,11 +30,13 @@ describe("BitwardenPasswordProtectedImporter", () => {
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
cipherService = mock<CipherService>(); cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
importer = new BitwardenPasswordProtectedImporter( importer = new BitwardenPasswordProtectedImporter(
cryptoService, cryptoService,
i18nService, i18nService,
cipherService, cipherService,
pinService,
promptForPassword_callback, promptForPassword_callback,
); );
}); });

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -25,6 +26,7 @@ describe("ImportService", () => {
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let collectionService: MockProxy<CollectionService>; let collectionService: MockProxy<CollectionService>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let pinService: MockProxy<PinServiceAbstraction>;
beforeEach(() => { beforeEach(() => {
cipherService = mock<CipherService>(); cipherService = mock<CipherService>();
@ -33,6 +35,7 @@ describe("ImportService", () => {
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
collectionService = mock<CollectionService>(); collectionService = mock<CollectionService>();
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
pinService = mock<PinServiceAbstraction>();
importService = new ImportService( importService = new ImportService(
cipherService, cipherService,
@ -41,6 +44,7 @@ describe("ImportService", () => {
i18nService, i18nService,
collectionService, collectionService,
cryptoService, 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 { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request"; import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
@ -100,6 +101,7 @@ export class ImportService implements ImportServiceAbstraction {
private i18nService: I18nService, private i18nService: I18nService,
private collectionService: CollectionService, private collectionService: CollectionService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private pinService: PinServiceAbstraction,
) {} ) {}
getImportOptions(): ImportOption[] { getImportOptions(): ImportOption[] {
@ -203,6 +205,7 @@ export class ImportService implements ImportServiceAbstraction {
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService, this.cipherService,
this.pinService,
promptForPassword_callback, promptForPassword_callback,
); );
case "lastpasscsv": 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; 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"; import { BitwardenCsvExportType, BitwardenPasswordProtectedFileFormat } from "../types";
export class BaseVaultExportService { export class BaseVaultExportService {
constructor( constructor(
protected pinService: PinServiceAbstraction,
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
@ -19,7 +21,7 @@ export class BaseVaultExportService {
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig();
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); 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 encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key);
const encText = await this.cryptoService.encrypt(clearText, key); const encText = await this.cryptoService.encrypt(clearText, key);

View File

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

View File

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

View File

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

View File

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