[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:
parent
c2812fc21d
commit
a42de41587
|
@ -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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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$,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
export abstract class PinCryptoServiceAbstraction {
|
|
||||||
decryptUserKeyWithPin: (pin: string) => Promise<UserKey | null>;
|
|
||||||
}
|
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue