From 9d10825dbd891c0f41fe1b4c4dd3ca4171f63be5 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Tue, 9 Apr 2024 20:50:20 -0400 Subject: [PATCH] [PM-5362] Add MP Service (attempt #2) (#8619) * create mp and kdf service * update mp service interface to not rely on active user * rename observable methods * update crypto service with new MP service * add master password service to login strategies - make fake service for easier testing - fix crypto service tests * update auth service and finish strategies * auth request refactors * more service refactors and constructor updates * setMasterKey refactors * remove master key methods from crypto service * remove master key and hash from state service * missed fixes * create migrations and fix references * fix master key imports * default force set password reason to none * add password reset reason observable factory to service * remove kdf changes and migrate only disk data * update migration number * fix sync service deps * use disk for force set password state * fix desktop migration * fix sso test * fix tests * fix more tests * fix even more tests * fix even more tests * fix cli * remove kdf service abstraction * add missing deps for browser * fix merge conflicts * clear reset password reason on lock or logout * fix tests * fix other tests * add jsdocs to abstraction * use state provider in crypto service * inverse master password service factory * add clearOn to master password service * add parameter validation to master password service * add component level userId * add missed userId * migrate key hash * fix login strategy service * delete crypto master key from account * migrate master key encrypted user key * rename key hash to master key hash * use mp service for getMasterKeyEncryptedUserKey * fix tests * fix user key decryption logic * add clear methods to mp service * fix circular dep and encryption issue * fix test * remove extra account service call * use EncString in state provider * fix tests * return to using encrypted string for serialization --- .../auth-request-service.factory.ts | 16 +- .../key-connector-service.factory.ts | 9 + .../login-strategy-service.factory.ts | 9 + .../master-password-service.factory.ts | 42 ++++ .../user-verification-service.factory.ts | 9 + apps/browser/src/auth/popup/lock.component.ts | 3 + .../src/auth/popup/set-password.component.ts | 58 +---- apps/browser/src/auth/popup/sso.component.ts | 8 +- .../src/auth/popup/two-factor.component.ts | 6 + .../browser/src/background/main.background.ts | 23 +- .../background/nativeMessaging.background.ts | 16 +- .../vault-timeout-service.factory.ts | 12 + .../crypto-service.factory.ts | 6 + .../services/browser-crypto.service.ts | 3 + apps/cli/src/auth/commands/unlock.command.ts | 17 +- apps/cli/src/bw.ts | 16 +- apps/cli/src/commands/serve.command.ts | 2 + apps/cli/src/program.ts | 4 + apps/desktop/src/app/app.component.ts | 7 +- .../src/app/services/services.module.ts | 2 + apps/desktop/src/auth/lock.component.spec.ts | 6 + apps/desktop/src/auth/lock.component.ts | 3 + .../src/auth/set-password.component.ts | 6 + apps/desktop/src/auth/sso.component.ts | 6 + apps/desktop/src/auth/two-factor.component.ts | 6 + .../services/electron-crypto.service.spec.ts | 4 + .../services/electron-crypto.service.ts | 13 +- .../src/services/native-messaging.service.ts | 6 +- .../user-key-rotation.service.spec.ts | 14 +- .../key-rotation/user-key-rotation.service.ts | 5 +- apps/web/src/app/auth/lock.component.ts | 70 +----- apps/web/src/app/auth/sso.component.ts | 6 + apps/web/src/app/auth/two-factor.component.ts | 6 + libs/angular/jest.config.js | 10 +- .../src/auth/components/lock.component.ts | 17 +- .../auth/components/set-password.component.ts | 24 +- .../src/auth/components/sso.component.spec.ts | 15 +- .../src/auth/components/sso.component.ts | 8 +- .../components/two-factor.component.spec.ts | 16 +- .../auth/components/two-factor.component.ts | 8 +- .../update-temp-password.component.ts | 14 +- libs/angular/src/auth/guards/auth.guard.ts | 12 +- .../src/services/jslib-services.module.ts | 28 ++- .../auth-request-login.strategy.spec.ts | 25 ++- .../auth-request-login.strategy.ts | 22 +- .../login-strategies/login.strategy.spec.ts | 18 +- .../common/login-strategies/login.strategy.ts | 4 + .../password-login.strategy.spec.ts | 26 ++- .../password-login.strategy.ts | 23 +- .../sso-login.strategy.spec.ts | 22 +- .../login-strategies/sso-login.strategy.ts | 17 +- .../user-api-login.strategy.spec.ts | 13 +- .../user-api-login.strategy.ts | 9 +- .../webauthn-login.strategy.spec.ts | 11 +- .../webauthn-login.strategy.ts | 6 + .../auth-request/auth-request.service.spec.ts | 42 +++- .../auth-request/auth-request.service.ts | 18 +- .../login-strategy.service.spec.ts | 17 +- .../login-strategy.service.ts | 19 +- .../master-password.service.abstraction.ts | 82 +++++++ .../services/key-connector.service.spec.ts | 16 +- .../auth/services/key-connector.service.ts | 13 +- .../fake-master-password.service.ts | 64 ++++++ .../master-password.service.ts | 140 ++++++++++++ .../user-verification.service.ts | 16 +- .../platform/abstractions/crypto.service.ts | 31 --- .../platform/abstractions/state.service.ts | 30 --- .../models/domain/account-keys.spec.ts | 7 - .../src/platform/models/domain/account.ts | 10 - .../platform/services/crypto.service.spec.ts | 18 +- .../src/platform/services/crypto.service.ts | 107 ++++----- .../src/platform/services/state.service.ts | 103 --------- .../src/platform/state/state-definitions.ts | 2 + .../vault-timeout.service.spec.ts | 23 +- .../vault-timeout/vault-timeout.service.ts | 10 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-master-key-state-to-provider.spec.ts | 210 ++++++++++++++++++ .../55-move-master-key-state-to-provider.ts | 111 +++++++++ .../src/vault/services/sync/sync.service.ts | 12 +- 79 files changed, 1373 insertions(+), 501 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/master-password-service.factory.ts create mode 100644 libs/common/src/auth/abstractions/master-password.service.abstraction.ts create mode 100644 libs/common/src/auth/services/master-password/fake-master-password.service.ts create mode 100644 libs/common/src/auth/services/master-password/master-password.service.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts diff --git a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts index bd96a211ba..295fedbadd 100644 --- a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts @@ -17,18 +17,21 @@ import { FactoryOptions, factory, } from "../../../platform/background/service-factories/factory-options"; + +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & - ApiServiceInitOptions & - StateServiceInitOptions; + ApiServiceInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -41,9 +44,10 @@ export function authRequestServiceFactory( async () => new AuthRequestService( await appIdServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts index 4a0dd07b32..c602acadae 100644 --- a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts @@ -31,6 +31,11 @@ import { StateProviderInitOptions, } from "../../../platform/background/service-factories/state-provider.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; type KeyConnectorServiceFactoryOptions = FactoryOptions & { @@ -40,6 +45,8 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -58,6 +65,8 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(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 2cc4692ca9..f184072cce 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 @@ -59,6 +59,7 @@ import { PasswordStrengthServiceInitOptions, } from "../../../tools/background/service_factories/password-strength-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { authRequestServiceFactory, AuthRequestServiceInitOptions, @@ -71,6 +72,10 @@ import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, } from "./key-connector-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; import { @@ -81,6 +86,8 @@ import { type LoginStrategyServiceFactoryOptions = FactoryOptions; export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -111,6 +118,8 @@ export function loginStrategyServiceFactory( opts, async () => new LoginStrategyService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts new file mode 100644 index 0000000000..a2f9052a3f --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts @@ -0,0 +1,42 @@ +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type MasterPasswordServiceFactoryOptions = FactoryOptions; + +export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions & + StateProviderInitOptions; + +export function internalMasterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return factory( + cache, + "masterPasswordService", + opts, + async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), + ); +} + +export async function masterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return (await internalMasterPasswordServiceFactory( + cache, + opts, + )) as MasterPasswordServiceAbstraction; +} 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 e8be9099ca..a8b67b21ca 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 @@ -31,6 +31,11 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { userDecryptionOptionsServiceFactory, @@ -46,6 +51,8 @@ type UserVerificationServiceFactoryOptions = FactoryOptions; export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & UserDecryptionOptionsServiceInitOptions & @@ -66,6 +73,8 @@ export function userVerificationServiceFactory( new UserVerificationService( await stateServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f232eca45a..16c32337cf 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -41,6 +42,7 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index ea1cacc7ac..accde2e9a0 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -1,65 +1,9 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -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 { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", }) -export class SetPasswordComponent extends BaseSetPasswordComponent { - constructor( - apiService: ApiService, - i18nService: I18nService, - cryptoService: CryptoService, - messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - policyApiService: PolicyApiServiceAbstraction, - policyService: PolicyService, - router: Router, - syncService: SyncService, - route: ActivatedRoute, - organizationApiService: OrganizationApiServiceAbstraction, - organizationUserService: OrganizationUserService, - userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - dialogService: DialogService, - ) { - super( - i18nService, - cryptoService, - messagingService, - passwordGenerationService, - platformUtilsService, - policyApiService, - policyService, - router, - apiService, - syncService, - route, - stateService, - organizationApiService, - organizationUserService, - userDecryptionOptionsService, - ssoLoginService, - dialogService, - ); - } -} +export class SetPasswordComponent extends BaseSetPasswordComponent {} diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 228c7401fd..14df0d1752 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,7 +9,9 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - protected authService: AuthService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, + private authService: AuthService, @Inject(WINDOW) private win: Window, ) { super( @@ -63,6 +67,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 9bac336695..98363bc93c 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -11,6 +11,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -58,6 +60,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -78,6 +82,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a7fadc6d6f..f649c5a598 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -32,6 +32,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; 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"; @@ -46,6 +47,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; 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"; @@ -242,6 +244,7 @@ export default class MainBackground { keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; + masterPasswordService: InternalMasterPasswordServiceAbstraction; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -480,8 +483,11 @@ export default class MainBackground { const themeStateService = new DefaultThemeStateService(this.globalStateProvider); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -525,6 +531,8 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -578,9 +586,10 @@ export default class MainBackground { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.authService = new AuthService( @@ -597,6 +606,8 @@ export default class MainBackground { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -672,6 +683,8 @@ export default class MainBackground { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -694,6 +707,8 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -729,6 +744,8 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -878,6 +895,8 @@ export default class MainBackground { this.fido2Service, ); this.nativeMessagingBackground = new NativeMessagingBackground( + this.accountService, + this.masterPasswordService, this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, @@ -1107,7 +1126,7 @@ export default class MainBackground { const status = await this.authService.getAuthStatus(userId); const forcePasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: userId })) != + (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 240fb1dede..faf2e6e2cc 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,6 +1,8 @@ import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -71,6 +73,8 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, @@ -336,10 +340,14 @@ export class NativeMessagingBackground { ) as UserKey; await this.cryptoService.setUserKey(userKey); } else if (message.keyB64) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // Backwards compatibility to support cases in which the user hasn't updated their desktop app // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No encrypted user key found"); } @@ -348,9 +356,9 @@ export class NativeMessagingBackground { ) as MasterKey; const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( masterKey, - new EncString(encUserKey), + encUserKey, ); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.cryptoService.setUserKey(userKey); } else { throw new Error("No key received"); diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 0e4d1420da..14f055114b 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,9 +1,17 @@ import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../auth/background/service-factories/master-password-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -57,6 +65,8 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & { }; export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & @@ -79,6 +89,8 @@ export function vaultTimeoutServiceFactory( opts, async () => new VaultTimeoutService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), 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 97614660d1..ed4fde162c 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 { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../../auth/background/service-factories/master-password-service.factory"; import { StateServiceInitOptions, stateServiceFactory, @@ -34,6 +38,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & + MasterPasswordServiceInitOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & @@ -53,6 +58,7 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( + await internalMasterPasswordServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 969dbdf761..d7533a22d6 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 { 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"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -17,6 +18,7 @@ import { UserKey } from "@bitwarden/common/types/key"; export class BrowserCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -28,6 +30,7 @@ export class BrowserCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 98bc926079..d52468139a 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -1,6 +1,10 @@ +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 { 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"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,6 +22,8 @@ import { CliUtils } from "../../utils"; export class UnlockCommand { constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private stateService: StateService, private cryptoFunctionService: CryptoFunctionService, @@ -45,11 +51,14 @@ export class UnlockCommand { const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); - const storedKeyHash = await this.cryptoService.getMasterKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; if (masterKey != null) { - if (storedKeyHash != null) { + if (storedMasterKeyHash != null) { passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey); } else { const serverKeyHash = await this.cryptoService.hashMasterKey( @@ -67,7 +76,7 @@ export class UnlockCommand { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch { // Ignore } @@ -75,7 +84,7 @@ export class UnlockCommand { } if (passwordValid) { - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 0e6571f775..a2e4afe709 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,6 +28,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +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"; @@ -168,6 +169,7 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -352,6 +354,7 @@ export class Main { ); this.cryptoService = new CryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -432,6 +435,8 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -471,9 +476,10 @@ export class Main { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( @@ -481,6 +487,8 @@ export class Main { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -568,6 +576,8 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -578,6 +588,8 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -596,6 +608,8 @@ export class Main { this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 4d0d1e5798..76447f769c 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -122,6 +122,8 @@ export class ServeCommand { this.shareCommand = new ShareCommand(this.main.cipherService); this.lockCommand = new LockCommand(this.main.vaultTimeoutService); this.unlockCommand = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index a79f3847da..fa71a88f54 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -253,6 +253,8 @@ export class Program { if (!cmd.check) { await this.exitIfNotAuthed(); const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, @@ -613,6 +615,8 @@ export class Program { this.processResponse(response, true); } else { const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 884296ea29..b0b411c5f0 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -26,6 +26,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } 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"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -120,6 +121,7 @@ export class AppComponent implements OnInit, OnDestroy { private accountCleanUpInProgress: { [userId: string]: boolean } = {}; constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, private syncService: SyncService, @@ -408,8 +410,9 @@ export class AppComponent implements OnInit, OnDestroy { (await this.authService.getAuthStatus(message.userId)) === AuthenticationStatus.Locked; const forcedPasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: message.userId })) != - ForceSetPasswordReason.None; + (await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(message.userId), + )) != ForceSetPasswordReason.None; if (locked) { this.messagingService.send("locked", { userId: message.userId }); } else if (forcedPasswordReset) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 84932ce7d9..8e412d4977 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,6 +20,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 { 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"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -228,6 +229,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 0339889bf7..c125eba022 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,7 +14,9 @@ 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -52,6 +54,7 @@ describe("LockComponent", () => { let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; let activatedRouteMock: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -67,6 +70,8 @@ describe("LockComponent", () => { activatedRouteMock = mock(); activatedRouteMock.queryParams = mock(); + mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -74,6 +79,7 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8b1448c06f..16b58c5bbe 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -38,6 +39,7 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index a75668a856..93dfe0abd8 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -8,6 +8,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; 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 { 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"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -29,6 +31,8 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; }) export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { constructor( + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, apiService: ApiService, i18nService: I18nService, cryptoService: CryptoService, @@ -50,6 +54,8 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService: DialogService, ) { super( + accountService, + masterPasswordService, i18nService, cryptoService, messagingService, diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 210319b9ed..cc261f1235 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,6 +7,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -39,6 +41,8 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -55,6 +59,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index fdbc52b4bf..d1b84c1fa0 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -11,6 +11,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -60,6 +62,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -79,6 +83,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. 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 04adfcac70..3d9171b52e 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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -30,6 +31,7 @@ describe("electronCryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); @@ -38,9 +40,11 @@ describe("electronCryptoService", () => { beforeEach(() => { accountService = mockAccountServiceWith("userId" as UserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); sut = new ElectronCryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 6b9327a9c4..d113a18200 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 { 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"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -20,6 +21,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export class ElectronCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -31,6 +33,7 @@ export class ElectronCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -159,12 +162,16 @@ export class ElectronCryptoService extends CryptoService { const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); // decrypt const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No user key found during biometric migration"); } - const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); + const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey); // migrate await this.storeBiometricKey(userKey, userId); await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 148e4f1e89..01d9476977 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -30,6 +31,7 @@ export class NativeMessagingService { private sharedSecrets = new Map(); constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, @@ -162,7 +164,9 @@ export class NativeMessagingService { KeySuffixOptions.Biometric, message.userId, ); - const masterKey = await this.cryptoService.getMasterKey(message.userId); + const masterKey = await firstValueFrom( + this.masterPasswordService.masterKey$(message.userId as UserId), + ); if (userKey != null) { // we send the master key still for backwards compatibility 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 09c7bf9ace..0997f18864 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -9,7 +10,6 @@ import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { Send } from "@bitwarden/common/tools/send/models/domain/send"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; @@ -22,6 +22,10 @@ import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + FakeAccountService, + mockAccountServiceWith, +} from "../../../../../../libs/common/spec/fake-account-service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { StateService } from "../../core"; import { EmergencyAccessService } from "../emergency-access"; @@ -46,8 +50,10 @@ describe("KeyRotationService", () => { const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { + mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock(); mockCipherService = mock(); mockFolderService = mock(); @@ -61,6 +67,7 @@ describe("KeyRotationService", () => { mockConfigService = mock(); keyRotationService = new UserKeyRotationService( + mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -174,7 +181,10 @@ describe("KeyRotationService", () => { it("saves the master key in state after creation", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); - expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any); + expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + "mockMasterKey" as any, + mockUserId, + ); }); it("uses legacy rotation if feature flag is off", async () => { 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 03bc604b4d..f5812d341a 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -25,6 +26,7 @@ import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; @Injectable() export class UserKeyRotationService { constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private apiService: UserKeyRotationApiService, private cipherService: CipherService, private folderService: FolderService, @@ -61,7 +63,8 @@ export class UserKeyRotationService { } // Set master key again in case it was lost (could be lost on refresh) - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); if (!newUserKey || !newEncUserKey) { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index a1d4724396..021bf0f9df 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,80 +1,12 @@ -import { Component, NgZone } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component } from "@angular/core"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-lock", templateUrl: "lock.component.html", }) export class LockComponent extends BaseLockComponent { - constructor( - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - messagingService: MessagingService, - cryptoService: CryptoService, - vaultTimeoutService: VaultTimeoutService, - vaultTimeoutSettingsService: VaultTimeoutSettingsService, - environmentService: EnvironmentService, - stateService: StateService, - apiService: ApiService, - logService: LogService, - ngZone: NgZone, - policyApiService: PolicyApiServiceAbstraction, - policyService: InternalPolicyService, - passwordStrengthService: PasswordStrengthServiceAbstraction, - dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, - userVerificationService: UserVerificationService, - pinCryptoService: PinCryptoServiceAbstraction, - biometricStateService: BiometricStateService, - accountService: AccountService, - ) { - super( - router, - i18nService, - platformUtilsService, - messagingService, - cryptoService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustCryptoService, - userVerificationService, - pinCryptoService, - biometricStateService, - accountService, - ); - } - async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index cdd979aa89..e120b2749f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,6 +10,8 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -46,6 +48,8 @@ export class SsoComponent extends BaseSsoComponent { private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -62,6 +66,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 65bf1dba58..eed84b91f1 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -10,6 +10,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -50,6 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -69,6 +73,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index e294e4ff47..c8e748575c 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -10,7 +10,11 @@ module.exports = { displayName: "libs/angular tests", preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), }; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index aa3b801ded..6602a917c9 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -56,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -206,6 +208,7 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); @@ -215,11 +218,13 @@ export class LockComponent implements OnInit, OnDestroy { kdf, kdfConfig, ); - const storedPasswordHash = await this.cryptoService.getMasterKeyHash(); + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; - if (storedPasswordHash != null) { + if (storedMasterKeyHash != null) { // Offline unlock possible passwordValid = await this.cryptoService.compareAndUpdateKeyHash( this.masterPassword, @@ -244,7 +249,7 @@ export class LockComponent implements OnInit, OnDestroy { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch (e) { this.logService.error(e); } finally { @@ -262,7 +267,7 @@ export class LockComponent implements OnInit, OnDestroy { } const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.setUserKeyAndContinue(userKey, true); } @@ -292,8 +297,10 @@ export class LockComponent implements OnInit, OnDestroy { } if (this.requirePasswordChange()) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index a7442f711b..eebf87655b 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -12,6 +12,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; @@ -29,6 +31,7 @@ import { 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"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -45,11 +48,14 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise; successRoute = "vault"; + userId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, @@ -88,7 +94,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.syncService.fullSync(true); this.syncLoading = false; - this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + this.forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(this.userId), + ); this.route.queryParams .pipe( @@ -176,7 +186,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { if (response == null) { throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); } - const userId = await this.stateService.getUserId(); const publicKey = Utils.fromB64ToArray(response.publicKey); // RSA Encrypt user key with organization public key @@ -189,7 +198,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { return this.organizationUserService.putOrganizationUserResetPasswordEnrollment( this.orgId, - userId, + this.userId, resetRequest, ); }); @@ -226,7 +235,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { keyPair: [string, EncString] | null, ) { // Clear force set password reason to allow navigation back to vault. - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + this.userId, + ); // User now has a password so update account decryption options in state const userDecryptionOpts = await firstValueFrom( @@ -237,7 +249,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfConfig(this.kdfConfig); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); // Set private key only for new JIT provisioned users in MP encryption orgs @@ -255,6 +267,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index c5c062d9a7..269ec51e30 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,10 +12,13 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; 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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -23,7 +26,9 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { SsoComponent } from "./sso.component"; // test component that extends the SsoComponent @@ -48,6 +53,7 @@ describe("SsoComponent", () => { let component: TestSsoComponent; let _component: SsoComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -67,6 +73,8 @@ describe("SsoComponent", () => { let mockLogService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -117,6 +125,8 @@ describe("SsoComponent", () => { mockLogService = mock(); mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -199,6 +209,8 @@ describe("SsoComponent", () => { }, { provide: LogService, useValue: mockLogService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -365,8 +377,9 @@ describe("SsoComponent", () => { await _component.logIn(code, codeVerifier, orgIdFromState); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled(); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 68d6e72e8d..30815beef8 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,6 +11,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -66,6 +68,8 @@ export class SsoComponent { protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -290,8 +294,10 @@ export class SsoComponent { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index bff39188ea..0eb248f6d9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -15,11 +15,14 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -27,6 +30,8 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { TwoFactorComponent } from "./two-factor.component"; @@ -46,6 +51,7 @@ describe("TwoFactorComponent", () => { let _component: TwoFactorComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => { let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => { mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -170,6 +180,8 @@ describe("TwoFactorComponent", () => { }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -407,9 +419,9 @@ describe("TwoFactorComponent", () => { await component.doSubmit(); // Assert - - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockRouter.navigate).toHaveBeenCalledTimes(1); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index c306e6cc80..f73f0483be 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,6 +14,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -92,6 +94,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -342,8 +346,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } 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 0b4541fe52..54fdc83239 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -1,9 +1,12 @@ import { Directive } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -56,6 +59,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { super( i18nService, @@ -72,7 +77,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async ngOnInit() { await this.syncService.fullSync(true); - this.reason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId)); // If we somehow end up here without a reason, go back to the home page if (this.reason == ForceSetPasswordReason.None) { @@ -163,7 +169,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { this.i18nService.t("updatedMasterPassword"), ); - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + userId, + ); if (this.onSuccessfulChangePassword != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 29024cfa0b..b8e37d0af3 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Injectable() export class AuthGuard implements CanActivate { @@ -15,7 +17,8 @@ export class AuthGuard implements CanActivate { private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, - private stateService: StateService, + private accountService: AccountService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { @@ -40,7 +43,10 @@ export class AuthGuard implements CanActivate { return this.router.createUrlTree(["/remove-password"]); } - const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(userId), + ); if ( forceSetPasswordReason === diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 73f2bb4a32..ce60271e27 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,6 +60,10 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; @@ -78,6 +82,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; 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"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -359,6 +364,8 @@ const safeProviders: SafeProvider[] = [ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -521,6 +528,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -587,6 +595,8 @@ const safeProviders: SafeProvider[] = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ + InternalMasterPasswordServiceAbstraction, + AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -626,6 +636,8 @@ const safeProviders: SafeProvider[] = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -771,10 +783,21 @@ const safeProviders: SafeProvider[] = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), + safeProvider({ + provide: InternalMasterPasswordServiceAbstraction, + useClass: MasterPasswordService, + deps: [StateProvider], + }), + safeProvider({ + provide: MasterPasswordServiceAbstraction, + useExisting: InternalMasterPasswordServiceAbstraction, + }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -791,6 +814,8 @@ const safeProviders: SafeProvider[] = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -934,9 +959,10 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, - StateServiceAbstraction, ], }), safeProvider({ 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 53722cd259..0ce6c9fed7 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 @@ -5,6 +5,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst 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"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,7 +15,9 @@ 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, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -42,6 +45,10 @@ describe("AuthRequestLoginStrategy", () => { let deviceTrustCryptoService: MockProxy; let billingAccountProfileStateService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let authRequestLoginStrategy: AuthRequestLoginStrategy; let credentials: AuthRequestLoginCredentials; let tokenResponse: IdentityTokenResponse; @@ -71,12 +78,17 @@ describe("AuthRequestLoginStrategy", () => { deviceTrustCryptoService = mock(); billingAccountProfileStateService = mock(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); + tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({}); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -108,13 +120,16 @@ describe("AuthRequestLoginStrategy", () => { const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await authRequestLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + decMasterKeyHash, + mockUserId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); @@ -136,8 +151,8 @@ describe("AuthRequestLoginStrategy", () => { await authRequestLoginStrategy.logIn(credentials); // setMasterKey and setMasterKeyHash should not be called - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); - expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); 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 31a0cebbfe..e47f0f88ee 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 @@ -1,8 +1,10 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -47,6 +49,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -61,6 +65,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -114,8 +120,15 @@ export class AuthRequestLoginStrategy extends LoginStrategy { authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKeyHash ) { - await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey); - await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey( + authRequestCredentials.decryptedMasterKey, + userId, + ); + await this.masterPasswordService.setMasterKeyHash( + authRequestCredentials.decryptedMasterKeyHash, + userId, + ); } } @@ -137,7 +150,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); 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 0ac22047c5..431f736e94 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -14,6 +14,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -31,11 +32,13 @@ import { } 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"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -56,7 +59,7 @@ const privateKey = "PRIVATE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY"; const kdf = 0; const kdfIterations = 10000; -const userId = Utils.newGuid(); +const userId = Utils.newGuid() as UserId; const masterPasswordHash = "MASTER_PASSWORD_HASH"; const name = "NAME"; const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = { @@ -98,6 +101,8 @@ export function identityTokenResponseFactory( // TODO: add tests for latest changes to base class for TDE describe("LoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -118,6 +123,9 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -139,6 +147,8 @@ describe("LoginStrategy", () => { // The base class is abstract so we test it via PasswordLoginStrategy passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -241,7 +251,7 @@ describe("LoginStrategy", () => { }); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); const result = await passwordLoginStrategy.logIn(credentials); @@ -260,7 +270,7 @@ describe("LoginStrategy", () => { cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); @@ -382,6 +392,8 @@ describe("LoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 4fe99b276c..df6aa171db 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,8 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; @@ -60,6 +62,8 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, 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 470a4ac713..b902fff574 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 @@ -9,6 +9,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -19,11 +20,13 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -42,6 +45,7 @@ const masterKey = new SymmetricCryptoKey( "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==", ), ) as MasterKey; +const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const masterPasswordPolicy = new MasterPasswordPolicyResponse({ EnforceOnLogin: true, @@ -50,6 +54,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({ describe("PasswordLoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -102,6 +111,8 @@ describe("PasswordLoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -145,13 +156,16 @@ describe("PasswordLoginStrategy", () => { it("sets keys after a successful authentication", async () => { const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, userId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + localHashedPassword, + userId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); @@ -183,8 +197,9 @@ describe("PasswordLoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); @@ -222,8 +237,9 @@ describe("PasswordLoginStrategy", () => { expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None); // Second login attempt should save the force password reset options and return in result - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); 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 d3de3ea6ba..52c97d5d85 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -1,9 +1,11 @@ -import { BehaviorSubject, map, Observable } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; import { Jsonify } from "type-fest"; 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 { 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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -70,6 +72,8 @@ export class PasswordLoginStrategy extends LoginStrategy { constructor( data: PasswordLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -86,6 +90,8 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -157,8 +163,10 @@ export class PasswordLoginStrategy extends LoginStrategy { }); } else { // Authentication was successful, save the force update password options with the state service - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; } @@ -184,7 +192,8 @@ export class PasswordLoginStrategy extends LoginStrategy { !result.requiresCaptcha && forcePasswordResetReason != ForceSetPasswordReason.None ) { - await this.stateService.setForceSetPasswordReason(forcePasswordResetReason); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId); result.forcePasswordReset = forcePasswordResetReason; } @@ -193,8 +202,9 @@ export class PasswordLoginStrategy extends LoginStrategy { protected override async setMasterKey(response: IdentityTokenResponse) { const { masterKey, localMasterKeyHash } = this.cache.value; - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); } protected override async setUserKey(response: IdentityTokenResponse): Promise { @@ -204,7 +214,8 @@ export class PasswordLoginStrategy extends LoginStrategy { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); 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 d4b0b13eaf..bce62681d0 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 @@ -9,6 +9,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -20,7 +21,9 @@ 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, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; import { @@ -33,6 +36,9 @@ import { identityTokenResponseFactory } from "./login.strategy.spec"; import { SsoLoginStrategy } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -52,6 +58,7 @@ describe("SsoLoginStrategy", () => { let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -61,6 +68,9 @@ describe("SsoLoginStrategy", () => { const ssoOrgId = "SSO_ORG_ID"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -83,6 +93,8 @@ describe("SsoLoginStrategy", () => { ssoLoginStrategy = new SsoLoginStrategy( null, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -130,7 +142,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setPrivateKey).not.toHaveBeenCalled(); }); @@ -395,7 +407,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -422,7 +434,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); @@ -446,7 +458,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -473,7 +485,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); 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 7745104bd1..db0228a338 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -1,9 +1,11 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; 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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -79,6 +81,8 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -96,6 +100,8 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -138,7 +144,11 @@ export class SsoLoginStrategy extends LoginStrategy { // Auth guard currently handles redirects for this. if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ssoAuthResult.forcePasswordReset, + userId, + ); } this.cache.next({ @@ -323,7 +333,8 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); // There is a scenario in which the master key is not set here. That will occur if the user // has a master password and is using Key Connector. In that case, we cannot set the master key 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 02aed305a4..5e7d7985b1 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 @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -19,7 +20,9 @@ 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, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -30,6 +33,8 @@ import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login describe("UserApiLoginStrategy", () => { let cache: UserApiLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; @@ -48,12 +53,16 @@ describe("UserApiLoginStrategy", () => { let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; const apiClientId = "API_CLIENT_ID"; const apiClientSecret = "API_CLIENT_SECRET"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -74,6 +83,8 @@ describe("UserApiLoginStrategy", () => { apiLogInStrategy = new UserApiLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -172,7 +183,7 @@ describe("UserApiLoginStrategy", () => { environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await apiLogInStrategy.logIn(credentials); 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 2af666f95c..421746b49c 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 @@ -2,7 +2,9 @@ import { firstValueFrom, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; @@ -39,6 +41,8 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -95,7 +101,8 @@ export class UserApiLoginStrategy extends LoginStrategy { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); 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 edc1441361..1d96921286 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 @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -16,6 +17,7 @@ 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 { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -26,6 +28,8 @@ import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-log describe("WebAuthnLoginStrategy", () => { let cache: WebAuthnLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService!: MockProxy; let apiService!: MockProxy; @@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); + accountService = new FakeAccountService(null); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -81,6 +88,8 @@ describe("WebAuthnLoginStrategy", () => { webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -207,7 +216,7 @@ describe("WebAuthnLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); // Master key and private key should not be set - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); }); it("does not try to set the user key when prfKey is missing", async () => { 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 a8e67597b8..843978e2a2 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,6 +2,8 @@ import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -41,6 +43,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 80d00b2a01..f04628ffd9 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -2,13 +2,15 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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 { 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"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestService } from "./auth-request.service"; @@ -16,17 +18,27 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const cryptoService = mock(); const apiService = mock(); - const stateService = mock(); let mockPrivateKey: Uint8Array; + const mockUserId = Utils.newGuid() as UserId; beforeEach(() => { jest.clearAllMocks(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); - sut = new AuthRequestService(appIdService, cryptoService, apiService, stateService); + sut = new AuthRequestService( + appIdService, + accountService, + masterPasswordService, + cryptoService, + apiService, + ); mockPrivateKey = new Uint8Array(64); }); @@ -67,8 +79,8 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - cryptoService.getMasterKey.mockResolvedValueOnce({ encKey: new Uint8Array(64) } as MasterKey); - stateService.getKeyHash.mockResolvedValueOnce("KEY_HASH"); + masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( true, @@ -130,8 +142,8 @@ describe("AuthRequestService", () => { masterKeyHash: mockDecryptedMasterKeyHash, }); - cryptoService.setMasterKey.mockResolvedValueOnce(undefined); - cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined); + masterPasswordService.masterKeySubject.next(undefined); + masterPasswordService.masterKeyHashSubject.next(undefined); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey); cryptoService.setUserKey.mockResolvedValueOnce(undefined); @@ -144,10 +156,18 @@ describe("AuthRequestService", () => { mockAuthReqResponse.masterPasswordHash, mockPrivateKey, ); - expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash); - expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + mockUserId, + ); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + mockDecryptedMasterKeyHash, + mockUserId, + ); + expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); }); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index eb39659f53..5f8dcfd729 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,12 +1,13 @@ -import { Observable, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -19,9 +20,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, - private stateService: StateService, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } @@ -38,8 +40,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { } const pubKey = Utils.fromB64ToArray(authRequest.publicKey); - const masterKey = await this.cryptoService.getMasterKey(); - const masterKeyHash = await this.stateService.getKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); let encryptedMasterKeyHash; let keyToEncrypt; @@ -92,8 +95,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(masterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); await this.cryptoService.setUserKey(userKey); } 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 981e4d81ac..fcc0220d0a 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 @@ -11,6 +11,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,8 +23,14 @@ 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 { KdfType } from "@bitwarden/common/platform/enums"; -import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; +import { + FakeAccountService, + FakeGlobalState, + FakeGlobalStateProvider, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { AuthRequestServiceAbstraction, @@ -38,6 +45,8 @@ import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; describe("LoginStrategyService", () => { let sut: LoginStrategyService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -61,7 +70,11 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; + const userId = "USER_ID" as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -84,6 +97,8 @@ describe("LoginStrategyService", () => { stateProvider = new FakeGlobalStateProvider(); sut = new LoginStrategyService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, 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 b55f38af7f..a8bd7bc2ff 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 @@ -9,8 +9,10 @@ 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; 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"; @@ -81,6 +83,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -257,7 +261,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise { const pubKey = Utils.fromB64ToArray(key); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); let keyToEncrypt; let encryptedMasterKeyHash = null; @@ -266,7 +271,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // Only encrypt the master password hash if masterKey exists as // we won't have a masterKeyHash without a masterKey - const masterKeyHash = await this.stateService.getKeyHash(); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); if (masterKeyHash != null) { encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt( Utils.fromUtf8ToArray(masterKeyHash), @@ -333,6 +338,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Password: return new PasswordLoginStrategy( data?.password, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -351,6 +358,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Sso: return new SsoLoginStrategy( data?.sso, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -370,6 +379,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( data?.userApiKey, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -387,6 +398,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( data?.authRequest, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -403,6 +416,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( data?.webAuthn, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, diff --git a/libs/common/src/auth/abstractions/master-password.service.abstraction.ts b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts new file mode 100644 index 0000000000..b36c8bfaae --- /dev/null +++ b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts @@ -0,0 +1,82 @@ +import { Observable } from "rxjs"; + +import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; +import { MasterKey } from "../../types/key"; +import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason"; + +export abstract class MasterPasswordServiceAbstraction { + /** + * An observable that emits if the user is being forced to set a password on login and why. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract forceSetPasswordReason$: (userId: UserId) => Observable; + /** + * An observable that emits the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKey$: (userId: UserId) => Observable; + /** + * An observable that emits the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKeyHash$: (userId: UserId) => Observable; + /** + * Returns the master key encrypted user key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise; +} + +export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { + /** + * Set the master key for the user. + * Note: Use {@link clearMasterKey} to clear the master key. + * @param masterKey The master key. + * @param userId The user ID. + * @throws If the user ID or master key is missing. + */ + abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise; + /** + * Clear the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKey: (userId: UserId) => Promise; + /** + * Set the master key hash for the user. + * Note: Use {@link clearMasterKeyHash} to clear the master key hash. + * @param masterKeyHash The master key hash. + * @param userId The user ID. + * @throws If the user ID or master key hash is missing. + */ + abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise; + /** + * Clear the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKeyHash: (userId: UserId) => Promise; + + /** + * Set the master key encrypted user key for the user. + * @param encryptedKey The master key encrypted user key. + * @param userId The user ID. + * @throws If the user ID or encrypted key is missing. + */ + abstract setMasterKeyEncryptedUserKey: (encryptedKey: EncString, userId: UserId) => Promise; + /** + * Set the force set password reason for the user. + * @param reason The reason the user is being forced to set a password. + * @param userId The user ID. + * @throws If the user ID or reason is missing. + */ + abstract setForceSetPasswordReason: ( + reason: ForceSetPasswordReason, + userId: UserId, + ) => Promise; +} diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 50fed856f9..e3e5fbdbe7 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -21,6 +21,7 @@ import { CONVERT_ACCOUNT_TO_KEY_CONNECTOR, KeyConnectorService, } from "./key-connector.service"; +import { FakeMasterPasswordService } from "./master-password/fake-master-password.service"; import { TokenService } from "./token.service"; describe("KeyConnectorService", () => { @@ -36,6 +37,7 @@ describe("KeyConnectorService", () => { let stateProvider: FakeStateProvider; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const mockOrgId = Utils.newGuid() as OrganizationId; @@ -47,10 +49,13 @@ describe("KeyConnectorService", () => { beforeEach(() => { jest.clearAllMocks(); + masterPasswordService = new FakeMasterPasswordService(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); keyConnectorService = new KeyConnectorService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -214,7 +219,10 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + masterKey, + expect.any(String), + ); }); it("should handle errors thrown during the process", async () => { @@ -241,10 +249,10 @@ describe("KeyConnectorService", () => { // Arrange const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); + masterPasswordService.masterKeySubject.next(masterKey); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); - jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act @@ -252,7 +260,6 @@ describe("KeyConnectorService", () => { // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, @@ -268,8 +275,8 @@ describe("KeyConnectorService", () => { const error = new Error("Failed to post user key to key connector"); organizationService.getAll.mockResolvedValue([organization]); + masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); - jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); jest.spyOn(logService, "error"); @@ -280,7 +287,6 @@ describe("KeyConnectorService", () => { // Assert expect(logService.error).toHaveBeenCalledWith(error); expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index d1502ce06c..f8e523cce4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -16,7 +16,9 @@ import { UserKeyDefinition, } from "../../platform/state"; import { MasterKey } from "../../types/key"; +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 { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; @@ -45,6 +47,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private usesKeyConnectorState: ActiveUserState; private convertAccountToKeyConnectorState: ActiveUserState; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -78,7 +82,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { async migrateUser() { const organization = await this.getManagingOrganization(); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); try { @@ -99,7 +104,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url); const keyArr = Utils.fromB64ToArray(masterKeyResponse.key); const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); } catch (e) { this.handleKeyConnectorError(e); } @@ -136,7 +142,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.makeUserKey(masterKey); await this.cryptoService.setUserKey(userKey[0]); diff --git a/libs/common/src/auth/services/master-password/fake-master-password.service.ts b/libs/common/src/auth/services/master-password/fake-master-password.service.ts new file mode 100644 index 0000000000..dd034ec50b --- /dev/null +++ b/libs/common/src/auth/services/master-password/fake-master-password.service.ts @@ -0,0 +1,64 @@ +import { mock } from "jest-mock-extended"; +import { ReplaySubject, Observable } from "rxjs"; + +import { EncString } from "../../../platform/models/domain/enc-string"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction { + mock = mock(); + + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeySubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeyHashSubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + forceSetPasswordReasonSubject = new ReplaySubject(1); + + constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { + this.masterKeySubject.next(initialMasterKey); + this.masterKeyHashSubject.next(initialMasterKeyHash); + } + + masterKey$(userId: UserId): Observable { + return this.masterKeySubject.asObservable(); + } + + setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + return this.mock.setMasterKey(masterKey, userId); + } + + clearMasterKey(userId: UserId): Promise { + return this.mock.clearMasterKey(userId); + } + + masterKeyHash$(userId: UserId): Observable { + return this.masterKeyHashSubject.asObservable(); + } + + getMasterKeyEncryptedUserKey(userId: UserId): Promise { + return this.mock.getMasterKeyEncryptedUserKey(userId); + } + + setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); + } + + setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + return this.mock.setMasterKeyHash(masterKeyHash, userId); + } + + clearMasterKeyHash(userId: UserId): Promise { + return this.mock.clearMasterKeyHash(userId); + } + + forceSetPasswordReason$(userId: UserId): Observable { + return this.forceSetPasswordReasonSubject.asObservable(); + } + + setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + return this.mock.setForceSetPasswordReason(reason, userId); + } +} diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts new file mode 100644 index 0000000000..fad48abc12 --- /dev/null +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -0,0 +1,140 @@ +import { firstValueFrom, map, Observable } from "rxjs"; + +import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { + MASTER_PASSWORD_DISK, + MASTER_PASSWORD_MEMORY, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +/** Memory since master key shouldn't be available on lock */ +const MASTER_KEY = new UserKeyDefinition(MASTER_PASSWORD_MEMORY, "masterKey", { + deserializer: (masterKey) => SymmetricCryptoKey.fromJSON(masterKey) as MasterKey, + clearOn: ["lock", "logout"], +}); + +/** Disk since master key hash is used for unlock */ +const MASTER_KEY_HASH = new UserKeyDefinition(MASTER_PASSWORD_DISK, "masterKeyHash", { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], +}); + +/** Disk to persist through lock */ +const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "masterKeyEncryptedUserKey", + { + deserializer: (key) => key, + clearOn: ["logout"], + }, +); + +/** Disk to persist through lock and account switches */ +const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "forceSetPasswordReason", + { + deserializer: (reason) => reason, + clearOn: ["logout"], + }, +); + +export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + + masterKey$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY).state$; + } + + masterKeyHash$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; + } + + forceSetPasswordReason$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider + .getUser(userId, FORCE_SET_PASSWORD_REASON) + .state$.pipe(map((reason) => reason ?? ForceSetPasswordReason.None)); + } + + // TODO: Remove this method and decrypt directly in the service instead + async getMasterKeyEncryptedUserKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + const key = await firstValueFrom( + this.stateProvider.getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY).state$, + ); + return EncString.fromJSON(key); + } + + async setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + if (masterKey == null) { + throw new Error("Master key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => masterKey); + } + + async clearMasterKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => null); + } + + async setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + if (masterKeyHash == null) { + throw new Error("Master key hash is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => masterKeyHash); + } + + async clearMasterKeyHash(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => null); + } + + async setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + if (encryptedKey == null) { + throw new Error("Encrypted Key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider + .getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY) + .update((_) => encryptedKey.toJSON() as EncryptedString); + } + + async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + if (reason == null) { + throw new Error("Reason is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); + } +} 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 0b4cd96099..5a443b784d 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 @@ -10,7 +10,10 @@ import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; +import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; +import { AccountService } from "../../abstractions/account.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"; import { VerificationType } from "../../enums/verification-type"; @@ -35,6 +38,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti constructor( private stateService: StateService, private cryptoService: CryptoService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, @@ -107,7 +112,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti if (verification.type === VerificationType.OTP) { request.otp = verification.secret; } else { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey && !alreadyHashed) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -164,7 +170,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -181,7 +188,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti throw new Error(this.i18nService.t("invalidMasterPassword")); } // TODO: we should re-evaluate later on if user verification should have the side effect of modifying state. Probably not. - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); return true; } @@ -230,9 +237,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; return ( (await this.hasMasterPassword(userId)) && - (await this.cryptoService.getMasterKeyHash()) != null + (await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId as UserId))) != null ); } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index ed451fd896..6609a1014e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -105,18 +105,6 @@ export abstract class CryptoService { * @param userId The desired user */ abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; - /** - * Sets the user's master key - * @param key The user's master key to set - * @param userId The desired user - */ - abstract setMasterKey(key: MasterKey, userId?: string): Promise; - /** - * @param userId The desired user - * @returns The user's master key - */ - abstract getMasterKey(userId?: string): Promise; - /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user @@ -136,11 +124,6 @@ export abstract class CryptoService { kdf: KdfType, KdfConfig: KdfConfig, ): Promise; - /** - * Clears the user's master key - * @param userId The desired user - */ - abstract clearMasterKey(userId?: string): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -178,20 +161,6 @@ export abstract class CryptoService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise; - /** - * Sets the user's master password hash - * @param keyHash The user's master password hash to set - */ - abstract setMasterKeyHash(keyHash: string): Promise; - /** - * @returns The user's master password hash - */ - abstract getMasterKeyHash(): Promise; - /** - * Clears the user's stored master password hash - * @param userId The desired user - */ - abstract clearMasterKeyHash(userId?: string): Promise; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4971481381..227cb43879 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,14 +1,12 @@ import { Observable } from "rxjs"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; 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 { MasterKey } from "../../types/key"; import { CipherData } from "../../vault/models/data/cipher.data"; import { LocalData } from "../../vault/models/data/local.data"; import { CipherView } from "../../vault/models/view/cipher.view"; @@ -17,7 +15,6 @@ import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; /** * Options for customizing the initiation behavior. @@ -48,22 +45,6 @@ export abstract class StateService { getAddEditCipherInfo: (options?: StorageOptions) => Promise; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; - /** - * Gets the user's master key - */ - getMasterKey: (options?: StorageOptions) => Promise; - /** - * Sets the user's master key - */ - setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise; - /** - * Gets the user key encrypted by the master key - */ - getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise; - /** - * Sets the user key encrypted by the master key - */ - setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise; /** * Gets the user's auto key */ @@ -108,10 +89,6 @@ export abstract class StateService { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - /** - * @deprecated For legacy purposes only, use getMasterKey instead - */ - getCryptoMasterKey: (options?: StorageOptions) => Promise; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -189,18 +166,11 @@ export abstract class StateService { setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; getEverBeenUnlocked: (options?: StorageOptions) => Promise; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; - getForceSetPasswordReason: (options?: StorageOptions) => Promise; - setForceSetPasswordReason: ( - value: ForceSetPasswordReason, - 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; - getKeyHash: (options?: StorageOptions) => Promise; - setKeyHash: (value: string, options?: StorageOptions) => Promise; getLastActive: (options?: StorageOptions) => Promise; setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account-keys.spec.ts b/libs/common/src/platform/models/domain/account-keys.spec.ts index 4a96da1b48..6bdb08edd5 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -2,7 +2,6 @@ import { makeStaticByteArray } from "../../../../spec"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -32,12 +31,6 @@ describe("AccountKeys", () => { expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello")); }); - it("should deserialize cryptoMasterKey", () => { - const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); - AccountKeys.fromJSON({} as any); - expect(spy).toHaveBeenCalled(); - }); - it("should deserialize privateKey", () => { const spy = jest.spyOn(EncryptionPair, "fromJSON"); AccountKeys.fromJSON({ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 4ed36fd389..753b15c09b 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,7 +1,6 @@ import { Jsonify } from "type-fest"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -10,7 +9,6 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { DeepJsonify } from "../../../types/deep-jsonify"; -import { MasterKey } from "../../../types/key"; import { CipherData } from "../../../vault/models/data/cipher.data"; import { CipherView } from "../../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; @@ -90,12 +88,8 @@ export class AccountData { } export class AccountKeys { - masterKey?: MasterKey; - masterKeyEncryptedUserKey?: string; publicKey?: Uint8Array; - /** @deprecated July 2023, left for migration purposes*/ - cryptoMasterKey?: SymmetricCryptoKey; /** @deprecated July 2023, left for migration purposes*/ cryptoMasterKeyAuto?: string; /** @deprecated July 2023, left for migration purposes*/ @@ -120,8 +114,6 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { - masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -150,10 +142,8 @@ export class AccountProfile { email?: string; emailVerified?: boolean; everBeenUnlocked?: boolean; - forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index c17d3f97d2..6d0fdb1423 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -5,6 +5,7 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey, MasterKey, PinKey } from "../../types/key"; @@ -41,12 +42,15 @@ describe("cryptoService", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); cryptoService = new CryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -158,14 +162,14 @@ describe("cryptoService", () => { describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; - let stateSvcGetMasterKey: jest.SpyInstance; + let getMasterKey: jest.SpyInstance; beforeEach(() => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; - stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey"); + getMasterKey = jest.spyOn(masterPasswordService, "masterKey$"); }); it("returns the User Key if available", async () => { @@ -175,17 +179,17 @@ describe("cryptoService", () => { const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); expect(getKeySpy).toHaveBeenCalledWith(mockUserId); - expect(stateSvcGetMasterKey).not.toHaveBeenCalled(); + expect(getMasterKey).not.toHaveBeenCalled(); expect(userKey).toEqual(mockUserKey); }); it("returns the user's master key when User Key is not available", async () => { - stateSvcGetMasterKey.mockResolvedValue(mockMasterKey); + masterPasswordService.masterKeySubject.next(mockMasterKey); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); - expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId }); + expect(getMasterKey).toHaveBeenCalledWith(mockUserId); expect(userKey).toEqual(mockMasterKey); }); }); @@ -340,9 +344,7 @@ describe("cryptoService", () => { describe("clearKeys", () => { it("resolves active user id when called with no user id", async () => { let callCount = 0; - accountService.activeAccount$ = accountService.activeAccountSubject.pipe( - tap(() => callCount++), - ); + stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); await cryptoService.clearKeys(null); expect(callCount).toBe(1); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index df7528b13c..ae588cbc31 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 { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -82,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable; constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -181,12 +183,16 @@ export class CryptoService implements CryptoServiceAbstraction { } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { - return await this.validateUserKey( - (masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey, - ); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + + return await this.validateUserKey(masterKey as unknown as UserKey); } + // TODO: legacy support for user key is no longer needed since we require users to migrate on login async getUserKeyWithLegacySupport(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -194,7 +200,8 @@ export class CryptoService implements CryptoServiceAbstraction { // Legacy support: encryption used to be done with the master key (derived from master password). // Users who have not migrated will have a null user key and must use the master key instead. - return (await this.getMasterKey(userId)) as unknown as UserKey; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + return masterKey as unknown as UserKey; } async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise { @@ -233,7 +240,10 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { - masterKey ||= await this.getMasterKey(); + if (!masterKey) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (masterKey == null) { throw new Error("No Master Key found."); } @@ -277,28 +287,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise { - await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); - } - - async setMasterKey(key: MasterKey, userId?: UserId): Promise { - await this.stateService.setMasterKey(key, { userId: userId }); - } - - async getMasterKey(userId?: UserId): Promise { - let masterKey = await this.stateService.getMasterKey({ userId: userId }); - if (!masterKey) { - masterKey = (await this.stateService.getCryptoMasterKey({ userId: userId })) as MasterKey; - // if master key was null/undefined and getCryptoMasterKey also returned null/undefined, - // don't set master key as it is unnecessary - if (masterKey) { - await this.setMasterKey(masterKey, userId); - } - } - return masterKey; + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKeyEncryptedUserKey( + new EncString(userKeyMasterKey), + userId, + ); } + // TODO: Move to MasterPasswordService async getOrDeriveMasterKey(password: string, userId?: UserId) { - let masterKey = await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), @@ -312,6 +311,7 @@ export class CryptoService implements CryptoServiceAbstraction { * * @remarks * 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, @@ -327,10 +327,6 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } - async clearMasterKey(userId?: UserId): Promise { - await this.stateService.setMasterKey(null, { userId: userId }); - } - async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -339,32 +335,28 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.buildProtectedSymmetricKey(masterKey, userKey.key); } + // TODO: move to master password service async decryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: EncString, userId?: UserId, ): Promise { - masterKey ||= await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey == null) { throw new Error("No master key found."); } - if (!userKey) { - let masterKeyEncryptedUserKey = await this.stateService.getMasterKeyEncryptedUserKey({ + // Try one more way to get the user key if it still wasn't found. + if (userKey == null) { + const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId, }); - - // Try one more way to get the user key if it still wasn't found. - if (masterKeyEncryptedUserKey == null) { - masterKeyEncryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - } - - if (masterKeyEncryptedUserKey == null) { + if (deprecatedKey == null) { throw new Error("No encrypted user key found."); } - userKey = new EncString(masterKeyEncryptedUserKey); + userKey = new EncString(deprecatedKey); } let decUserKey: Uint8Array; @@ -383,12 +375,16 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(decUserKey) as UserKey; } + // TODO: move to MasterPasswordService async hashMasterKey( password: string, key: MasterKey, hashPurpose?: HashPurpose, ): Promise { - key ||= await this.getMasterKey(); + if (!key) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (password == null || key == null) { throw new Error("Invalid parameters."); @@ -399,20 +395,12 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async setMasterKeyHash(keyHash: string): Promise { - await this.stateService.setKeyHash(keyHash); - } - - async getMasterKeyHash(): Promise { - return await this.stateService.getKeyHash(); - } - - async clearMasterKeyHash(userId?: UserId): Promise { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - + // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise { - const storedPasswordHash = await this.getMasterKeyHash(); + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const storedPasswordHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); if (masterPassword != null && storedPasswordHash != null) { const localKeyHash = await this.hashMasterKey( masterPassword, @@ -430,7 +418,7 @@ export class CryptoService implements CryptoServiceAbstraction { HashPurpose.ServerAuthorization, ); if (serverKeyHash != null && storedPasswordHash === serverKeyHash) { - await this.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); return true; } } @@ -652,14 +640,14 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise { - userId ||= (await firstValueFrom(this.accountService.activeAccount$))?.id; + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); if (userId == null) { throw new Error("Cannot clear keys, no user Id resolved."); } + await this.masterPasswordService.clearMasterKeyHash(userId); await this.clearUserKey(userId); - await this.clearMasterKeyHash(userId); await this.clearOrgKeys(userId); await this.clearProviderKeys(userId); await this.clearKeyPair(userId); @@ -1014,7 +1002,8 @@ export class CryptoService implements CryptoServiceAbstraction { if (await this.isLegacyUser(masterKey, userId)) { // Legacy users don't have a user key, so no need to migrate. // Instead, set the master key for additional isLegacyUser checks that will log the user out. - await this.setMasterKey(masterKey, userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKey(masterKey, userId); return; } const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index a35659a7ac..b3e33cf362 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -5,14 +5,12 @@ import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; 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 { MasterKey } from "../../types/key"; import { CipherData } from "../../vault/models/data/cipher.data"; import { LocalData } from "../../vault/models/data/local.data"; import { CipherView } from "../../vault/models/view/cipher.view"; @@ -35,7 +33,6 @@ import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; import { State } from "../models/domain/state"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { MigrationRunner } from "./migration-runner"; @@ -273,65 +270,6 @@ export class StateService< ); } - /** - * @deprecated Do not save the Master Key. Use the User Symmetric Key instead - */ - async getCryptoMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.cryptoMasterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async getMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.masterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async setMasterKey(value: MasterKey, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.keys.masterKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async getMasterKeyEncryptedUserKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys.masterKeyEncryptedUserKey; - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async setMasterKeyEncryptedUserKey(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.masterKeyEncryptedUserKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - /** * user key when using the "never" option of vault timeout */ @@ -823,30 +761,6 @@ export class StateService< ); } - async getForceSetPasswordReason(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ) - )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None - ); - } - - async setForceSetPasswordReason( - value: ForceSetPasswordReason, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.profile.forceSetPasswordReason = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -897,23 +811,6 @@ export class StateService< ); } - async getKeyHash(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.keyHash = 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/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index d9265cf10c..10c2f3d36d 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -37,6 +37,8 @@ export const BILLING_DISK = new StateDefinition("billing", "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 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/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index f20bc910c0..0594de741c 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -1,17 +1,21 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { Account } from "../../platform/models/domain/account"; import { StateEventRunnerService } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -19,6 +23,8 @@ import { FolderService } from "../../vault/abstractions/folder/folder.service.ab import { VaultTimeoutService } from "./vault-timeout.service"; describe("VaultTimeoutService", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cipherService: MockProxy; let folderService: MockProxy; let collectionService: MockProxy; @@ -39,7 +45,11 @@ describe("VaultTimeoutService", () => { let vaultTimeoutService: VaultTimeoutService; + const userId = Utils.newGuid() as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cipherService = mock(); folderService = mock(); collectionService = mock(); @@ -66,6 +76,8 @@ describe("VaultTimeoutService", () => { availableVaultTimeoutActionsSubject = new BehaviorSubject([]); vaultTimeoutService = new VaultTimeoutService( + accountService, + masterPasswordService, cipherService, folderService, collectionService, @@ -123,6 +135,15 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject(globalSetups?.userId); + if (globalSetups?.userId) { + accountService.activeAccountSubject.next({ + id: globalSetups.userId as UserId, + status: accounts[globalSetups.userId]?.authStatus, + email: null, + name: null, + }); + } + platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -156,7 +177,7 @@ describe("VaultTimeoutService", () => { expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); - expect(cryptoService.clearMasterKey).toHaveBeenCalledWith(userId); + expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).toHaveBeenCalledWith(userId); expect(lockedCallback).toHaveBeenCalledWith(userId); }; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 22d658c552..72252036c8 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -3,7 +3,9 @@ import { firstValueFrom, timeout } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { ClientType } from "../../enums"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; @@ -21,6 +23,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private inited = false; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, @@ -84,7 +88,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = await this.stateService.getUserId(); + const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (userId == null || userId === currentUserId) { this.searchService.clearIndex(); @@ -92,12 +96,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.collectionService.clearActiveUserCache(); } + await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); + await this.stateService.setEverBeenUnlocked(true, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.cryptoService.clearMasterKey(userId); - await this.cipherService.clearCache(userId); await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index faccddb0af..76f0d7fd46 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -51,6 +51,7 @@ import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-t import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; import { SendMigrator } from "./migrations/54-move-encrypted-sends"; +import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-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"; @@ -58,7 +59,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 54; +export const CURRENT_VERSION = 55; export type MinVersion = typeof MIN_VERSION; @@ -115,7 +116,8 @@ export function createMigrationBuilder() { .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) - .with(SendMigrator, 53, 54); + .with(SendMigrator, 53, 54) + .with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts new file mode 100644 index 0000000000..bbf0352e95 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts @@ -0,0 +1,210 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + FORCE_SET_PASSWORD_REASON_DEFINITION, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + MASTER_KEY_HASH_DEFINITION, + MoveMasterKeyStateToProviderMigrator, +} from "./55-move-master-key-state-to-provider"; + +function preMigrationState() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +function postMigrationState() { + return { + user_FirstAccount_masterPassword_forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + user_FirstAccount_masterPassword_masterKeyHash: "FirstAccount_keyHash", + user_FirstAccount_masterPassword_masterKeyEncryptedUserKey: + "FirstAccount_masterKeyEncryptedUserKey", + user_SecondAccount_masterPassword_forceSetPasswordReason: + "SecondAccount_forceSetPasswordReason", + user_SecondAccount_masterPassword_masterKeyHash: "SecondAccount_keyHash", + user_SecondAccount_masterPassword_masterKeyEncryptedUserKey: + "SecondAccount_masterKeyEncryptedUserKey", + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +describe("MoveForceSetPasswordReasonToStateProviderMigrator", () => { + let helper: MockProxy; + let sut: MoveMasterKeyStateToProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationState(), 54); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it("should remove properties from existing accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "overStuff2", + }, + keys: {}, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + keys: {}, + otherStuff: "otherStuff5", + }); + }); + + it("should set properties for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "FirstAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_HASH_DEFINITION, + "FirstAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "FirstAccount_masterKeyEncryptedUserKey", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "SecondAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_HASH_DEFINITION, + "SecondAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "SecondAccount_masterKeyEncryptedUserKey", + ); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(postMigrationState(), 55); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it.each(["FirstAccount", "SecondAccount"])("should null out new values", async (userId) => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + null, + ); + + expect(helper.setToUser).toHaveBeenCalledWith(userId, MASTER_KEY_HASH_DEFINITION, null); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts new file mode 100644 index 0000000000..99b22b5661 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + keys?: { + masterKeyEncryptedUserKey?: string; + }; + profile?: { + forceSetPasswordReason?: number; + keyHash?: string; + }; +}; + +export const FORCE_SET_PASSWORD_REASON_DEFINITION: KeyDefinitionLike = { + key: "forceSetPasswordReason", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_HASH_DEFINITION: KeyDefinitionLike = { + key: "masterKeyHash", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION: KeyDefinitionLike = { + key: "masterKeyEncryptedUserKey", + stateDefinition: { + name: "masterPassword", + }, +}; + +export class MoveMasterKeyStateToProviderMigrator extends Migrator<54, 55> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const forceSetPasswordReason = account?.profile?.forceSetPasswordReason; + if (forceSetPasswordReason != null) { + await helper.setToUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + forceSetPasswordReason, + ); + + delete account.profile.forceSetPasswordReason; + await helper.set(userId, account); + } + + const masterKeyHash = account?.profile?.keyHash; + if (masterKeyHash != null) { + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, masterKeyHash); + + delete account.profile.keyHash; + await helper.set(userId, account); + } + + const masterKeyEncryptedUserKey = account?.keys?.masterKeyEncryptedUserKey; + if (masterKeyEncryptedUserKey != null) { + await helper.setToUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + masterKeyEncryptedUserKey, + ); + + delete account.keys.masterKeyEncryptedUserKey; + 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 forceSetPasswordReason = await helper.getFromUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + ); + const masterKeyHash = await helper.getFromUser(userId, MASTER_KEY_HASH_DEFINITION); + const masterKeyEncryptedUserKey = await helper.getFromUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + ); + if (account != null) { + if (forceSetPasswordReason != null) { + account.profile = Object.assign(account.profile ?? {}, { + forceSetPasswordReason, + }); + } + if (masterKeyHash != null) { + account.profile = Object.assign(account.profile ?? {}, { + keyHash: masterKeyHash, + }); + } + if (masterKeyEncryptedUserKey != null) { + account.keys = Object.assign(account.keys ?? {}, { + masterKeyEncryptedUserKey, + }); + } + await helper.set(userId, account); + } + + await helper.setToUser(userId, FORCE_SET_PASSWORD_REASON_DEFINITION, null); + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index d4601d9621..ff8e9f1f4f 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -11,8 +11,10 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio import { PolicyData } from "../../../admin-console/models/data/policy.data"; import { ProviderData } from "../../../admin-console/models/data/provider.data"; import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; +import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -49,6 +51,8 @@ export class SyncService implements SyncServiceAbstraction { syncInProgress = false; constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private accountService: AccountService, private apiService: ApiService, private domainSettingsService: DomainSettingsService, private folderService: InternalFolderService, @@ -352,8 +356,10 @@ export class SyncService implements SyncServiceAbstraction { private async setForceSetPasswordReasonIfNeeded(profileResponse: ProfileResponse) { // The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated if (profileResponse.forcePasswordReset) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.AdminForcePasswordReset, + userId, ); } @@ -387,8 +393,10 @@ export class SyncService implements SyncServiceAbstraction { ) { // TDE user w/out MP went from having no password reset permission to having it. // Must set the force password reset reason so the auth guard will redirect to the set password page. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } }