1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-06-28 10:55:27 +02:00

[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
This commit is contained in:
Jake Fink 2024-04-09 20:50:20 -04:00 committed by GitHub
parent c02723d6a6
commit 9d10825dbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 1373 additions and 501 deletions

View File

@ -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),
),
);
}

View File

@ -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),

View File

@ -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),

View File

@ -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<InternalMasterPasswordServiceAbstraction> {
return factory(
cache,
"masterPasswordService",
opts,
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)),
);
}
export async function masterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
opts: MasterPasswordServiceInitOptions,
): Promise<MasterPasswordServiceAbstraction> {
return (await internalMasterPasswordServiceFactory(
cache,
opts,
)) as MasterPasswordServiceAbstraction;
}

View File

@ -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),

View File

@ -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,

View File

@ -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 {}

View File

@ -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) => {

View File

@ -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.

View File

@ -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();

View File

@ -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");

View File

@ -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),

View File

@ -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),

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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) {

View File

@ -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,

View File

@ -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<BroadcasterService>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
let activatedRouteMock: MockProxy<ActivatedRoute>;
let mockMasterPasswordService: FakeMasterPasswordService;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@ -67,6 +70,8 @@ describe("LockComponent", () => {
activatedRouteMock = mock<ActivatedRoute>();
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
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<I18nService>(),

View File

@ -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,

View File

@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<StateService>();
let masterPasswordService: FakeMasterPasswordService;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
const biometricStateService = mock<BiometricStateService>();
@ -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,

View File

@ -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 });

View File

@ -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<string, SymmetricCryptoKey>();
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

View File

@ -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<UserKeyRotationApiService>();
mockCipherService = mock<CipherService>();
mockFolderService = mock<FolderService>();
@ -61,6 +67,7 @@ describe("KeyRotationService", () => {
mockConfigService = mock<ConfigService>();
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 () => {

View File

@ -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) {

View File

@ -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 () => {

View File

@ -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";

View File

@ -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;
}

View File

@ -10,7 +10,11 @@ module.exports = {
displayName: "libs/angular tests",
preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
moduleNameMapper: pathsToModuleNameMapper(
// lets us use @bitwarden/common/spec in tests
{ "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) },
{
prefix: "<rootDir>/",
},
),
};

View File

@ -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<void>();
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

View File

@ -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<void>;
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);
}
}

View File

@ -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<TestSsoComponent>;
const userId = "userId" as UserId;
// Mock Services
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
@ -67,6 +73,8 @@ describe("SsoComponent", () => {
let mockLogService: MockProxy<LogService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
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();

View File

@ -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,
);
}

View File

@ -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<TestTwoFactorComponent>;
const userId = "userId" as UserId;
// Mock Services
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => {
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockAccountService: FakeAccountService;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => {
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigService>();
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);

View File

@ -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,
);
}

View File

@ -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.

View File

@ -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 ===

View File

@ -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({

View File

@ -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<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
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<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
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);

View File

@ -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<void> {
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);

View File

@ -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<LoginStrategyServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>;
@ -118,6 +123,9 @@ describe("LoginStrategy", () => {
let credentials: PasswordLoginCredentials;
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
masterPasswordService = new FakeMasterPasswordService();
loginStrategyService = mock<LoginStrategyServiceAbstraction>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
@ -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,

View File

@ -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<LoginStrategyData>;
constructor(
protected accountService: AccountService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,

View File

@ -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<LoginStrategyServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>;
@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => {
let tokenResponse: IdentityTokenResponse;
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
masterPasswordService = new FakeMasterPasswordService();
loginStrategyService = mock<LoginStrategyServiceAbstraction>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
@ -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);
});

View File

@ -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<void> {
@ -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);

View File

@ -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<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
@ -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<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
@ -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);

View File

@ -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<void> {
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

View File

@ -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<CryptoService>;
let apiService: MockProxy<ApiService>;
@ -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<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
@ -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);

View File

@ -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);

View File

@ -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<CryptoService>;
let apiService!: MockProxy<ApiService>;
@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => {
beforeEach(() => {
jest.clearAllMocks();
accountService = new FakeAccountService(null);
masterPasswordService = new FakeMasterPasswordService();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
@ -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 () => {

View File

@ -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,

View File

@ -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<AppIdService>();
const cryptoService = mock<CryptoService>();
const apiService = mock<ApiService>();
const stateService = mock<StateService>();
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);
});
});

View File

@ -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);
}

View File

@ -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<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
@ -61,7 +70,11 @@ describe("LoginStrategyService", () => {
let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
const userId = "USER_ID" as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
masterPasswordService = new FakeMasterPasswordService();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
@ -84,6 +97,8 @@ describe("LoginStrategyService", () => {
stateProvider = new FakeGlobalStateProvider();
sut = new LoginStrategyService(
accountService,
masterPasswordService,
cryptoService,
apiService,
tokenService,

View File

@ -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<AuthenticationType | null>;
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<AuthRequestResponse> {
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,

View File

@ -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<ForceSetPasswordReason>;
/**
* 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<MasterKey>;
/**
* 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<string>;
/**
* 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<EncString>;
}
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<void>;
/**
* Clear the master key for the user.
* @param userId The user ID.
* @throws If the user ID is missing.
*/
abstract clearMasterKey: (userId: UserId) => Promise<void>;
/**
* 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<void>;
/**
* 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<void>;
/**
* 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<void>;
/**
* 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<void>;
}

View File

@ -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,

View File

@ -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<boolean>;
private convertAccountToKeyConnectorState: ActiveUserState<boolean>;
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]);

View File

@ -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<InternalMasterPasswordServiceAbstraction>();
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
masterKeySubject = new ReplaySubject<MasterKey>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
masterKeyHashSubject = new ReplaySubject<string>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1);
constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) {
this.masterKeySubject.next(initialMasterKey);
this.masterKeyHashSubject.next(initialMasterKeyHash);
}
masterKey$(userId: UserId): Observable<MasterKey> {
return this.masterKeySubject.asObservable();
}
setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> {
return this.mock.setMasterKey(masterKey, userId);
}
clearMasterKey(userId: UserId): Promise<void> {
return this.mock.clearMasterKey(userId);
}
masterKeyHash$(userId: UserId): Observable<string> {
return this.masterKeyHashSubject.asObservable();
}
getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> {
return this.mock.getMasterKeyEncryptedUserKey(userId);
}
setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> {
return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId);
}
setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> {
return this.mock.setMasterKeyHash(masterKeyHash, userId);
}
clearMasterKeyHash(userId: UserId): Promise<void> {
return this.mock.clearMasterKeyHash(userId);
}
forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> {
return this.forceSetPasswordReasonSubject.asObservable();
}
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
return this.mock.setForceSetPasswordReason(reason, userId);
}
}

View File

@ -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<MasterKey>(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<string>(MASTER_PASSWORD_DISK, "masterKeyHash", {
deserializer: (masterKeyHash) => masterKeyHash,
clearOn: ["logout"],
});
/** Disk to persist through lock */
const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncryptedString>(
MASTER_PASSWORD_DISK,
"masterKeyEncryptedUserKey",
{
deserializer: (key) => key,
clearOn: ["logout"],
},
);
/** Disk to persist through lock and account switches */
const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
MASTER_PASSWORD_DISK,
"forceSetPasswordReason",
{
deserializer: (reason) => reason,
clearOn: ["logout"],
},
);
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
constructor(private stateProvider: StateProvider) {}
masterKey$(userId: UserId): Observable<MasterKey> {
if (userId == null) {
throw new Error("User ID is required.");
}
return this.stateProvider.getUser(userId, MASTER_KEY).state$;
}
masterKeyHash$(userId: UserId): Observable<string> {
if (userId == null) {
throw new Error("User ID is required.");
}
return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$;
}
forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> {
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<EncString> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}

View File

@ -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<boolean> {
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<boolean> {
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
);
}

View File

@ -105,18 +105,6 @@ export abstract class CryptoService {
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>;
/**
* 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<void>;
/**
* @param userId The desired user
* @returns The user's master key
*/
abstract getMasterKey(userId?: string): Promise<MasterKey>;
/**
* @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<MasterKey>;
/**
* Clears the user's master key
* @param userId The desired user
*/
abstract clearMasterKey(userId?: string): Promise<void>;
/**
* 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<string>;
/**
* Sets the user's master password hash
* @param keyHash The user's master password hash to set
*/
abstract setMasterKeyHash(keyHash: string): Promise<void>;
/**
* @returns The user's master password hash
*/
abstract getMasterKeyHash(): Promise<string>;
/**
* Clears the user's stored master password hash
* @param userId The desired user
*/
abstract clearMasterKeyHash(userId?: string): Promise<void>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.

View File

@ -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<T extends Account = Account> {
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's master key
*/
getMasterKey: (options?: StorageOptions) => Promise<MasterKey>;
/**
* Sets the user's master key
*/
setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the master key
*/
getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user key encrypted by the master key
*/
setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's auto key
*/
@ -108,10 +89,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
*/
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For legacy purposes only, use getMasterKey instead
*/
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
/**
* @deprecated For migration purposes only, use getUserKeyAuto instead
*/
@ -189,18 +166,11 @@ export abstract class StateService<T extends Account = Account> {
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;
setForceSetPasswordReason: (
value: ForceSetPasswordReason,
options?: StorageOptions,
) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>;
setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>;
getKdfType: (options?: StorageOptions) => Promise<KdfType>;
setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>;
getKeyHash: (options?: StorageOptions) => Promise<string>;
setKeyHash: (value: string, options?: StorageOptions) => Promise<void>;
getLastActive: (options?: StorageOptions) => Promise<number>;
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
getLastSync: (options?: StorageOptions) => Promise<string>;

View File

@ -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({

View File

@ -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;

View File

@ -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);

View File

@ -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<boolean>;
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<boolean> {
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<UserKey> {
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<UserKey> {
@ -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<void> {
await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId });
}
async setMasterKey(key: MasterKey, userId?: UserId): Promise<void> {
await this.stateService.setMasterKey(key, { userId: userId });
}
async getMasterKey(userId?: UserId): Promise<MasterKey> {
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<void> {
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<UserKey> {
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<string> {
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<void> {
await this.stateService.setKeyHash(keyHash);
}
async getMasterKeyHash(): Promise<string> {
return await this.stateService.getKeyHash();
}
async clearMasterKeyHash(userId?: UserId): Promise<void> {
return await this.stateService.setKeyHash(null, { userId: userId });
}
// TODO: move to MasterPasswordService
async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> {
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<any> {
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({

View File

@ -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<SymmetricCryptoKey> {
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<MasterKey> {
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<void> {
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<string> {
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<void> {
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<ForceSetPasswordReason> {
return (
(
await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
)
)?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None
);
}
async setForceSetPasswordReason(
value: ForceSetPasswordReason,
options?: StorageOptions,
): Promise<void> {
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<boolean> {
return (
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
@ -897,23 +811,6 @@ export class StateService<
);
}
async getKeyHash(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.keyHash;
}
async setKeyHash(value: string, options?: StorageOptions): Promise<void> {
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<number> {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());

View File

@ -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", {

View File

@ -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<CipherService>;
let folderService: MockProxy<FolderService>;
let collectionService: MockProxy<CollectionService>;
@ -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<VaultTimeoutAction[]>([]);
vaultTimeoutService = new VaultTimeoutService(
accountService,
masterPasswordService,
cipherService,
folderService,
collectionService,
@ -123,6 +135,15 @@ describe("VaultTimeoutService", () => {
stateService.activeAccount$ = new BehaviorSubject<string>(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);
};

View File

@ -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);

View File

@ -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(

View File

@ -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<MigrationHelper>;
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());
});
});
});

View File

@ -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<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
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<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
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))]);
}
}

View File

@ -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,
);
}
}