diff --git a/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts new file mode 100644 index 0000000000..eb5ba3a264 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts @@ -0,0 +1,28 @@ +import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; + +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type KdfConfigServiceFactoryOptions = FactoryOptions; + +export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions; + +export function kdfConfigServiceFactory( + cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices, + opts: KdfConfigServiceInitOptions, +): Promise { + return factory( + cache, + "kdfConfigService", + opts, + async () => new KdfConfigService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index 075ba614b7..c414300431 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -68,6 +68,7 @@ import { deviceTrustServiceFactory, DeviceTrustServiceInitOptions, } from "./device-trust-service.factory"; +import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -106,7 +107,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & - BillingAccountProfileStateServiceInitOptions; + BillingAccountProfileStateServiceInitOptions & + KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -140,6 +142,7 @@ export function loginStrategyServiceFactory( await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts index f5360f48fa..db16245f67 100644 --- a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts @@ -22,13 +22,16 @@ import { 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; + LogServiceInitOptions & + KdfConfigServiceInitOptions; export function pinCryptoServiceFactory( cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices, @@ -44,6 +47,7 @@ export function pinCryptoServiceFactory( await cryptoServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await logServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts index 1d79bbbaf1..5af5eb0017 100644 --- a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts @@ -1,11 +1,13 @@ import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { FactoryOptions, CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory"; import { I18nServiceInitOptions, i18nServiceFactory, @@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions; export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + GlobalStateProvider; export async function twoFactorServiceFactory( cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices, @@ -33,6 +36,7 @@ export async function twoFactorServiceFactory( new TwoFactorService( await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await globalStateProviderFactory(cache, opts), ), ); service.init(); diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index a8b67b21ca..d6f9ce7624 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -32,6 +32,7 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + KdfConfigServiceInitOptions; export function userVerificationServiceFactory( cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, @@ -82,6 +84,7 @@ export function userVerificationServiceFactory( await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 78039d793f..4d47417df6 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent { private routerService: BrowserRouterService, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -90,6 +92,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index bad2e4a9e7..6191d277ad 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { this.navigateTo2FA(); } - choose(p: any) { - super.choose(p); - this.twoFactorService.setSelectedProvider(p.type); + override async choose(p: TwoFactorProviderDetails) { + await super.choose(p); + await this.twoFactorService.setSelectedProvider(p.type); this.navigateTo2FA(); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index dc93de7803..758c226bc3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, PinCryptoService, - LoginStrategyServiceAbstraction, - LoginStrategyService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, AuthRequestServiceAbstraction, @@ -33,11 +31,11 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -48,11 +46,11 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { @@ -275,7 +273,6 @@ export default class MainBackground { containerService: ContainerService; auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; - loginStrategyService: LoginStrategyServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; @@ -299,7 +296,6 @@ export default class MainBackground { providerService: ProviderServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; - twoFactorService: TwoFactorServiceAbstraction; vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; @@ -339,6 +335,7 @@ export default class MainBackground { intraprocessMessagingSubject: Subject>; userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; + kdfConfigService: kdfConfigServiceAbstraction; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -542,6 +539,9 @@ export default class MainBackground { this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new BrowserCryptoService( this.masterPasswordService, this.keyGenerationService, @@ -553,6 +553,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -607,8 +608,6 @@ export default class MainBackground { this.stateService, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -652,31 +651,6 @@ export default class MainBackground { this.loginEmailService = new LoginEmailService(this.stateProvider); - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -725,6 +699,7 @@ export default class MainBackground { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -739,6 +714,7 @@ export default class MainBackground { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultFilterService = new VaultFilterService( @@ -861,7 +837,7 @@ export default class MainBackground { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -869,8 +845,8 @@ export default class MainBackground { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( @@ -1104,8 +1080,7 @@ export default class MainBackground { this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); await (this.i18nService as I18nService).init(); - await (this.eventUploadService as EventUploadService).init(true); - this.twoFactorService.init(); + (this.eventUploadService as EventUploadService).init(true); if (this.popupOnlyContext) { return; diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index ed4fde162c..1f848e1d0f 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + KdfConfigServiceInitOptions, + kdfConfigServiceFactory, +} from "../../../auth/background/service-factories/kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -18,7 +22,10 @@ import { } from "../../background/service-factories/log-service.factory"; import { BrowserCryptoService } from "../../services/browser-crypto.service"; -import { biometricStateServiceFactory } from "./biometric-state-service.factory"; +import { + BiometricStateServiceInitOptions, + biometricStateServiceFactory, +} from "./biometric-state-service.factory"; import { cryptoFunctionServiceFactory, CryptoFunctionServiceInitOptions, @@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & LogServiceInitOptions & StateServiceInitOptions & AccountServiceInitOptions & - StateProviderInitOptions; + StateProviderInitOptions & + BiometricStateServiceInitOptions & + KdfConfigServiceInitOptions; export function cryptoServiceFactory( cache: { cryptoService?: AbstractCryptoService } & CachedServices, @@ -68,6 +77,7 @@ export function cryptoServiceFactory( await accountServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 64935ab591..259d6f154a 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService async save(key: string, obj: any): Promise { obj = objToStore(obj); + if (obj == null) { + // Safari does not support set of null values + return this.remove(key); + } + const keyedObj = { [key]: obj }; return new Promise((resolve) => { this.chromeStorageApi.set(keyedObj, () => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index 812901879d..ceadc16a58 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => { expect.any(Function), ); }); + + it("removes the key when the value is null", async () => { + const removeMock = chrome.storage.local.remove as jest.Mock; + removeMock.mockImplementation((key, callback) => { + delete store[key]; + callback(); + }); + const key = "key"; + await service.save(key, null); + expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function)); + }); }); describe("get", () => { diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index d7533a22d6..cd23c916c6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index ee842565d7..63ce45c9b7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -15,6 +16,7 @@ export class InitService { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private stateService: StateServiceAbstraction, + private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, @Inject(DOCUMENT) private document: Document, @@ -24,6 +26,7 @@ export class InitService { return async () => { await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); + this.twoFactorService.init(); if (!BrowserPopupUtils.inPopup(window)) { window.document.body.classList.add("body-full"); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 38068d1849..052e341004 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -15,10 +15,7 @@ import { INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; @@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { @@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }), safeProvider({ provide: AuthServiceAbstraction, useFactory: getBgService("authService"), deps: [], }), - safeProvider({ - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - deps: [], - }), safeProvider({ provide: SsoLoginServiceAbstraction, useFactory: getBgService("ssoLoginService"), diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index a91e876e92..bd61727a6c 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -68,6 +69,7 @@ export class LoginCommand { protected policyApiService: PolicyApiServiceAbstraction, protected orgService: OrganizationService, protected logoutCallback: () => Promise, + protected kdfConfigService: KdfConfigService, ) {} async run(email: string, password: string, options: OptionValues) { @@ -229,7 +231,7 @@ export class LoginCommand { } } if (response.requiresTwoFactor) { - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); if (twoFactorProviders.length === 0) { return Response.badRequest("No providers available for this client."); } @@ -270,7 +272,7 @@ export class LoginCommand { if ( twoFactorToken == null && - response.twoFactorProviders.size > 1 && + Object.keys(response.twoFactorProviders).length > 1 && selectedProvider.type === TwoFactorProviderType.Email ) { const emailReq = new TwoFactorEmailRequest(); @@ -563,14 +565,12 @@ export class LoginCommand { message: "Master Password Hint (optional):", }); const masterPasswordHint = hint.input; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); // Create new key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, this.email.trim().toLowerCase(), - kdf, kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey); diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index d52468139a..6b97b59c88 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -34,6 +35,7 @@ export class UnlockCommand { private syncService: SyncService, private organizationApiService: OrganizationApiServiceAbstraction, private logout: () => Promise, + private kdfConfigService: KdfConfigService, ) {} async run(password: string, cmdOptions: Record) { @@ -48,9 +50,8 @@ export class UnlockCommand { await this.setNewSessionKey(); const email = await this.stateService.getEmail(); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); - const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const storedMasterKeyHash = await firstValueFrom( this.masterPasswordService.masterKeyHash$(userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index be3ad9ea0e..45c394e912 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -30,12 +30,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -235,6 +237,7 @@ export class Main { billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; userKeyInitService: UserKeyInitService; + kdfConfigService: KdfConfigServiceAbstraction; constructor() { let p = null; @@ -357,6 +360,8 @@ export class Main { this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new CryptoService( this.masterPasswordService, this.keyGenerationService, @@ -367,6 +372,7 @@ export class Main { this.stateService, this.accountService, this.stateProvider, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -449,7 +455,11 @@ export class Main { this.stateProvider, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); this.passwordStrengthService = new PasswordStrengthService(); @@ -512,6 +522,7 @@ export class Main { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.authService = new AuthService( @@ -574,6 +585,7 @@ export class Main { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -588,6 +600,7 @@ export class Main { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultTimeoutService = new VaultTimeoutService( @@ -654,7 +667,7 @@ export class Main { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationExportService = new OrganizationVaultExportService( @@ -662,8 +675,8 @@ export class Main { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 76447f769c..7a11dc4b4a 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -134,6 +134,7 @@ export class ServeCommand { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index fa71a88f54..5d26b0850e 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -156,6 +156,7 @@ export class Program { this.main.policyApiService, this.main.organizationService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(email, password, options); this.processResponse(response, true); @@ -265,6 +266,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(password, cmd); this.processResponse(response); @@ -627,6 +629,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, this.main.logout, + this.main.kdfConfigService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index c15743ba5c..b888df8013 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; @@ -258,6 +259,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 480e443eab..f998e75d7a 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,6 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -164,6 +165,10 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: KdfConfigService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index b8feef4ab5..8e87b6663f 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; @@ -63,6 +64,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -87,6 +89,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 93dfe0abd8..feea5edd86 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService, ssoLoginService, dialogService, + kdfConfigService, ); } diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 3d9171b52e..86463dccaa 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,6 +1,7 @@ import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; 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"; @@ -35,6 +36,7 @@ describe("electronCryptoService", () => { let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); + const kdfConfigService = mock(); const mockUserId = "mock user id" as UserId; @@ -54,6 +56,7 @@ describe("electronCryptoService", () => { accountService, stateProvider, biometricStateService, + kdfConfigService, ); }); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index d113a18200..0ed0f73d41 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index cd94513f19..fcdbe1e496 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -7,10 +7,15 @@ import { OrganizationUserResetPasswordRequest, OrganizationUserResetPasswordWithIdRequest, } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -90,12 +95,17 @@ export class OrganizationUserResetPasswordService { const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey); const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; + // determine Kdf Algorithm + const kdfConfig: KdfConfig = + response.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(response.kdfIterations) + : new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism); + // Create new master key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( newMasterPassword, email.trim().toLowerCase(), - response.kdf, - new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism), + kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( newMasterPassword, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 6bcb933e51..dbc1ce820c 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -3,10 +3,15 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -231,16 +236,22 @@ export class EmergencyAccessService { const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const masterKey = await this.cryptoService.makeMasterKey( - masterPassword, - email, - takeoverResponse.kdf, - new KdfConfig( - takeoverResponse.kdfIterations, - takeoverResponse.kdfMemory, - takeoverResponse.kdfParallelism, - ), - ); + let config: KdfConfig; + + switch (takeoverResponse.kdf) { + case KdfType.PBKDF2_SHA256: + config = new PBKDF2KdfConfig(takeoverResponse.kdfIterations); + break; + case KdfType.Argon2id: + config = new Argon2KdfConfig( + takeoverResponse.kdfIterations, + takeoverResponse.kdfMemory, + takeoverResponse.kdfParallelism, + ); + break; + } + + const masterKey = await this.cryptoService.makeMasterKey(masterPassword, email, config); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey); diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index ed665fe773..ec68556931 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -47,6 +48,7 @@ describe("KeyRotationService", () => { let mockEncryptService: MockProxy; let mockStateService: MockProxy; let mockConfigService: MockProxy; + let mockKdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -65,6 +67,7 @@ describe("KeyRotationService", () => { mockEncryptService = mock(); mockStateService = mock(); mockConfigService = mock(); + mockKdfConfigService = mock(); keyRotationService = new UserKeyRotationService( mockMasterPasswordService, @@ -80,6 +83,7 @@ describe("KeyRotationService", () => { mockStateService, mockAccountService, mockConfigService, + mockKdfConfigService, ); }); diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 2ff48809a0..94c6208115 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -39,6 +40,7 @@ export class UserKeyRotationService { private stateService: StateService, private accountService: AccountService, private configService: ConfigService, + private kdfConfigService: KdfConfigService, ) {} /** @@ -54,8 +56,7 @@ export class UserKeyRotationService { const masterKey = await this.cryptoService.makeMasterKey( masterPassword, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); if (!masterKey) { diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index 372b344b10..e5a3c72337 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request"; import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request"; @@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit { private logService: LogService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -83,12 +85,10 @@ export class ChangeEmailComponent implements OnInit { step1Value.masterPassword, await this.cryptoService.getOrDeriveMasterKey(step1Value.masterPassword), ); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const newMasterKey = await this.cryptoService.makeMasterKey( step1Value.masterPassword, newEmail, - kdf, kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 6d16893170..454d96f2bd 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -5,6 +5,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -48,6 +49,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { dialogService: DialogService, private userVerificationService: UserVerificationService, private keyRotationService: UserKeyRotationService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -58,6 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index 575c6f4a23..73b1fa775d 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -5,6 +5,7 @@ import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -58,6 +59,7 @@ export class EmergencyAccessTakeoverComponent private logService: LogService, dialogService: DialogService, private dialogRef: DialogRef, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -68,6 +70,7 @@ export class EmergencyAccessTakeoverComponent policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0284c665d8..985fb3e038 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -3,6 +3,7 @@ import { Component, Inject } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,7 +19,6 @@ import { KdfType } from "@bitwarden/common/platform/enums"; templateUrl: "change-kdf-confirmation.component.html", }) export class ChangeKdfConfirmationComponent { - kdf: KdfType; kdfConfig: KdfConfig; form = new FormGroup({ @@ -37,9 +37,9 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, private stateService: StateService, private logService: LogService, - @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, + private kdfConfigService: KdfConfigService, + @Inject(DIALOG_DATA) params: { kdfConfig: KdfConfig }, ) { - this.kdf = params.kdf; this.kdfConfig = params.kdfConfig; this.masterPassword = null; } @@ -65,22 +65,24 @@ export class ChangeKdfConfirmationComponent { private async makeKeyAndSaveAsync() { const masterPassword = this.form.value.masterPassword; + + // Ensure the KDF config is valid. + this.kdfConfig.validateKdfConfig(); + const request = new KdfRequest(); - request.kdf = this.kdf; + request.kdf = this.kdfConfig.kdfType; request.kdfIterations = this.kdfConfig.iterations; - request.kdfMemory = this.kdfConfig.memory; - request.kdfParallelism = this.kdfConfig.parallelism; + if (this.kdfConfig.kdfType === KdfType.Argon2id) { + request.kdfMemory = this.kdfConfig.memory; + request.kdfParallelism = this.kdfConfig.parallelism; + } const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword); request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const email = await this.stateService.getEmail(); - // Ensure the KDF config is valid. - this.cryptoService.validateKdfConfig(this.kdf, this.kdfConfig); - const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, email, - this.kdf, this.kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html index 9b16c446be..8b1dec8e13 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html @@ -19,14 +19,14 @@ - +
- + - +
- +

{{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }}

@@ -100,7 +100,7 @@ {{ "kdfIterationsWarning" | i18n: (100000 | number) }}
- +

{{ "argon2Desc" | i18n }}

{{ "argon2Warning" | i18n }}
diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index d91fb8d083..5c05f1ba2a 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -1,7 +1,11 @@ import { Component, OnInit } from "@angular/core"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { DEFAULT_KDF_CONFIG, PBKDF2_ITERATIONS, @@ -19,7 +23,6 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon templateUrl: "change-kdf.component.html", }) export class ChangeKdfComponent implements OnInit { - kdf = KdfType.PBKDF2_SHA256; kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; kdfType = KdfType; kdfOptions: any[] = []; @@ -31,8 +34,8 @@ export class ChangeKdfComponent implements OnInit { protected ARGON2_PARALLELISM = ARGON2_PARALLELISM; constructor( - private stateService: StateService, private dialogService: DialogService, + private kdfConfigService: KdfConfigService, ) { this.kdfOptions = [ { name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }, @@ -41,19 +44,22 @@ export class ChangeKdfComponent implements OnInit { } async ngOnInit() { - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + } + + isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig { + return t instanceof PBKDF2KdfConfig; + } + + isArgon2(t: KdfConfig): t is Argon2KdfConfig { + return t instanceof Argon2KdfConfig; } async onChangeKdf(newValue: KdfType) { if (newValue === KdfType.PBKDF2_SHA256) { - this.kdfConfig = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); + this.kdfConfig = new PBKDF2KdfConfig(); } else if (newValue === KdfType.Argon2id) { - this.kdfConfig = new KdfConfig( - ARGON2_ITERATIONS.defaultValue, - ARGON2_MEMORY.defaultValue, - ARGON2_PARALLELISM.defaultValue, - ); + this.kdfConfig = new Argon2KdfConfig(); } else { throw new Error("Unknown KDF type."); } @@ -62,7 +68,6 @@ export class ChangeKdfComponent implements OnInit { async openConfirmationModal() { this.dialogService.open(ChangeKdfConfirmationComponent, { data: { - kdf: this.kdf, kdfConfig: this.kdfConfig, }, }); diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index 2844d2d862..123e3e4ac1 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,6 +33,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { stateService: StateService, userVerificationService: UserVerificationService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( router, @@ -46,6 +48,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { userVerificationService, logService, dialogService, + kdfConfigService, ); } } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 2c20328336..c97dd93d76 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -35,6 +35,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -184,6 +185,7 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -972,10 +974,10 @@ export class VaultComponent implements OnInit, OnDestroy { } async isLowKdfIteration() { - const kdfType = await this.stateService.getKdfType(); - const kdfOptions = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); return ( - kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < PBKDF2_ITERATIONS.defaultValue + kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && + kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue ); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts new file mode 100644 index 0000000000..604d61f3db --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts @@ -0,0 +1,130 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Directive, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject, from, Subject, switchMap } from "rxjs"; +import { first, takeUntil } from "rxjs/operators"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../services/web-provider.service"; + +@Directive() +export abstract class BaseClientsComponent implements OnInit, OnDestroy { + protected destroy$ = new Subject(); + + private searchText$ = new BehaviorSubject(""); + + get searchText() { + return this.searchText$.value; + } + + set searchText(value: string) { + this.searchText$.next(value); + this.selection.clear(); + this.dataSource.filter = value; + } + + private searching = false; + protected scrolled = false; + protected pageSize = 100; + private pagedClientsCount = 0; + protected selection = new SelectionModel(true, []); + + protected clients: ProviderOrganizationOrganizationDetailsResponse[]; + protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + protected dataSource = new TableDataSource(); + + abstract providerId: string; + + protected constructor( + protected activatedRoute: ActivatedRoute, + protected dialogService: DialogService, + private i18nService: I18nService, + private searchService: SearchService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) {} + + abstract load(): Promise; + + ngOnInit() { + this.activatedRoute.queryParams + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((queryParams) => { + this.searchText = queryParams.search; + }); + + this.searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.searching = isSearchable; + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + isPaging() { + if (this.searching && this.scrolled) { + this.resetPaging(); + } + return !this.searching && this.clients && this.clients.length > this.pageSize; + } + + resetPaging() { + this.pagedClients = []; + this.loadMore(); + } + + loadMore() { + if (!this.clients || this.clients.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedClients.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { + pagedSize = this.pagedClientsCount; + } + if (this.clients.length > pagedLength) { + this.pagedClients = this.pagedClients.concat( + this.clients.slice(pagedLength, pagedLength + pagedSize), + ); + } + this.pagedClientsCount = this.pagedClients.length; + this.scrolled = this.pagedClients.length > this.pageSize; + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index abdfd6deff..54e264c666 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,29 +1,26 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { combineLatest, firstValueFrom, from } from "rxjs"; +import { concatMap, switchMap, takeUntil } from "rxjs/operators"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { PlanType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; +import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -36,90 +33,76 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ClientsComponent implements OnInit { +export class ClientsComponent extends BaseClientsComponent { providerId: string; addableOrganizations: Organization[]; loading = true; manageOrganizations = false; showAddExisting = false; - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise; - private pagedClientsCount = 0; - - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, false, ); - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearching: boolean = false; - - get searchText() { - return this._searchText$.value; - } - - set searchText(value: string) { - this._searchText$.next(value); - } constructor( - private route: ActivatedRoute, private router: Router, private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private logService: LogService, - private modalService: ModalService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, private configService: ConfigService, - ) {} + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); + } - async ngOnInit() { - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - - if (enableConsolidatedBilling) { - await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); - } else { - this.route.parent.params - .pipe( - switchMap((params) => { - this.providerId = params.providerId; - return from(this.load()); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { - this.searchText = qParams.search; - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); - } + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + consolidatedBillingEnabled && + provider.providerStatus === ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); } ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -141,37 +124,6 @@ export class ClientsComponent implements OnInit { this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - resetPaging() { - this.pagedClients = []; - this.loadMore(); - } - - loadMore() { - if (!this.clients || this.clients.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedClients.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { - pagedSize = this.pagedClientsCount; - } - if (this.clients.length > pagedLength) { - this.pagedClients = this.pagedClients.concat( - this.clients.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedClientsCount = this.pagedClients.length; - this.didScroll = this.pagedClients.length > this.pageSize; - } - async addExistingOrganization() { const dialogRef = AddOrganizationComponent.open(this.dialogService, { providerId: this.providerId, @@ -182,33 +134,4 @@ export class ClientsComponent implements OnInit { await this.load(); } } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.webProviderService.detachOrganization( - this.providerId, - organization.id, - ); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("detachedOrganization", organization.organizationName), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index ca2b1a3545..55efbe1386 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -7,7 +7,12 @@ {{ "assignedSeats" | i18n }} - +

diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts index 2184a617cf..3cc96c4589 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts @@ -1,21 +1,23 @@ -import { SelectionModel } from "@angular/cdk/collections"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs"; +import { concatMap, switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, TableDataSource } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { @@ -27,127 +29,91 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o @Component({ templateUrl: "manage-client-organizations.component.html", }) - -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { +export class ManageClientOrganizationsComponent extends BaseClientsComponent { providerId: string; + provider: Provider; + loading = true; manageOrganizations = false; - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearching: boolean = false; + private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); - get searchText() { - return this._searchText$.value; - } - - set searchText(search: string) { - this._searchText$.value; - - this.selection.clear(); - this.dataSource.filter = search; - } - - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise; - private pagedClientsCount = 0; - selection = new SelectionModel(true, []); - protected dataSource = new TableDataSource(); protected plans: PlanResponse[]; constructor( - private route: ActivatedRoute, - private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private dialogService: DialogService, private billingApiService: BillingApiService, - ) {} - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - this.providerId = params.providerId; - - await this.load(); - - /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - this.searchText = qParams.search; - }); - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); + private configService: ConfigService, + private providerService: ProviderService, + private router: Router, + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + !consolidatedBillingEnabled || + provider.providerStatus !== ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../clients"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + this.provider = provider; + this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin; + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + super.ngOnDestroy(); } async load() { - const clientsResponse = await this.apiService.getProviderClients(this.providerId); - this.clients = - clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : []; - this.dataSource.data = this.clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + this.clients = (await this.apiService.getProviderClients(this.providerId)).data; - const plansResponse = await this.billingApiService.getPlans(); - this.plans = plansResponse.data; + this.dataSource.data = this.clients; + + this.plans = (await this.billingApiService.getPlans()).data; this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - async resetPaging() { - this.pagedClients = []; - this.loadMore(); - } - - loadMore() { - if (!this.clients || this.clients.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedClients.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { - pagedSize = this.pagedClientsCount; - } - if (this.clients.length > pagedLength) { - this.pagedClients = this.pagedClients.concat( - this.clients.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedClientsCount = this.pagedClients.length; - this.didScroll = this.pagedClients.length > this.pageSize; - } - async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) { if (organization == null) { return; @@ -161,35 +127,6 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { await this.load(); } - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.webProviderService.detachOrganization( - this.providerId, - organization.id, - ); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("detachedOrganization", organization.organizationName), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } - createClientOrganization = async () => { const reference = openCreateClientOrganizationDialog(this.dialogService, { data: { diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 1086428f4c..b1f75de58c 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -3,13 +3,13 @@ import { Subject, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -31,7 +31,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { minimumLength = Utils.minimumPasswordLength; protected email: string; - protected kdf: KdfType; protected kdfConfig: KdfConfig; protected destroy$ = new Subject(); @@ -45,6 +44,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected stateService: StateService, protected dialogService: DialogService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -73,18 +73,14 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { } const email = await this.stateService.getEmail(); - if (this.kdf == null) { - this.kdf = await this.stateService.getKdfType(); - } if (this.kdfConfig == null) { - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); } // Create new master key const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 927fbb27b1..89af31da81 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -79,6 +80,7 @@ export class LockComponent implements OnInit, OnDestroy { protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -208,14 +210,12 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email, - kdf, kdfConfig, ); const storedMasterKeyHash = await firstValueFrom( diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 3cffebe71b..2ba7669290 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,7 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DEFAULT_KDF_CONFIG, DEFAULT_KDF_TYPE } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; @@ -273,9 +273,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn name: string, ): Promise { const hint = this.formGroup.value.hint; - const kdf = DEFAULT_KDF_TYPE; const kdfConfig = DEFAULT_KDF_CONFIG; - const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); const newUserKey = await this.cryptoService.makeUserKey(key); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key); const keys = await this.cryptoService.makeKeyPair(newUserKey[0]); @@ -287,10 +286,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn newUserKey[1].encryptedString, this.referenceData, this.captchaToken, - kdf, + kdfConfig.kdfType, kdfConfig.iterations, - kdfConfig.memory, - kdfConfig.parallelism, ); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.getOrganizationInvitation(); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index eebf87655b..00a36434b0 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -13,6 +13,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -23,11 +24,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - HashPurpose, - DEFAULT_KDF_TYPE, - DEFAULT_KDF_CONFIG, -} from "@bitwarden/common/platform/enums"; +import { HashPurpose, 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -73,6 +70,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -83,6 +81,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -139,7 +138,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { } async setupSubmitActions() { - this.kdf = DEFAULT_KDF_TYPE; this.kdfConfig = DEFAULT_KDF_CONFIG; return true; } @@ -169,10 +167,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.hint, this.orgSsoIdentifier, keysRequest, - this.kdf, + this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions this.kdfConfig.iterations, - this.kdfConfig.memory, - this.kdfConfig.parallelism, ); try { if (this.resetPasswordAutoEnroll) { @@ -246,9 +242,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - - await this.stateService.setKdfType(this.kdf); - await this.stateService.setKdfConfig(this.kdfConfig); + await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index ade23f4fef..f0b66b8e70 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -2,6 +2,7 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Directive, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -22,6 +23,7 @@ export class SetPinComponent implements OnInit { private userVerificationService: UserVerificationService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -43,8 +45,7 @@ export class SetPinComponent implements OnInit { const pinKey = await this.cryptoService.makePinKey( pin, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); const userKey = await this.cryptoService.getUserKey(); const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey); diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 269ec51e30..ae644028f9 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -253,7 +253,7 @@ describe("SsoComponent", () => { describe("2FA scenarios", () => { beforeEach(() => { const authResult = new AuthResult(); - authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); + authResult.twoFactorProviders = { [TwoFactorProviderType.Authenticator]: {} }; // use standard user with MP because this test is not concerned with password reset. selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); diff --git a/libs/angular/src/auth/components/two-factor-options.component.ts b/libs/angular/src/auth/components/two-factor-options.component.ts index 2808e41cc2..1bbf81fa34 100644 --- a/libs/angular/src/auth/components/two-factor-options.component.ts +++ b/libs/angular/src/auth/components/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Directive, EventEmitter, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -24,11 +27,11 @@ export class TwoFactorOptionsComponent implements OnInit { protected environmentService: EnvironmentService, ) {} - ngOnInit() { - this.providers = this.twoFactorService.getSupportedProviders(this.win); + async ngOnInit() { + this.providers = await this.twoFactorService.getSupportedProviders(this.win); } - choose(p: any) { + async choose(p: TwoFactorProviderDetails) { this.onProviderSelected.emit(p.type); } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f73f0483be..8e96c48ba0 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,7 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async ngOnInit() { - if (!(await this.authing()) || this.twoFactorService.getProviders() == null) { + if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate([this.loginRoute]); @@ -145,7 +145,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ); } - this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported); + this.selectedProviderType = await this.twoFactorService.getDefaultProvider( + this.webAuthnSupported, + ); await this.init(); } @@ -162,12 +164,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.cleanupWebAuthn(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); switch (this.selectedProviderType) { case TwoFactorProviderType.WebAuthn: if (!this.webAuthnNewTab) { - setTimeout(() => { - this.authWebAuthn(); + setTimeout(async () => { + await this.authWebAuthn(); }, 500); } break; @@ -212,7 +216,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; - if (this.twoFactorService.getProviders().size > 1) { + if ((await this.twoFactorService.getProviders()).size > 1) { await this.sendEmail(false); } break; @@ -474,8 +478,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = null; } - authWebAuthn() { - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + async authWebAuthn() { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); if (!this.webAuthnSupported || this.webAuthn == null) { return; diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 2ffffb6c5d..264f351542 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; @@ -44,6 +45,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -54,6 +56,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -90,8 +93,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { return false; } - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 54fdc83239..bd6da6b760 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; @@ -59,6 +60,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + kdfConfigService: KdfConfigService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { @@ -71,6 +73,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -104,8 +107,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { this.email = await this.stateService.getEmail(); - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } @@ -124,7 +126,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 42879a8424..c7b27a25c2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -63,6 +63,7 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction, @@ -85,6 +86,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; @@ -390,6 +392,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -543,6 +546,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AccountServiceAbstraction, StateProvider, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -713,7 +717,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -724,8 +728,8 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, CollectionServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -834,6 +838,7 @@ const safeProviders: SafeProvider[] = [ LogService, VaultTimeoutSettingsServiceAbstraction, PlatformUtilsServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -869,7 +874,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: TwoFactorServiceAbstraction, useClass: TwoFactorService, - deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], + deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider], }), safeProvider({ provide: FormValidationErrorsServiceAbstraction, @@ -985,6 +990,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, LogService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -1150,6 +1156,11 @@ const safeProviders: SafeProvider[] = [ useClass: ProviderApiService, deps: [ApiServiceAbstraction], }), + safeProvider({ + provide: KdfConfigServiceAbstraction, + useClass: KdfConfigService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d29c74b42d..d9b73a0e7f 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -184,8 +184,6 @@ export class AddEditComponent implements OnInit, OnDestroy { FeatureFlag.FlexibleCollectionsV1, false, ); - this.writeableCollections = await this.loadCollections(); - this.canUseReprompt = await this.passwordRepromptService.enabled(); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -197,6 +195,9 @@ export class AddEditComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.writeableCollections = await this.loadCollections(); + this.canUseReprompt = await this.passwordRepromptService.enabled(); } ngOnDestroy() { diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 4e0b1ac3ac..a123e30053 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -44,6 +45,7 @@ describe("AuthRequestLoginStrategy", () => { let userDecryptionOptions: MockProxy; let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -77,13 +79,16 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions = mock(); deviceTrustService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: mockUserId, + }); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, @@ -101,6 +106,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions, deviceTrustService, billingAccountProfileStateService, + kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 5220e432de..a66d987984 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -63,6 +64,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -78,6 +80,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e0833342ce..3284f6e947 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -24,11 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - Account, - AccountProfile, - AccountKeys, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -117,6 +114,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -136,6 +134,7 @@ describe("LoginStrategy", () => { stateService = mock(); twoFactorService = mock(); userDecryptionOptionsService = mock(); + kdfConfigService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -162,6 +161,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); }); @@ -208,11 +208,8 @@ describe("LoginStrategy", () => { userId: userId, name: name, email: email, - kdfIterations: kdfIterations, - kdfType: kdf, }, }, - keys: new AccountKeys(), }), ); expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( @@ -221,6 +218,21 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); + it("throws if active account isn't found after being initialized", async () => { + const idTokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValue(idTokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + + stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); + stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + + accountService.activeAccountSubject.next(null); + + await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); + }); + it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; @@ -304,8 +316,10 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(0, null); + expected.twoFactorProviders = { 0: null } as Record< + TwoFactorProviderType, + Record + >; expect(result).toEqual(expected); }); @@ -334,8 +348,9 @@ describe("LoginStrategy", () => { expect(messagingService.send).not.toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" }); + expected.twoFactorProviders = { + [TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" }, + }; expected.email = userEmail; expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken; @@ -404,6 +419,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e120..fd268d955e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,13 +1,15 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request"; import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; @@ -27,6 +29,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; @@ -72,6 +75,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected KdfConfigService: KdfConfigService, ) {} abstract exportCache(): CacheData; @@ -97,7 +101,7 @@ export abstract class LoginStrategy { } protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> { - this.twoFactorService.clearSelectedProvider(); + await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; const response = await this.apiService.postIdentityToken(tokenRequest); @@ -155,12 +159,12 @@ export abstract class LoginStrategy { * It also sets the access token and refresh token in the token service. * * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. - * @returns {Promise} - A promise that resolves when the account information has been successfully saved. + * @returns {Promise} - A promise that resolves the the UserId when the account information has been successfully saved. */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub; + const userId = accountInformation.sub as UserId; const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); @@ -182,21 +186,30 @@ export abstract class LoginStrategy { userId, name: accountInformation.name, email: accountInformation.email, - kdfIterations: tokenResponse.kdfIterations, - kdfMemory: tokenResponse.kdfMemory, - kdfParallelism: tokenResponse.kdfParallelism, - kdfType: tokenResponse.kdf, }, }, }), ); + await this.verifyAccountAdded(userId); + await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); + await this.KdfConfigService.setKdfConfig( + userId as UserId, + tokenResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(tokenResponse.kdfIterations) + : new Argon2KdfConfig( + tokenResponse.kdfIterations, + tokenResponse.kdfMemory, + tokenResponse.kdfParallelism, + ), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); - return userId as UserId; + return userId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise { @@ -273,7 +286,7 @@ export abstract class LoginStrategy { const result = new AuthResult(); result.twoFactorProviders = response.twoFactorProviders2; - this.twoFactorService.setProviders(response); + await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; result.email = response.email; @@ -295,4 +308,24 @@ export abstract class LoginStrategy { result.captchaSiteKey = response.siteKey; return result; } + + /** + * Verifies that the active account is set after initialization. + * Note: In browser there is a slight delay between when active account emits in background, + * and when it emits in foreground. We're giving the foreground 1 second to catch up. + * If nothing is emitted, we throw an error. + */ + private async verifyAccountAdded(expectedUserId: UserId) { + await firstValueFrom( + this.accountService.activeAccount$.pipe( + filter((account) => account?.id === expectedUserId), + timeout({ + first: 1000, + with: () => { + throw new Error("Expected user never made active user after initialization."); + }, + }), + ), + ); + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index b902fff574..5c1fe9b1fe 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -71,6 +72,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -94,9 +96,12 @@ describe("PasswordLoginStrategy", () => { policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); @@ -127,6 +132,7 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 2490c35a00..d3ce8fa9e8 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -89,6 +90,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -104,6 +106,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index df33415247..416e910b47 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -54,6 +55,7 @@ describe("SsoLoginStrategy", () => { let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; @@ -86,10 +88,13 @@ describe("SsoLoginStrategy", () => { authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); ssoLoginStrategy = new SsoLoginStrategy( null, @@ -110,6 +115,7 @@ describe("SsoLoginStrategy", () => { authRequestService, i18nService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index dc63f0fae1..c7cd9052f8 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -98,6 +99,7 @@ export class SsoLoginStrategy extends LoginStrategy { private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -113,6 +115,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 5e7d7985b1..130e6c2d89 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -49,6 +50,7 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; @@ -76,10 +78,13 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); apiLogInStrategy = new UserApiLoginStrategy( cache, @@ -98,6 +103,7 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 4a0d005b1c..d7ee6fdc4b 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -57,6 +58,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); } diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 1d96921286..bbcd3bafdd 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -17,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService } from "@bitwarden/common/spec"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -42,11 +44,13 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; const token = "mockToken"; const deviceId = Utils.newGuid(); + const userId = Utils.newGuid() as UserId; let webAuthnCredentials!: WebAuthnLoginCredentials; @@ -67,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = new FakeAccountService(null); + accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); @@ -81,10 +85,13 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, @@ -101,6 +108,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); // Create credentials diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 8a62a8fb3c..ac487b3a82 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -57,6 +58,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 33708885e2..f1b5590404 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -3,6 +3,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -66,6 +67,7 @@ describe("LoginStrategyService", () => { let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -95,6 +97,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); + kdfConfigService = mock(); sut = new LoginStrategyService( accountService, @@ -119,6 +122,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index aee74e6607..13cca69b3a 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -10,13 +10,18 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; @@ -32,7 +37,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; @@ -105,6 +110,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -233,24 +239,25 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdf: KdfType = null; let kdfConfig: KdfConfig = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - kdf = preloginResponse.kdf; - kdfConfig = new KdfConfig( - preloginResponse.kdfIterations, - preloginResponse.kdfMemory, - preloginResponse.kdfParallelism, - ); + kdfConfig = + preloginResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(preloginResponse.kdfIterations) + : new Argon2KdfConfig( + preloginResponse.kdfIterations, + preloginResponse.kdfMemory, + preloginResponse.kdfParallelism, + ); } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); } // TODO: move to auth request service @@ -354,6 +361,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.Sso: return new SsoLoginStrategy( @@ -375,6 +383,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( @@ -394,6 +403,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( @@ -412,6 +422,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.userDecryptionOptionsService, this.deviceTrustService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( @@ -429,6 +440,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.kdfConfigService, ); } }), diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts index 149d5d9a53..85d36b8d73 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts @@ -1,9 +1,9 @@ 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 { KdfType } from "@bitwarden/common/platform/enums"; 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"; @@ -16,6 +16,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { private cryptoService: CryptoService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logService: LogService, + private kdfConfigService: KdfConfigService, ) {} async decryptUserKeyWithPin(pin: string): Promise { try { @@ -24,8 +25,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } = await this.getPinKeyEncryptedKeys(pinLockType); - const kdf: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); let userKey: UserKey; const email = await this.stateService.getEmail(); if (oldPinKeyEncryptedMasterKey) { @@ -33,7 +33,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { pinLockType === "TRANSIENT", pin, email, - kdf, kdfConfig, oldPinKeyEncryptedMasterKey, ); @@ -41,7 +40,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { userKey = await this.cryptoService.decryptUserKeyWithPin( pin, email, - kdf, kdfConfig, pinKeyEncryptedUserKey, ); diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts index 17e0e14c51..c6fddf8efb 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts @@ -1,9 +1,10 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +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 { @@ -13,6 +14,7 @@ import { import { UserKey } from "@bitwarden/common/types/key"; import { PinCryptoService } from "./pin-crypto.service.implementation"; + describe("PinCryptoService", () => { let pinCryptoService: PinCryptoService; @@ -20,6 +22,7 @@ describe("PinCryptoService", () => { const cryptoService = mock(); const vaultTimeoutSettingsService = mock(); const logService = mock(); + const kdfConfigService = mock(); beforeEach(() => { jest.clearAllMocks(); @@ -29,6 +32,7 @@ describe("PinCryptoService", () => { cryptoService, vaultTimeoutSettingsService, logService, + kdfConfigService, ); }); @@ -39,7 +43,6 @@ describe("PinCryptoService", () => { describe("decryptUserKeyWithPin(...)", () => { const mockPin = "1234"; const mockProtectedPin = "protectedPin"; - const DEFAULT_PBKDF2_ITERATIONS = 600000; const mockUserEmail = "user@example.com"; const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey; @@ -49,7 +52,7 @@ describe("PinCryptoService", () => { ) { vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); stateService.getEmail.mockResolvedValue(mockUserEmail); if (migrationStatus === "PRE") { diff --git a/libs/common/src/admin-console/enums/index.ts b/libs/common/src/admin-console/enums/index.ts index 0cbdf65805..83b8a941a0 100644 --- a/libs/common/src/admin-console/enums/index.ts +++ b/libs/common/src/admin-console/enums/index.ts @@ -7,3 +7,4 @@ export * from "./provider-type.enum"; export * from "./provider-user-status-type.enum"; export * from "./provider-user-type.enum"; export * from "./scim-provider-type.enum"; +export * from "./provider-status-type.enum"; diff --git a/libs/common/src/admin-console/enums/provider-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-status-type.enum.ts new file mode 100644 index 0000000000..8da60af0eb --- /dev/null +++ b/libs/common/src/admin-console/enums/provider-status-type.enum.ts @@ -0,0 +1,5 @@ +export enum ProviderStatusType { + Pending = 0, + Created = 1, + Billable = 2, +} diff --git a/libs/common/src/admin-console/models/data/provider.data.ts b/libs/common/src/admin-console/models/data/provider.data.ts index a848888025..ff060ae270 100644 --- a/libs/common/src/admin-console/models/data/provider.data.ts +++ b/libs/common/src/admin-console/models/data/provider.data.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProfileProviderResponse } from "../response/profile-provider.response"; export class ProviderData { @@ -9,6 +9,7 @@ export class ProviderData { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: ProfileProviderResponse) { this.id = response.id; @@ -18,5 +19,6 @@ export class ProviderData { this.enabled = response.enabled; this.userId = response.userId; this.useEvents = response.useEvents; + this.providerStatus = response.providerStatus; } } diff --git a/libs/common/src/admin-console/models/domain/provider.ts b/libs/common/src/admin-console/models/domain/provider.ts index d6d3d3c462..d51f698547 100644 --- a/libs/common/src/admin-console/models/domain/provider.ts +++ b/libs/common/src/admin-console/models/domain/provider.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProviderData } from "../data/provider.data"; export class Provider { @@ -9,6 +9,7 @@ export class Provider { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(obj?: ProviderData) { if (obj == null) { @@ -22,6 +23,7 @@ export class Provider { this.enabled = obj.enabled; this.userId = obj.userId; this.useEvents = obj.useEvents; + this.providerStatus = obj.providerStatus; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-provider.response.ts b/libs/common/src/admin-console/models/response/profile-provider.response.ts index eaecc9b847..701fe843de 100644 --- a/libs/common/src/admin-console/models/response/profile-provider.response.ts +++ b/libs/common/src/admin-console/models/response/profile-provider.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../../../models/response/base.response"; -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; export class ProfileProviderResponse extends BaseResponse { @@ -12,6 +12,7 @@ export class ProfileProviderResponse extends BaseResponse { permissions: PermissionsApi; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: any) { super(response); @@ -24,5 +25,6 @@ export class ProfileProviderResponse extends BaseResponse { this.permissions = new PermissionsApi(this.getResponseProperty("permissions")); this.userId = this.getResponseProperty("UserId"); this.useEvents = this.getResponseProperty("UseEvents"); + this.providerStatus = this.getResponseProperty("ProviderStatus"); } } diff --git a/libs/common/src/admin-console/services/provider.service.spec.ts b/libs/common/src/admin-console/services/provider.service.spec.ts index fcba9d5023..95da633f5c 100644 --- a/libs/common/src/admin-console/services/provider.service.spec.ts +++ b/libs/common/src/admin-console/services/provider.service.spec.ts @@ -2,7 +2,7 @@ import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from ". import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { ProviderUserStatusType, ProviderUserType } from "../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../enums"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; @@ -64,6 +64,7 @@ describe("PROVIDERS key definition", () => { enabled: true, userId: "string", useEvents: true, + providerStatus: ProviderStatusType.Pending, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts new file mode 100644 index 0000000000..6b41979e1b --- /dev/null +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -0,0 +1,7 @@ +import { UserId } from "../../types/guid"; +import { KdfConfig } from "../models/domain/kdf-config"; + +export abstract class KdfConfigService { + setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise; + getKdfConfig: () => Promise; +} diff --git a/libs/common/src/auth/abstractions/two-factor.service.ts b/libs/common/src/auth/abstractions/two-factor.service.ts index 3ea7eb8db9..a0a9ecd2af 100644 --- a/libs/common/src/auth/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/abstractions/two-factor.service.ts @@ -12,12 +12,12 @@ export interface TwoFactorProviderDetails { export abstract class TwoFactorService { init: () => void; - getSupportedProviders: (win: Window) => TwoFactorProviderDetails[]; - getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType; - setSelectedProvider: (type: TwoFactorProviderType) => void; - clearSelectedProvider: () => void; + getSupportedProviders: (win: Window) => Promise; + getDefaultProvider: (webAuthnSupported: boolean) => Promise; + setSelectedProvider: (type: TwoFactorProviderType) => Promise; + clearSelectedProvider: () => Promise; - setProviders: (response: IdentityTwoFactorResponse) => void; - clearProviders: () => void; - getProviders: () => Map; + setProviders: (response: IdentityTwoFactorResponse) => Promise; + clearProviders: () => Promise; + getProviders: () => Promise>; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 993ce08d58..f45466777e 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -14,7 +14,7 @@ export class AuthResult { resetMasterPassword = false; forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None; - twoFactorProviders: Map = null; + twoFactorProviders: Partial>> = null; ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index a25ba586e9..ce01f09702 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -1,11 +1,86 @@ -export class KdfConfig { - iterations: number; - memory?: number; - parallelism?: number; +import { Jsonify } from "type-fest"; - constructor(iterations: number, memory?: number, parallelism?: number) { - this.iterations = iterations; - this.memory = memory; - this.parallelism = parallelism; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + KdfType, + PBKDF2_ITERATIONS, +} from "../../../platform/enums/kdf-type.enum"; + +/** + * Represents a type safe KDF configuration. + */ +export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; + +/** + * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. + */ +export class PBKDF2KdfConfig { + kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; + iterations: number; + + constructor(iterations?: number) { + this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue; + } + + /** + * Validates the PBKDF2 KDF configuration. + * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. + */ + validateKdfConfig(): void { + if (!PBKDF2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + } + } + + static fromJSON(json: Jsonify): PBKDF2KdfConfig { + return new PBKDF2KdfConfig(json.iterations); + } +} + +/** + * Argon2 KDF configuration. + */ +export class Argon2KdfConfig { + kdfType: KdfType.Argon2id = KdfType.Argon2id; + iterations: number; + memory: number; + parallelism: number; + + constructor(iterations?: number, memory?: number, parallelism?: number) { + this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue; + this.memory = memory ?? ARGON2_MEMORY.defaultValue; + this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue; + } + + /** + * Validates the Argon2 KDF configuration. + * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. + */ + validateKdfConfig(): void { + if (!ARGON2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + } + + if (!ARGON2_MEMORY.inRange(this.memory)) { + throw new Error( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + } + + if (!ARGON2_PARALLELISM.inRange(this.parallelism)) { + throw new Error( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, + ); + } + } + + static fromJSON(json: Jsonify): Argon2KdfConfig { + return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } } diff --git a/libs/common/src/auth/models/request/set-key-connector-key.request.ts b/libs/common/src/auth/models/request/set-key-connector-key.request.ts index dfd32689d8..c8081bdec2 100644 --- a/libs/common/src/auth/models/request/set-key-connector-key.request.ts +++ b/libs/common/src/auth/models/request/set-key-connector-key.request.ts @@ -11,18 +11,14 @@ export class SetKeyConnectorKeyRequest { kdfParallelism?: number; orgIdentifier: string; - constructor( - key: string, - kdf: KdfType, - kdfConfig: KdfConfig, - orgIdentifier: string, - keys: KeysRequest, - ) { + constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) { this.key = key; - this.kdf = kdf; + this.kdf = kdfConfig.kdfType; this.kdfIterations = kdfConfig.iterations; - this.kdfMemory = kdfConfig.memory; - this.kdfParallelism = kdfConfig.parallelism; + if (kdfConfig.kdfType === KdfType.Argon2id) { + this.kdfMemory = kdfConfig.memory; + this.kdfParallelism = kdfConfig.parallelism; + } this.orgIdentifier = orgIdentifier; this.keys = keys; } diff --git a/libs/common/src/auth/models/response/identity-two-factor.response.ts b/libs/common/src/auth/models/response/identity-two-factor.response.ts index bc5d2fbf85..dce64e8ef3 100644 --- a/libs/common/src/auth/models/response/identity-two-factor.response.ts +++ b/libs/common/src/auth/models/response/identity-two-factor.response.ts @@ -4,8 +4,10 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; import { MasterPasswordPolicyResponse } from "./master-password-policy.response"; export class IdentityTwoFactorResponse extends BaseResponse { + // contains available two-factor providers twoFactorProviders: TwoFactorProviderType[]; - twoFactorProviders2 = new Map(); + // a map of two-factor providers to necessary data for completion + twoFactorProviders2: Record>; captchaToken: string; ssoEmail2faSessionToken: string; email?: string; @@ -15,15 +17,7 @@ export class IdentityTwoFactorResponse extends BaseResponse { super(response); this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders"); - const twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); - if (twoFactorProviders2 != null) { - for (const prop in twoFactorProviders2) { - // eslint-disable-next-line - if (twoFactorProviders2.hasOwnProperty(prop)) { - this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]); - } - } - } + this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); this.masterPasswordPolicy = new MasterPasswordPolicyResponse( this.getResponseProperty("MasterPasswordPolicy"), ); diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts new file mode 100644 index 0000000000..67bcf721bc --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -0,0 +1,104 @@ +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + PBKDF2_ITERATIONS, +} from "../../platform/enums/kdf-type.enum"; +import { Utils } from "../../platform/misc/utils"; +import { UserId } from "../../types/guid"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +import { KdfConfigService } from "./kdf-config.service"; + +describe("KdfConfigService", () => { + let sutKdfConfigService: KdfConfigService; + + let fakeStateProvider: FakeStateProvider; + let fakeAccountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + fakeAccountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(fakeAccountService); + sutKdfConfigService = new KdfConfigService(fakeStateProvider); + }); + + it("setKdfConfig(): should set the KDF config", async () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should get the KDF config", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should throw error KDF cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = null; + try { + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("kdfConfig cannot be null")); + } + }); + + it("setKdfConfig(): should throw error userId cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + try { + await sutKdfConfigService.setKdfConfig(null, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); + + it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + try { + await sutKdfConfigService.getKdfConfig(); + } catch (e) { + expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + } + }); + + it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`, + ); + }); +}); diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts new file mode 100644 index 0000000000..cfd2a3e1de --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -0,0 +1,41 @@ +import { firstValueFrom } from "rxjs"; + +import { KdfType } from "../../platform/enums/kdf-type.enum"; +import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "../abstractions/kdf-config.service"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdfConfig", { + deserializer: (kdfConfig: KdfConfig) => { + if (kdfConfig == null) { + return null; + } + return kdfConfig.kdfType === KdfType.PBKDF2_SHA256 + ? PBKDF2KdfConfig.fromJSON(kdfConfig) + : Argon2KdfConfig.fromJSON(kdfConfig); + }, + clearOn: ["logout"], +}); + +export class KdfConfigService implements KdfConfigServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) { + if (!userId) { + throw new Error("userId cannot be null"); + } + if (kdfConfig === null) { + throw new Error("kdfConfig cannot be null"); + } + await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); + } + + async getKdfConfig(): Promise { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); + if (state === null) { + throw new Error("KdfConfig for active user account state is null"); + } + return state; + } +} diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f8e523cce4..c19185ae91 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -7,6 +7,7 @@ import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; +import { KdfType } from "../../platform/enums/kdf-type.enum"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { @@ -20,7 +21,7 @@ import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; -import { KdfConfig } from "../models/domain/kdf-config"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; @@ -133,12 +134,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userDecryptionOptions, } = tokenResponse; const password = await this.keyGenerationService.createKey(512); - const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism); + const kdfConfig: KdfConfig = + kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(kdfIterations) + : new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism); const masterKey = await this.cryptoService.makeMasterKey( password.keyB64, await this.tokenService.getEmail(), - kdf, kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); @@ -162,7 +165,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const keys = new KeysRequest(pubKey, privKey.encryptedString); const setPasswordRequest = new SetKeyConnectorKeyRequest( userKey[1].encryptedString, - kdf, kdfConfig, orgId, keys, diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index cd1e5ea122..50d2556157 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom, map } from "rxjs"; + import { I18nService } from "../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; +import { Utils } from "../../platform/misc/utils"; +import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state"; import { TwoFactorProviderDetails, TwoFactorService as TwoFactorServiceAbstraction, @@ -59,13 +63,36 @@ export const TwoFactorProviders: Partial, TwoFactorProviderType>( + TWO_FACTOR_MEMORY, + "providers", + { + deserializer: (obj) => obj, + }, +); + +// Memory storage as only required during authentication process +export const SELECTED_PROVIDER = new KeyDefinition( + TWO_FACTOR_MEMORY, + "selected", + { + deserializer: (obj) => obj, + }, +); + export class TwoFactorService implements TwoFactorServiceAbstraction { - private twoFactorProvidersData: Map; - private selectedTwoFactorProviderType: TwoFactorProviderType = null; + private providersState = this.globalStateProvider.get(PROVIDERS); + private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER); + readonly providers$ = this.providersState.state$.pipe( + map((providers) => Utils.recordToMap(providers)), + ); + readonly selected$ = this.selectedState.state$; constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private globalStateProvider: GlobalStateProvider, ) {} init() { @@ -93,63 +120,60 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { this.i18nService.t("yubiKeyDesc"); } - getSupportedProviders(win: Window): TwoFactorProviderDetails[] { + async getSupportedProviders(win: Window): Promise { + const data = await firstValueFrom(this.providers$); const providers: any[] = []; - if (this.twoFactorProvidersData == null) { + if (data == null) { return providers; } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && + data.has(TwoFactorProviderType.OrganizationDuo) && this.platformUtilsService.supportsDuo() ) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { + if (data.has(TwoFactorProviderType.Authenticator)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { + if (data.has(TwoFactorProviderType.Yubikey)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && - this.platformUtilsService.supportsDuo() - ) { + if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && + data.has(TwoFactorProviderType.WebAuthn) && this.platformUtilsService.supportsWebAuthn(win) ) { providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { + if (data.has(TwoFactorProviderType.Email)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } return providers; } - getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProvidersData == null) { + async getDefaultProvider(webAuthnSupported: boolean): Promise { + const data = await firstValueFrom(this.providers$); + const selected = await firstValueFrom(this.selected$); + if (data == null) { return null; } - if ( - this.selectedTwoFactorProviderType != null && - this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType) - ) { - return this.selectedTwoFactorProviderType; + if (selected != null && data.has(selected)) { + return selected; } let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProvidersData.forEach((_value, type) => { + data.forEach((_value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { @@ -164,23 +188,23 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { return providerType; } - setSelectedProvider(type: TwoFactorProviderType) { - this.selectedTwoFactorProviderType = type; + async setSelectedProvider(type: TwoFactorProviderType): Promise { + await this.selectedState.update(() => type); } - clearSelectedProvider() { - this.selectedTwoFactorProviderType = null; + async clearSelectedProvider(): Promise { + await this.selectedState.update(() => null); } - setProviders(response: IdentityTwoFactorResponse) { - this.twoFactorProvidersData = response.twoFactorProviders2; + async setProviders(response: IdentityTwoFactorResponse): Promise { + await this.providersState.update(() => response.twoFactorProviders2); } - clearProviders() { - this.twoFactorProvidersData = null; + async clearProviders(): Promise { + await this.providersState.update(() => null); } - getProviders() { - return this.twoFactorProvidersData; + getProviders(): Promise> { + return firstValueFrom(this.providers$); } } diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 5a443b784d..94adad8bc7 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -13,6 +13,7 @@ import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enu import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; +import { KdfConfigService } from "../../abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; @@ -47,6 +48,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private platformUtilsService: PlatformUtilsService, + private kdfConfigService: KdfConfigService, ) {} async getAvailableVerificationOptions( @@ -118,8 +120,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } request.masterPasswordHash = alreadyHashed @@ -176,8 +177,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 6609a1014e..79a58f9d57 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileProviderResponse } from "../../admin-console/models/response/pro import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; -import { KeySuffixOptions, KdfType, HashPurpose } from "../enums"; +import { KeySuffixOptions, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -114,16 +114,10 @@ export abstract class CryptoService { * Generates a master key from the provided password * @param password The user's master password * @param email The user's email - * @param kdf The user's selected key derivation function to use * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - abstract makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise; + abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -258,16 +252,10 @@ export abstract class CryptoService { /** * @param pin The user's pin * @param salt The user's salt - * @param kdf The user's kdf * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - abstract makePinKey( - pin: string, - salt: string, - kdf: KdfType, - kdfConfig: KdfConfig, - ): Promise; + abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, @@ -279,7 +267,6 @@ export abstract class CryptoService { * Decrypts the user key with their pin * @param pin The user's PIN * @param salt The user's salt - * @param kdf The user's KDF * @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 @@ -288,7 +275,6 @@ export abstract class CryptoService { abstract decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; @@ -298,7 +284,6 @@ export abstract class CryptoService { * @param masterPasswordOnRestart True if Master Password on Restart is enabled * @param pin User's PIN * @param email User's email - * @param kdf User's KdfType * @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) @@ -308,7 +293,6 @@ export abstract class CryptoService { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise; @@ -358,21 +342,12 @@ export abstract class CryptoService { privateKey: EncString; }>; - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; - /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ abstract decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index 223eb75038..3a6971ba5d 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -1,6 +1,5 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; -import { KdfType } from "../enums"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class KeyGenerationService { @@ -46,14 +45,12 @@ export abstract class KeyGenerationService { * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. * @param salt Salt for the key derivation function. - * @param kdf Key derivation function to use. * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848e..13c33305d1 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,12 +1,10 @@ import { Observable } from "rxjs"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; @@ -149,10 +147,6 @@ export abstract class StateService { */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; - getKdfConfig: (options?: StorageOptions) => Promise; - setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; - getKdfType: (options?: StorageOptions) => Promise; - setKdfType: (value: KdfType, options?: StorageOptions) => Promise; getLastActive: (options?: StorageOptions) => Promise; setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/enums/kdf-type.enum.ts b/libs/common/src/platform/enums/kdf-type.enum.ts index 97157910f5..fd29bf308c 100644 --- a/libs/common/src/platform/enums/kdf-type.enum.ts +++ b/libs/common/src/platform/enums/kdf-type.enum.ts @@ -1,4 +1,4 @@ -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { RangeWithDefault } from "../misc/range-with-default"; export enum KdfType { @@ -12,4 +12,4 @@ export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3); export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256; export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); -export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); +export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue); diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 2f68cf2ce7..d9992adb57 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of, tap } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; @@ -37,6 +38,7 @@ describe("cryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + const kdfConfigService = mock(); let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -58,6 +60,7 @@ describe("cryptoService", () => { stateService, accountService, stateProvider, + kdfConfigService, ); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 3cd443c073..798173f513 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -28,16 +29,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; -import { - KeySuffixOptions, - HashPurpose, - KdfType, - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - EncryptionType, - PBKDF2_ITERATIONS, -} from "../enums"; +import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; import { sequentialize } from "../misc/sequentialize"; import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; @@ -91,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { protected stateService: StateService, protected accountService: AccountService, protected stateProvider: StateProvider, + protected kdfConfigService: KdfConfigService, ) { // User Key this.activeUserKeyState = stateProvider.getActive(USER_KEY); @@ -283,8 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction { return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), )); } @@ -295,16 +287,10 @@ export class CryptoService implements CryptoServiceAbstraction { * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. * TODO: Move to MasterPasswordService */ - async makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise { + async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise { return (await this.keyGenerationService.deriveKeyFromPassword( password, email, - kdf, KdfConfig, )) as MasterKey; } @@ -560,8 +546,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } - async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { - const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig); + async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise { + const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig); return (await this.stretchKey(pinKey)) as PinKey; } @@ -575,7 +561,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedUserKey?: EncString, ): Promise { @@ -584,7 +569,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (!pinProtectedUserKey) { throw new Error("No PIN protected key found."); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey); return new SymmetricCryptoKey(userKey) as UserKey; } @@ -593,7 +578,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedMasterKey?: EncString, ): Promise { @@ -604,7 +588,7 @@ export class CryptoService implements CryptoServiceAbstraction { } pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey); return new SymmetricCryptoKey(masterKey) as MasterKey; } @@ -831,8 +815,7 @@ export class CryptoService implements CryptoServiceAbstraction { const pinKey = await this.makePinKey( pin, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), ); const encPin = await this.encryptService.encrypt(key.key, pinKey); @@ -873,43 +856,6 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void { - switch (kdf) { - case KdfType.PBKDF2_SHA256: - if (!PBKDF2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, - ); - } - break; - case KdfType.Argon2id: - if (!ARGON2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, - ); - } - - if (!ARGON2_MEMORY.inRange(kdfConfig.memory)) { - throw new Error( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, - ); - } - - if (!ARGON2_PARALLELISM.inRange(kdfConfig.parallelism)) { - throw new Error( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, - ); - } - break; - } - } - protected async clearAllStoredUserKeys(userId?: UserId): Promise { await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); @@ -1007,16 +953,15 @@ export class CryptoService implements CryptoServiceAbstraction { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise { // Decrypt - const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdf, kdfConfig, oldPinKey); + 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, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, email, kdfConfig); const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey); if (masterPasswordOnRestart) { await this.stateService.setDecryptedPinProtected(null); diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index b3e0aa6d4e..4f04eebd04 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -1,9 +1,8 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { KdfType } from "../enums"; import { KeyGenerationService } from "./key-generation.service"; @@ -75,12 +74,11 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using pbkdf2", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.PBKDF2_SHA256; - const kdfConfig = new KdfConfig(600_000); + const kdfConfig = new PBKDF2KdfConfig(600_000); cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); @@ -88,13 +86,12 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using argon2id", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.Argon2id; - const kdfConfig = new KdfConfig(600_000, 15); + const kdfConfig = new Argon2KdfConfig(3, 16, 4); cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32)); cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index c592f35e5f..9202b37100 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -46,17 +46,16 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { async deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise { let key: Uint8Array = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { + if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { if (kdfConfig.iterations == null) { kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue; } key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations); - } else if (kdf == KdfType.Argon2id) { + } else if (kdfConfig.kdfType == KdfType.Argon2id) { if (kdfConfig.iterations == null) { kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0c7cdd22d2..cab5768d2a 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; @@ -19,7 +18,7 @@ import { AbstractMemoryStorageService, AbstractStorageService, } from "../abstractions/storage.service"; -import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; +import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; @@ -643,49 +642,6 @@ export class StateService< ); } - async getKdfConfig(options?: StorageOptions): Promise { - const iterations = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfIterations; - const memory = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfMemory; - const parallelism = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfParallelism; - return new KdfConfig(iterations, memory, parallelism); - } - - async setKdfConfig(config: KdfConfig, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfIterations = config.iterations; - account.profile.kdfMemory = config.memory; - account.profile.kdfParallelism = config.parallelism; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKdfType(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfType; - } - - async setKdfType(value: KdfType, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getLastActive(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/deserialization-helpers.spec.ts b/libs/common/src/platform/state/deserialization-helpers.spec.ts new file mode 100644 index 0000000000..b1ae447997 --- /dev/null +++ b/libs/common/src/platform/state/deserialization-helpers.spec.ts @@ -0,0 +1,25 @@ +import { record } from "./deserialization-helpers"; + +describe("deserialization helpers", () => { + describe("record", () => { + it("deserializes a record when keys are strings", () => { + const deserializer = record((value: number) => value); + const input = { + a: 1, + b: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + + it("deserializes a record when keys are numbers", () => { + const deserializer = record((value: number) => value); + const input = { + 1: 1, + 2: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + }); +}); diff --git a/libs/common/src/platform/state/deserialization-helpers.ts b/libs/common/src/platform/state/deserialization-helpers.ts index d68a3d0444..8fd5d2da19 100644 --- a/libs/common/src/platform/state/deserialization-helpers.ts +++ b/libs/common/src/platform/state/deserialization-helpers.ts @@ -21,7 +21,7 @@ export function array( * * @param valueDeserializer */ -export function record( +export function record( valueDeserializer: (value: Jsonify) => T, ): (record: Jsonify>) => Record { return (jsonValue: Jsonify | null>) => { @@ -29,10 +29,10 @@ export function record( return null; } - const output: Record = {}; - for (const key in jsonValue) { - output[key] = valueDeserializer((jsonValue as Record>)[key]); - } + const output: Record = {} as any; + Object.entries(jsonValue).forEach(([key, value]) => { + output[key as TKey] = valueDeserializer(value); + }); return output; }; } diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index b2a8ff8712..bdabd8df50 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -113,7 +113,7 @@ export class KeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index e04110f28b..ee5005202f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,10 +35,12 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const 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", { diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3eb9369080..4c622e29f1 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -120,7 +120,7 @@ export class UserKeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 2d8ef1619e..31bc5460b4 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -55,6 +55,7 @@ import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-maste import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; +import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -62,7 +63,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 58; +export const CURRENT_VERSION = 59; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -122,7 +123,8 @@ export function createMigrationBuilder() { .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) - .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) + .with(KdfConfigMigrator, 58, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts new file mode 100644 index 0000000000..dbce750a7e --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts @@ -0,0 +1,153 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KdfConfigMigrator } from "./59-move-kdf-config-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }, + SecondAccount: { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_kdfConfig_kdfConfig: { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }, + user_SecondAccount_kdfConfig_kdfConfig: { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +describe("KdfConfigMigrator", () => { + let helper: MockProxy; + let sut: KdfConfigMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should remove kdfType and kdfConfig from Account.Profile", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should null out new KdfConfig account value and set account.profile", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, null); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, null); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts new file mode 100644 index 0000000000..332306c6d4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +enum KdfType { + PBKDF2_SHA256 = 0, + Argon2id = 1, +} + +class KdfConfig { + iterations: number; + kdfType: KdfType; + memory?: number; + parallelism?: number; +} + +type ExpectedAccountType = { + profile?: { + kdfIterations: number; + kdfType: KdfType; + kdfMemory?: number; + kdfParallelism?: number; + }; +}; + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +export class KdfConfigMigrator extends Migrator<58, 59> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const iterations = account?.profile?.kdfIterations; + const kdfType = account?.profile?.kdfType; + const memory = account?.profile?.kdfMemory; + const parallelism = account?.profile?.kdfParallelism; + + const kdfConfig: KdfConfig = { + iterations: iterations, + kdfType: kdfType, + memory: memory, + parallelism: parallelism, + }; + + if (kdfConfig != null) { + await helper.setToUser(userId, kdfConfigKeyDefinition, kdfConfig); + delete account?.profile?.kdfIterations; + delete account?.profile?.kdfType; + delete account?.profile?.kdfMemory; + delete account?.profile?.kdfParallelism; + } + + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const kdfConfig: KdfConfig = await helper.getFromUser(userId, kdfConfigKeyDefinition); + + if (kdfConfig != null) { + account.profile.kdfIterations = kdfConfig.iterations; + account.profile.kdfType = kdfConfig.kdfType; + account.profile.kdfMemory = kdfConfig.memory; + account.profile.kdfParallelism = kdfConfig.parallelism; + await helper.setToUser(userId, kdfConfigKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 33b1f28be0..fb67de5501 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,10 +1,10 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; -import { KdfType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -69,8 +69,7 @@ export class SendService implements InternalSendServiceAbstraction { const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, - KdfType.PBKDF2_SHA256, - { iterations: SEND_KDF_ITERATIONS }, + new PBKDF2KdfConfig(SEND_KDF_ITERATIONS), ); send.password = passwordKey.keyB64; } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 9e047b063c..8f9e1abaf1 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -1,4 +1,8 @@ -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; @@ -69,12 +73,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im return false; } - this.key = await this.cryptoService.makePinKey( - password, - jdoc.salt, - jdoc.kdfType, - new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism), - ); + const kdfConfig: KdfConfig = + jdoc.kdfType === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(jdoc.kdfIterations) + : new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism); + + this.key = await this.cryptoService.makePinKey(password, jdoc.salt, kdfConfig); const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts index 1865c94c7d..dd5a210bf8 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts @@ -1,7 +1,7 @@ +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -12,15 +12,14 @@ export class BaseVaultExportService { constructor( protected cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService, + private kdfConfigService: KdfConfigService, ) {} protected async buildPasswordExport(clearText: string, password: string): Promise { - const kdfType: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); - const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); + const key = await this.cryptoService.makePinKey(password, salt, kdfConfig); const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); const encText = await this.cryptoService.encrypt(clearText, key); @@ -29,14 +28,17 @@ export class BaseVaultExportService { encrypted: true, passwordProtected: true, salt: salt, - kdfType: kdfType, + kdfType: kdfConfig.kdfType, kdfIterations: kdfConfig.iterations, - kdfMemory: kdfConfig.memory, - kdfParallelism: kdfConfig.parallelism, encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, data: encText.encryptedString, }; + if (kdfConfig.kdfType === KdfType.Argon2id) { + jsonDoc.kdfMemory = kdfConfig.memory; + jsonDoc.kdfParallelism = kdfConfig.parallelism; + } + return JSON.stringify(jsonDoc, null, " "); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 5f3bd9de52..ee178767f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,9 +1,9 @@ import * as papa from "papaparse"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -32,9 +32,9 @@ export class IndividualVaultExportService private cipherService: CipherService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getExport(format: ExportFormat = "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 2346545231..98baf5dca3 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,10 +1,10 @@ import * as papa from "papaparse"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -36,10 +36,10 @@ export class OrganizationVaultExportService private apiService: ApiService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, private collectionService: CollectionService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getPasswordProtectedExport( diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); });