[PM-5404, PM-3518] Migrate user decryption options to new service (#7344)

* create new user decryption options service

* rename new service to user decryption options

* add hasMasterPassword to user decryption options service

* migrate device trust service to new user decryption options service

* add migration for user-decryption-options

* migrate sync service and calls to trust-device-service

* rename abstraction file

* migrate two factor component

* migrate two factor spec

* migrate sso component

* migrate set-password component

* migrate base login decryption component

* migrate organization options component

* fix component imports

* add missing imports
- remove state service calls
- add update user decryption options method

* remove acct decryption options from account

* lint

* fix tests and linting

* fix browser

* fix desktop

* add user decryption options service to cli

* remove default value from migration

* bump migration number

* fix merge conflict

* fix vault timeout settings

* fix cli

* more fixes

* add user decryption options service to deps of vault timeout settings service

* update login strategy service with user decryption options

* remove early return from sync bandaid for user decryption options

* move user decryption options service to lib/auth

* move user decryption options to libs/auth

* fix reference

* fix browser

* check user decryption options after 2fa check

* update migration and revert tsconfig changes

* add more documentation

* clear user decryption options on logout

* fix tests by creating helper for user decryption options

* fix tests

* pr feedback

* fix factory

* update migration

* add tests

* update missed migration num in test
This commit is contained in:
Jake Fink 2024-03-20 20:33:57 -04:00 committed by GitHub
parent e2fe1e1567
commit 2111b37c32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 1158 additions and 360 deletions

View File

@ -15,8 +15,8 @@ import {
factory, factory,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import { import {
messagingServiceFactory,
MessagingServiceInitOptions, MessagingServiceInitOptions,
messagingServiceFactory,
} from "../../../platform/background/service-factories/messaging-service.factory"; } from "../../../platform/background/service-factories/messaging-service.factory";
import { import {
StateServiceInitOptions, StateServiceInitOptions,

View File

@ -43,6 +43,11 @@ import {
stateServiceFactory, stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory"; } from "../../../platform/background/service-factories/state-service.factory";
import {
UserDecryptionOptionsServiceInitOptions,
userDecryptionOptionsServiceFactory,
} from "./user-decryption-options-service.factory";
type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions;
export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions &
@ -54,7 +59,8 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
AppIdServiceInitOptions & AppIdServiceInitOptions &
DevicesApiServiceInitOptions & DevicesApiServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
PlatformUtilsServiceInitOptions; PlatformUtilsServiceInitOptions &
UserDecryptionOptionsServiceInitOptions;
export function deviceTrustCryptoServiceFactory( export function deviceTrustCryptoServiceFactory(
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
@ -75,6 +81,7 @@ export function deviceTrustCryptoServiceFactory(
await devicesApiServiceFactory(cache, opts), await devicesApiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -9,7 +9,10 @@ import {
ApiServiceInitOptions, ApiServiceInitOptions,
} from "../../../platform/background/service-factories/api-service.factory"; } from "../../../platform/background/service-factories/api-service.factory";
import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory"; import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; import {
billingAccountProfileStateServiceFactory,
BillingAccountProfileStateServiceInitOptions,
} from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import { import {
CryptoServiceInitOptions, CryptoServiceInitOptions,
cryptoServiceFactory, cryptoServiceFactory,
@ -70,6 +73,10 @@ import {
} from "./key-connector-service.factory"; } from "./key-connector-service.factory";
import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory";
import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory";
import {
internalUserDecryptionOptionServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "./user-decryption-options-service.factory";
type LoginStrategyServiceFactoryOptions = FactoryOptions; type LoginStrategyServiceFactoryOptions = FactoryOptions;
@ -90,7 +97,9 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
PasswordStrengthServiceInitOptions & PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions & DeviceTrustCryptoServiceInitOptions &
AuthRequestServiceInitOptions & AuthRequestServiceInitOptions &
GlobalStateProviderInitOptions; UserDecryptionOptionsServiceInitOptions &
GlobalStateProviderInitOptions &
BillingAccountProfileStateServiceInitOptions;
export function loginStrategyServiceFactory( export function loginStrategyServiceFactory(
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
@ -119,6 +128,7 @@ export function loginStrategyServiceFactory(
await policyServiceFactory(cache, opts), await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts),
await internalUserDecryptionOptionServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts), await globalStateProviderFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts),
), ),

View File

@ -0,0 +1,46 @@
import {
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type UserDecryptionOptionsServiceFactoryOptions = FactoryOptions;
export type UserDecryptionOptionsServiceInitOptions = UserDecryptionOptionsServiceFactoryOptions &
StateProviderInitOptions;
export function userDecryptionOptionsServiceFactory(
cache: {
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
} & CachedServices,
opts: UserDecryptionOptionsServiceInitOptions,
): Promise<UserDecryptionOptionsServiceAbstraction> {
return factory(
cache,
"userDecryptionOptionsService",
opts,
async () => new UserDecryptionOptionsService(await stateProviderFactory(cache, opts)),
);
}
export async function internalUserDecryptionOptionServiceFactory(
cache: {
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
} & CachedServices,
opts: UserDecryptionOptionsServiceInitOptions,
): Promise<InternalUserDecryptionOptionsServiceAbstraction> {
return (await userDecryptionOptionsServiceFactory(
cache,
opts,
)) as InternalUserDecryptionOptionsServiceAbstraction;
}

View File

@ -32,6 +32,10 @@ import {
} from "../../../platform/background/service-factories/state-service.factory"; } from "../../../platform/background/service-factories/state-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "./user-decryption-options-service.factory";
import { import {
UserVerificationApiServiceInitOptions, UserVerificationApiServiceInitOptions,
userVerificationApiServiceFactory, userVerificationApiServiceFactory,
@ -44,6 +48,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
CryptoServiceInitOptions & CryptoServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
UserVerificationApiServiceInitOptions & UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions & PinCryptoServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions & VaultTimeoutSettingsServiceInitOptions &
@ -63,6 +68,7 @@ export function userVerificationServiceFactory(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts), await pinCryptoServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts),

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -37,6 +38,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
route: ActivatedRoute, route: ActivatedRoute,
organizationApiService: OrganizationApiServiceAbstraction, organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService, organizationUserService: OrganizationUserService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService, dialogService: DialogService,
) { ) {
@ -55,6 +57,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
stateService, stateService,
organizationApiService, organizationApiService,
organizationUserService, organizationUserService,
userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
dialogService, dialogService,
); );

View File

@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@ -39,6 +42,7 @@ export class SsoComponent extends BaseSsoComponent {
syncService: SyncService, syncService: SyncService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
logService: LogService, logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
protected authService: AuthService, protected authService: AuthService,
@Inject(WINDOW) private win: Window, @Inject(WINDOW) private win: Window,
@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService, environmentService,
passwordGenerationService, passwordGenerationService,
logService, logService,
userDecryptionOptionsService,
configService, configService,
); );

View File

@ -5,7 +5,10 @@ import { filter, first, takeUntil } from "rxjs/operators";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@ -55,6 +58,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService, appIdService: AppIdService,
loginService: LoginService, loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
private dialogService: DialogService, private dialogService: DialogService,
@ -75,6 +79,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService, twoFactorService,
appIdService, appIdService,
loginService, loginService,
userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
); );

View File

@ -5,6 +5,8 @@ import {
PinCryptoService, PinCryptoService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
LoginStrategyService, LoginStrategyService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
AuthRequestService, AuthRequestService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
@ -242,6 +244,7 @@ export default class MainBackground {
environmentService: BrowserEnvironmentService; environmentService: BrowserEnvironmentService;
cipherService: CipherServiceAbstraction; cipherService: CipherServiceAbstraction;
folderService: InternalFolderServiceAbstraction; folderService: InternalFolderServiceAbstraction;
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
collectionService: CollectionServiceAbstraction; collectionService: CollectionServiceAbstraction;
vaultTimeoutService: VaultTimeoutService; vaultTimeoutService: VaultTimeoutService;
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
@ -539,6 +542,8 @@ export default class MainBackground {
}; };
})(); })();
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.keyGenerationService, this.keyGenerationService,
@ -550,6 +555,7 @@ export default class MainBackground {
this.devicesApiService, this.devicesApiService,
this.i18nService, this.i18nService,
this.platformUtilsService, this.platformUtilsService,
this.userDecryptionOptionsService,
); );
this.devicesService = new DevicesServiceImplementation(this.devicesApiService); this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
@ -590,6 +596,7 @@ export default class MainBackground {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.userDecryptionOptionsService,
this.globalStateProvider, this.globalStateProvider,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
); );
@ -631,6 +638,7 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.userDecryptionOptionsService,
this.cryptoService, this.cryptoService,
this.tokenService, this.tokenService,
this.policyService, this.policyService,
@ -650,6 +658,7 @@ export default class MainBackground {
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService, this.pinCryptoService,
this.logService, this.logService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
@ -717,6 +726,7 @@ export default class MainBackground {
this.folderApiService, this.folderApiService,
this.organizationService, this.organizationService,
this.sendApiService, this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService, this.avatarService,
logoutCallback, logoutCallback,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,

View File

@ -9,6 +9,10 @@ import {
tokenServiceFactory, tokenServiceFactory,
TokenServiceInitOptions, TokenServiceInitOptions,
} from "../../auth/background/service-factories/token-service.factory"; } from "../../auth/background/service-factories/token-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "../../auth/background/service-factories/user-decryption-options-service.factory";
import { import {
biometricStateServiceFactory, biometricStateServiceFactory,
BiometricStateServiceInitOptions, BiometricStateServiceInitOptions,
@ -30,6 +34,7 @@ import {
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions & export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
UserDecryptionOptionsServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
TokenServiceInitOptions & TokenServiceInitOptions &
PolicyServiceInitOptions & PolicyServiceInitOptions &
@ -46,6 +51,7 @@ export function vaultTimeoutSettingsServiceFactory(
opts, opts,
async () => async () =>
new VaultTimeoutSettingsService( new VaultTimeoutSettingsService(
await userDecryptionOptionsServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts), await tokenServiceFactory(cache, opts),
await policyServiceFactory(cache, opts), await policyServiceFactory(cache, opts),

View File

@ -5,11 +5,13 @@ import { program } from "commander";
import * as jsdom from "jsdom"; import * as jsdom from "jsdom";
import { import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestService, AuthRequestService,
LoginStrategyService, LoginStrategyService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
PinCryptoService, PinCryptoService,
PinCryptoServiceAbstraction, PinCryptoServiceAbstraction,
UserDecryptionOptionsService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
@ -169,6 +171,7 @@ export class Main {
eventUploadService: EventUploadServiceAbstraction; eventUploadService: EventUploadServiceAbstraction;
passwordGenerationService: PasswordGenerationServiceAbstraction; passwordGenerationService: PasswordGenerationServiceAbstraction;
passwordStrengthService: PasswordStrengthServiceAbstraction; passwordStrengthService: PasswordStrengthServiceAbstraction;
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
totpService: TotpService; totpService: TotpService;
containerService: ContainerService; containerService: ContainerService;
auditService: AuditService; auditService: AuditService;
@ -436,6 +439,8 @@ export class Main {
this.stateService, this.stateService,
); );
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.keyGenerationService, this.keyGenerationService,
@ -447,6 +452,7 @@ export class Main {
this.devicesApiService, this.devicesApiService,
this.i18nService, this.i18nService,
this.platformUtilsService, this.platformUtilsService,
this.userDecryptionOptionsService,
); );
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(
@ -478,6 +484,7 @@ export class Main {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.userDecryptionOptionsService,
this.globalStateProvider, this.globalStateProvider,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
); );
@ -529,6 +536,7 @@ export class Main {
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.userDecryptionOptionsService,
this.cryptoService, this.cryptoService,
this.tokenService, this.tokenService,
this.policyService, this.policyService,
@ -548,6 +556,7 @@ export class Main {
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService, this.pinCryptoService,
this.logService, this.logService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
@ -589,6 +598,7 @@ export class Main {
this.folderApiService, this.folderApiService,
this.organizationService, this.organizationService,
this.sendApiService, this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService, this.avatarService,
async (expired: boolean) => await this.logout(), async (expired: boolean) => await this.logout(),
this.billingAccountProfileStateService, this.billingAccountProfileStateService,

View File

@ -2,6 +2,7 @@ import { Component, NgZone, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -44,6 +45,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService: StateService, stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction, organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService, organizationUserService: OrganizationUserService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService, dialogService: DialogService,
) { ) {
@ -62,6 +64,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService, stateService,
organizationApiService, organizationApiService,
organizationUserService, organizationUserService,
userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
dialogService, dialogService,
); );

View File

@ -2,7 +2,10 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@ -34,6 +37,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService: EnvironmentService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction, passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService, logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
) { ) {
super( super(
@ -49,6 +53,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService, environmentService,
passwordGenerationService, passwordGenerationService,
logService, logService,
userDecryptionOptionsService,
configService, configService,
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {

View File

@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@ -53,6 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService, appIdService: AppIdService,
loginService: LoginService, loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window, @Inject(WINDOW) protected win: Window,
@ -71,6 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService, twoFactorService,
appIdService, appIdService,
loginService, loginService,
userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
); );

View File

@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; 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 { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
@ -41,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent {
logService: LogService, logService: LogService,
private orgDomainApiService: OrgDomainApiServiceAbstraction, private orgDomainApiService: OrgDomainApiServiceAbstraction,
private validationService: ValidationService, private validationService: ValidationService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
) { ) {
super( super(
@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService, environmentService,
passwordGenerationService, passwordGenerationService,
logService, logService,
userDecryptionOptionsService,
configService, configService,
); );
this.redirectUri = window.location.origin + "/sso-connector.html"; this.redirectUri = window.location.origin + "/sso-connector.html";

View File

@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@ -44,6 +47,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService, appIdService: AppIdService,
loginService: LoginService, loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window, @Inject(WINDOW) protected win: Window,
@ -62,6 +66,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService, twoFactorService,
appIdService, appIdService,
loginService, loginService,
userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
); );

View File

@ -1,6 +1,7 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -12,7 +13,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@ -44,8 +44,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private logService: LogService, private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private dialogService: DialogService, private dialogService: DialogService,
private stateService: StateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -56,7 +56,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
combineLatest([ combineLatest([
this.organization$, this.organization$,
resetPasswordPolicies$, resetPasswordPolicies$,
this.stateService.getAccountDecryptionOptions(), this.userDecryptionOptionsService.userDecryptionOptions$,
]) ])
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => { .subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {

View File

@ -14,6 +14,10 @@ import {
throwError, throwError,
} from "rxjs"; } from "rxjs";
import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -30,7 +34,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
enum State { enum State {
NewUser, NewUser,
@ -88,6 +91,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected validationService: ValidationService, protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction,
) {} ) {}
@ -101,14 +105,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
await this.setRememberDeviceDefaultValue(); await this.setRememberDeviceDefaultValue();
try { try {
const accountDecryptionOptions: AccountDecryptionOptions = const userDecryptionOptions = await firstValueFrom(
await this.stateService.getAccountDecryptionOptions(); this.userDecryptionOptionsService.userDecryptionOptions$,
);
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response.. // see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
// can we check if they have a user key or master key in crypto service? Would that be sufficient? // can we check if they have a user key or master key in crypto service? Would that be sufficient?
if ( if (
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval && !userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
!accountDecryptionOptions?.hasMasterPassword !userDecryptionOptions?.hasMasterPassword
) { ) {
// We are dealing with a new account if: // We are dealing with a new account if:
// - User does not have admin approval (i.e. has not enrolled into admin reset) // - User does not have admin approval (i.e. has not enrolled into admin reset)
@ -118,7 +123,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadNewUserData(); this.loadNewUserData();
} else { } else {
this.loadUntrustedDeviceData(accountDecryptionOptions); this.loadUntrustedDeviceData(userDecryptionOptions);
} }
// Note: this is probably not a comprehensive write up of all scenarios: // Note: this is probably not a comprehensive write up of all scenarios:
@ -195,7 +200,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
this.loading = false; this.loading = false;
} }
loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) { loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
this.loading = true; this.loading = true;
const email$ = from(this.stateService.getEmail()).pipe( const email$ = from(this.stateService.getEmail()).pipe(
@ -215,13 +220,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
) )
.subscribe((email) => { .subscribe((email) => {
const showApproveFromOtherDeviceBtn = const showApproveFromOtherDeviceBtn =
accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false; userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
const showReqAdminApprovalBtn = const showReqAdminApprovalBtn =
!!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false; !!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
const showApproveWithMasterPasswordBtn = const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
accountDecryptionOptions?.hasMasterPassword || false;
const userEmail = email; const userEmail = email;

View File

@ -1,8 +1,9 @@
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { of } from "rxjs"; import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators"; import { filter, first, switchMap, tap } from "rxjs/operators";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@ -26,7 +27,6 @@ import {
DEFAULT_KDF_CONFIG, DEFAULT_KDF_CONFIG,
} from "@bitwarden/common/platform/enums"; } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
@ -64,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService: StateService, stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private ssoLoginService: SsoLoginServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService, dialogService: DialogService,
) { ) {
@ -228,11 +229,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
// User now has a password so update account decryption options in state // User now has a password so update account decryption options in state
const acctDecryptionOpts: AccountDecryptionOptions = const userDecryptionOpts = await firstValueFrom(
await this.stateService.getAccountDecryptionOptions(); this.userDecryptionOptionsService.userDecryptionOptions$,
);
acctDecryptionOpts.hasMasterPassword = true; userDecryptionOpts.hasMasterPassword = true;
await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts); await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfType(this.kdf);
await this.stateService.setKdfConfig(this.kdfConfig); await this.stateService.setKdfConfig(this.kdfConfig);

View File

@ -2,16 +2,20 @@ import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended"; import { MockProxy, mock } from "jest-mock-extended";
import { Observable, of } from "rxjs"; import { BehaviorSubject, Observable, of } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.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 { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -19,7 +23,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SsoComponent } from "./sso.component"; import { SsoComponent } from "./sso.component";
@ -62,6 +65,7 @@ describe("SsoComponent", () => {
let mockEnvironmentService: MockProxy<EnvironmentService>; let mockEnvironmentService: MockProxy<EnvironmentService>;
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>; let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
let mockLogService: MockProxy<LogService>; let mockLogService: MockProxy<LogService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>; let mockConfigService: MockProxy<ConfigServiceAbstraction>;
// Mock authService.logIn params // Mock authService.logIn params
@ -77,17 +81,19 @@ describe("SsoComponent", () => {
let mockOnSuccessfulLoginForceResetNavigate: jest.Mock; let mockOnSuccessfulLoginForceResetNavigate: jest.Mock;
let mockOnSuccessfulLoginTdeNavigate: jest.Mock; let mockOnSuccessfulLoginTdeNavigate: jest.Mock;
let mockAcctDecryptionOpts: { let mockUserDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions; noMasterPassword: UserDecryptionOptions;
withMasterPassword: AccountDecryptionOptions; withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions; withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions; withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions; noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions; noMasterPasswordWithKeyConnector: UserDecryptionOptions;
}; };
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => { beforeEach(() => {
// Mock Services // Mock Services
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>(); mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
@ -109,6 +115,7 @@ describe("SsoComponent", () => {
mockEnvironmentService = mock<EnvironmentService>(); mockEnvironmentService = mock<EnvironmentService>();
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>(); mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
mockLogService = mock<LogService>(); mockLogService = mock<LogService>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>(); mockConfigService = mock<ConfigServiceAbstraction>();
// Mock loginStrategyService.logIn params // Mock loginStrategyService.logIn params
@ -124,49 +131,52 @@ describe("SsoComponent", () => {
mockOnSuccessfulLoginForceResetNavigate = jest.fn(); mockOnSuccessfulLoginForceResetNavigate = jest.fn();
mockOnSuccessfulLoginTdeNavigate = jest.fn(); mockOnSuccessfulLoginTdeNavigate = jest.fn();
mockAcctDecryptionOpts = { mockUserDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({ noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPassword: new AccountDecryptionOptions({ withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({ withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({ withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}), }),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({ noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({ noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}), }),
}; };
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [TestSsoComponent], declarations: [TestSsoComponent],
providers: [ providers: [
@ -183,6 +193,10 @@ describe("SsoComponent", () => {
{ provide: EnvironmentService, useValue: mockEnvironmentService }, { provide: EnvironmentService, useValue: mockEnvironmentService },
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService }, { provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: LogService, useValue: mockLogService }, { provide: LogService, useValue: mockLogService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService }, { provide: ConfigServiceAbstraction, useValue: mockConfigService },
], ],
@ -230,9 +244,7 @@ describe("SsoComponent", () => {
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
// use standard user with MP because this test is not concerned with password reset. // use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockAcctDecryptionOpts.withMasterPassword,
);
mockLoginStrategyService.logIn.mockResolvedValue(authResult); mockLoginStrategyService.logIn.mockResolvedValue(authResult);
}); });
@ -341,8 +353,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
let authResult; let authResult;
beforeEach(() => { beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
); );
authResult = new AuthResult(); authResult = new AuthResult();
@ -377,8 +389,8 @@ describe("SsoComponent", () => {
const reasonString = ForceSetPasswordReason[forceResetPasswordReason]; const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
let authResult; let authResult;
beforeEach(() => { beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
); );
authResult = new AuthResult(); authResult = new AuthResult();
@ -394,8 +406,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => { describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult; let authResult;
beforeEach(() => { beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
); );
authResult = new AuthResult(); authResult = new AuthResult();
@ -440,9 +452,7 @@ describe("SsoComponent", () => {
describe("Given user needs to set a master password", () => { describe("Given user needs to set a master password", () => {
beforeEach(() => { beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here // Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
mockAcctDecryptionOpts.noMasterPassword,
);
}); });
testChangePasswordOnSuccessfulLogin(); testChangePasswordOnSuccessfulLogin();
@ -450,9 +460,7 @@ describe("SsoComponent", () => {
}); });
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => { it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPasswordWithKeyConnector);
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
);
await _component.logIn(code, codeVerifier, orgIdFromState); await _component.logIn(code, codeVerifier, orgIdFromState);
expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1);
@ -475,9 +483,7 @@ describe("SsoComponent", () => {
beforeEach(() => { beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset. // use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockAcctDecryptionOpts.withMasterPassword,
);
const authResult = new AuthResult(); const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason; authResult.forcePasswordReset = forceResetPasswordReason;
@ -494,9 +500,7 @@ describe("SsoComponent", () => {
const authResult = new AuthResult(); const authResult = new AuthResult();
authResult.twoFactorProviders = null; authResult.twoFactorProviders = null;
// use standard user with MP because this test is not concerned with password reset. // use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockAcctDecryptionOpts.withMasterPassword,
);
authResult.forcePasswordReset = ForceSetPasswordReason.None; authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockLoginStrategyService.logIn.mockResolvedValue(authResult); mockLoginStrategyService.logIn.mockResolvedValue(authResult);
}); });

View File

@ -1,13 +1,19 @@
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
SsoLoginCredentials,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -17,7 +23,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@Directive() @Directive()
@ -59,6 +64,7 @@ export class SsoComponent {
protected environmentService: EnvironmentService, protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected logService: LogService, protected logService: LogService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected configService: ConfigServiceAbstraction, protected configService: ConfigServiceAbstraction,
) {} ) {}
@ -194,9 +200,6 @@ export class SsoComponent {
this.formPromise = this.loginStrategyService.logIn(credentials); this.formPromise = this.loginStrategyService.logIn(credentials);
const authResult = await this.formPromise; const authResult = await this.formPromise;
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
if (authResult.requiresTwoFactor) { if (authResult.requiresTwoFactor) {
return await this.handleTwoFactorRequired(orgSsoIdentifier); return await this.handleTwoFactorRequired(orgSsoIdentifier);
} }
@ -217,15 +220,20 @@ export class SsoComponent {
return await this.handleForcePasswordReset(orgSsoIdentifier); return await this.handleForcePasswordReset(orgSsoIdentifier);
} }
// must come after 2fa check since user decryption options aren't available if 2fa is required
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled( const tdeEnabled = await this.isTrustedDeviceEncEnabled(
acctDecryptionOpts.trustedDeviceOption, userDecryptionOpts.trustedDeviceOption,
); );
if (tdeEnabled) { if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled( return await this.handleTrustedDeviceEncryptionEnabled(
authResult, authResult,
orgSsoIdentifier, orgSsoIdentifier,
acctDecryptionOpts, userDecryptionOpts,
); );
} }
@ -233,8 +241,8 @@ export class SsoComponent {
// have one and they aren't using key connector. // have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options. // Note: TDE & Key connector are mutually exclusive org config options.
const requireSetPassword = const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword && !userDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.keyConnectorOption === undefined; userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) { if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case // Change implies going no password -> password in this case
@ -270,12 +278,12 @@ export class SsoComponent {
private async handleTrustedDeviceEncryptionEnabled( private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult, authResult: AuthResult,
orgIdentifier: string, orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions, userDecryptionOpts: UserDecryptionOptions,
): Promise<void> { ): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP // If user doesn't have a MP, but has reset password permission, they must set a MP
if ( if (
!acctDecryptionOpts.hasMasterPassword && !userDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) { ) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // 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 // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and

View File

@ -1,19 +1,24 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router"; import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@ -22,7 +27,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { TwoFactorComponent } from "./two-factor.component"; import { TwoFactorComponent } from "./two-factor.component";
@ -56,20 +60,23 @@ describe("TwoFactorComponent", () => {
let mockTwoFactorService: MockProxy<TwoFactorService>; let mockTwoFactorService: MockProxy<TwoFactorService>;
let mockAppIdService: MockProxy<AppIdService>; let mockAppIdService: MockProxy<AppIdService>;
let mockLoginService: MockProxy<LoginService>; let mockLoginService: MockProxy<LoginService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>; let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockAcctDecryptionOpts: { let mockUserDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions; noMasterPassword: UserDecryptionOptions;
withMasterPassword: AccountDecryptionOptions; withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions; withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions; withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions; noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions; noMasterPasswordWithKeyConnector: UserDecryptionOptions;
}; };
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => { beforeEach(() => {
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>(); mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
mockRouter = mock<Router>(); mockRouter = mock<Router>();
@ -83,52 +90,56 @@ describe("TwoFactorComponent", () => {
mockTwoFactorService = mock<TwoFactorService>(); mockTwoFactorService = mock<TwoFactorService>();
mockAppIdService = mock<AppIdService>(); mockAppIdService = mock<AppIdService>();
mockLoginService = mock<LoginService>(); mockLoginService = mock<LoginService>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>(); mockConfigService = mock<ConfigServiceAbstraction>();
mockAcctDecryptionOpts = { mockUserDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({ noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPassword: new AccountDecryptionOptions({ withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({ withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({ withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true, hasMasterPassword: true,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}), }),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({ noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined, keyConnectorOption: undefined,
}), }),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({ noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false, hasMasterPassword: false,
trustedDeviceOption: undefined, trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}), }),
}; };
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [TestTwoFactorComponent], declarations: [TestTwoFactorComponent],
providers: [ providers: [
@ -153,6 +164,10 @@ describe("TwoFactorComponent", () => {
{ provide: TwoFactorService, useValue: mockTwoFactorService }, { provide: TwoFactorService, useValue: mockTwoFactorService },
{ provide: AppIdService, useValue: mockAppIdService }, { provide: AppIdService, useValue: mockAppIdService },
{ provide: LoginService, useValue: mockLoginService }, { provide: LoginService, useValue: mockLoginService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService }, { provide: ConfigServiceAbstraction, useValue: mockConfigService },
], ],
@ -213,9 +228,7 @@ describe("TwoFactorComponent", () => {
component.remember = remember; component.remember = remember;
component.captchaToken = captchaToken; component.captchaToken = captchaToken;
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockAcctDecryptionOpts.withMasterPassword,
);
}); });
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => { it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
@ -289,17 +302,15 @@ describe("TwoFactorComponent", () => {
describe("Given user needs to set a master password", () => { describe("Given user needs to set a master password", () => {
beforeEach(() => { beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here // Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
mockAcctDecryptionOpts.noMasterPassword,
);
}); });
testChangePasswordOnSuccessfulLogin(); testChangePasswordOnSuccessfulLogin();
}); });
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => { it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector, mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
); );
await component.doSubmit(); await component.doSubmit();
@ -321,9 +332,7 @@ describe("TwoFactorComponent", () => {
beforeEach(() => { beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset. // use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockAcctDecryptionOpts.withMasterPassword,
);
const authResult = new AuthResult(); const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason; authResult.forcePasswordReset = forceResetPasswordReason;
@ -385,8 +394,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
beforeEach(() => { beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
); );
const authResult = new AuthResult(); const authResult = new AuthResult();
@ -420,8 +429,8 @@ describe("TwoFactorComponent", () => {
beforeEach(() => { beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset. // use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
); );
const authResult = new AuthResult(); const authResult = new AuthResult();
@ -436,8 +445,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => { describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult; let authResult;
beforeEach(() => { beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue( selectedUserDecryptionOptions.next(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
); );
authResult = new AuthResult(); authResult = new AuthResult();

View File

@ -6,7 +6,12 @@ import { first } from "rxjs/operators";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import {
LoginStrategyServiceAbstraction,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@ -15,7 +20,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
@ -27,7 +31,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { CaptchaProtectedComponent } from "./captcha-protected.component"; import { CaptchaProtectedComponent } from "./captcha-protected.component";
@ -86,6 +89,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected twoFactorService: TwoFactorService, protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService, protected appIdService: AppIdService,
protected loginService: LoginService, protected loginService: LoginService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigServiceAbstraction, protected configService: ConfigServiceAbstraction,
) { ) {
@ -290,22 +294,23 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return await this.handleForcePasswordReset(this.orgIdentifier); return await this.handleForcePasswordReset(this.orgIdentifier);
} }
const acctDecryptionOpts: AccountDecryptionOptions = const userDecryptionOpts = await firstValueFrom(
await this.stateService.getAccountDecryptionOptions(); this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption); const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
if (tdeEnabled) { if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled( return await this.handleTrustedDeviceEncryptionEnabled(
authResult, authResult,
this.orgIdentifier, this.orgIdentifier,
acctDecryptionOpts, userDecryptionOpts,
); );
} }
// User must set password if they don't have one and they aren't using either TDE or key connector. // User must set password if they don't have one and they aren't using either TDE or key connector.
const requireSetPassword = const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined; !userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) { if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case // Change implies going no password -> password in this case
@ -326,12 +331,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
private async handleTrustedDeviceEncryptionEnabled( private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult, authResult: AuthResult,
orgIdentifier: string, orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions, userDecryptionOpts: UserDecryptionOptions,
): Promise<void> { ): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP // If user doesn't have a MP, but has reset password permission, they must set a MP
if ( if (
!acctDecryptionOpts.hasMasterPassword && !userDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) { ) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // 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 // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and

View File

@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn {
// User is authN and in locked state. // User is authN and in locked state.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
// Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow // Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow
// The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP // The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP

View File

@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
// login decryption options component. // login decryption options component.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });

View File

@ -26,7 +26,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
const router = inject(Router); const router = inject(Router);
const authStatus = await authService.getAuthStatus(); const authStatus = await authService.getAuthStatus();
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) { if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
return router.createUrlTree(["/"]); return router.createUrlTree(["/"]);

View File

@ -8,6 +8,9 @@ import {
PinCryptoService, PinCryptoService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
LoginStrategyService, LoginStrategyService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -243,8 +246,8 @@ import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
import { import {
LOCALES_DIRECTORY, LOCALES_DIRECTORY,
LOCKED_CALLBACK, LOCKED_CALLBACK,
LOG_MAC_FAILURES,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
LOG_MAC_FAILURES,
MEMORY_STORAGE, MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_MEMORY_STORAGE,
@ -369,6 +372,7 @@ const typesafeProviders: Array<SafeProvider> = [
PolicyServiceAbstraction, PolicyServiceAbstraction,
DeviceTrustCryptoServiceAbstraction, DeviceTrustCryptoServiceAbstraction,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
GlobalStateProvider, GlobalStateProvider,
BillingAccountProfileStateService, BillingAccountProfileStateService,
], ],
@ -477,6 +481,15 @@ const typesafeProviders: Array<SafeProvider> = [
useClass: EnvironmentService, useClass: EnvironmentService,
deps: [StateProvider, AccountServiceAbstraction], deps: [StateProvider, AccountServiceAbstraction],
}), }),
safeProvider({
provide: InternalUserDecryptionOptionsServiceAbstraction,
useClass: UserDecryptionOptionsService,
deps: [StateProvider],
}),
safeProvider({
provide: UserDecryptionOptionsServiceAbstraction,
useExisting: InternalUserDecryptionOptionsServiceAbstraction,
}),
safeProvider({ safeProvider({
provide: TotpServiceAbstraction, provide: TotpServiceAbstraction,
useClass: TotpService, useClass: TotpService,
@ -577,6 +590,7 @@ const typesafeProviders: Array<SafeProvider> = [
FolderApiServiceAbstraction, FolderApiServiceAbstraction,
InternalOrganizationServiceAbstraction, InternalOrganizationServiceAbstraction,
SendApiServiceAbstraction, SendApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
AvatarServiceAbstraction, AvatarServiceAbstraction,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
BillingAccountProfileStateService, BillingAccountProfileStateService,
@ -587,6 +601,7 @@ const typesafeProviders: Array<SafeProvider> = [
provide: VaultTimeoutSettingsServiceAbstraction, provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService, useClass: VaultTimeoutSettingsService,
deps: [ deps: [
UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
TokenServiceAbstraction, TokenServiceAbstraction,
PolicyServiceAbstraction, PolicyServiceAbstraction,
@ -765,6 +780,7 @@ const typesafeProviders: Array<SafeProvider> = [
CryptoServiceAbstraction, CryptoServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
UserVerificationApiServiceAbstraction, UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction, PinCryptoServiceAbstraction,
LogService, LogService,
VaultTimeoutSettingsServiceAbstraction, VaultTimeoutSettingsServiceAbstraction,
@ -902,6 +918,7 @@ const typesafeProviders: Array<SafeProvider> = [
DevicesApiServiceAbstraction, DevicesApiServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -1,3 +1,4 @@
export * from "./pin-crypto.service.abstraction"; export * from "./pin-crypto.service.abstraction";
export * from "./login-strategy.service"; export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction";
export * from "./auth-request.service.abstraction"; export * from "./auth-request.service.abstraction";

View File

@ -0,0 +1,34 @@
import { Observable } from "rxjs";
import { UserDecryptionOptions } from "../models";
export abstract class UserDecryptionOptionsServiceAbstraction {
/**
* Returns what decryption options are available for the current user.
* @remark This is sent from the server on authentication.
*/
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
/**
* Uses user decryption options to determine if current user has a master password.
* @remark This is sent from the server, and does not indicate if the master password
* was used to login and/or if a master key is saved locally.
*/
abstract hasMasterPassword$: Observable<boolean>;
/**
* Returns the user decryption options for the given user id.
* @param userId The user id to check.
*/
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
}
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
/**
* Sets the current decryption options for the user, contains the current configuration
* of the users account related to how they can decrypt their vault.
* @remark Intended to be used when user decryption options are received from server, does
* not update the server. Consider syncing instead of updating locally.
* @param userDecryptionOptions Current user decryption options received from server.
*/
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
}

View File

@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { import {
@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptions,
deviceTrustCryptoService, deviceTrustCryptoService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -28,7 +28,6 @@ import {
AccountProfile, AccountProfile,
AccountTokens, AccountTokens,
AccountKeys, AccountKeys,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account"; } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@ -39,8 +38,10 @@ import {
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key"; import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service"; import { LoginStrategyServiceAbstraction } from "../abstractions";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
@ -108,6 +109,7 @@ describe("LoginStrategy", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@ -126,7 +128,7 @@ describe("LoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>(); passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@ -146,6 +148,7 @@ describe("LoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,
@ -204,9 +207,11 @@ describe("LoginStrategy", () => {
...new AccountTokens(), ...new AccountTokens(),
}, },
keys: new AccountKeys(), keys: new AccountKeys(),
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
}), }),
); );
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
UserDecryptionOptions.fromResponse(idTokenResponse),
);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
}); });
@ -409,6 +414,7 @@ describe("LoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,

View File

@ -30,9 +30,9 @@ import {
Account, Account,
AccountProfile, AccountProfile,
AccountTokens, AccountTokens,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account"; } from "@bitwarden/common/platform/models/domain/account";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { import {
UserApiLoginCredentials, UserApiLoginCredentials,
PasswordLoginCredentials, PasswordLoginCredentials,
@ -40,6 +40,7 @@ import {
AuthRequestLoginCredentials, AuthRequestLoginCredentials,
WebAuthnLoginCredentials, WebAuthnLoginCredentials,
} from "../models/domain/login-credentials"; } from "../models/domain/login-credentials";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
@ -69,6 +70,7 @@ export abstract class LoginStrategy {
protected logService: LogService, protected logService: LogService,
protected stateService: StateService, protected stateService: StateService,
protected twoFactorService: TwoFactorService, protected twoFactorService: TwoFactorService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected billingAccountProfileStateService: BillingAccountProfileStateService, protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
@ -203,11 +205,14 @@ export abstract class LoginStrategy {
...new AccountTokens(), ...new AccountTokens(),
}, },
keys: accountKeys, keys: accountKeys,
decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse),
adminAuthRequest: adminAuthRequest?.toJSON(), adminAuthRequest: adminAuthRequest?.toJSON(),
}), }),
); );
await this.userDecryptionOptionsService.setUserDecryptionOptions(
UserDecryptionOptions.fromResponse(tokenResponse),
);
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
} }

View File

@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions"; import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>(); passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,

View File

@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions"; import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
@ -84,6 +85,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
protected stateService: StateService, protected stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private passwordStrengthService: PasswordStrengthServiceAbstraction, private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService, private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction,
@ -99,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction } from "../abstractions"; import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>; let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>(); keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>();
@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
keyConnectorService, keyConnectorService,
deviceTrustCryptoService, deviceTrustCryptoService,
authRequestService, authRequestService,

View File

@ -21,7 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AuthRequestServiceAbstraction } from "../abstractions"; import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
@ -84,6 +87,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
@ -100,6 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
@ -35,6 +36,7 @@ describe("UserApiLoginStrategy", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>; let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>; let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>(); keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>(); environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@ -76,6 +79,7 @@ describe("UserApiLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
environmentService, environmentService,
keyConnectorService, keyConnectorService,
billingAccountProfileStateService, billingAccountProfileStateService,

View File

@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);

View File

@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => {
let logService!: MockProxy<LogService>; let logService!: MockProxy<LogService>;
let stateService!: MockProxy<StateService>; let stateService!: MockProxy<StateService>;
let twoFactorService!: MockProxy<TwoFactorService>; let twoFactorService!: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -17,6 +17,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state"; import { CacheData } from "../services/login-strategies/login-strategy.state";
@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -1,2 +1,3 @@
export * from "./rotateable-key-set"; export * from "./rotateable-key-set";
export * from "./login-credentials"; export * from "./login-credentials";
export * from "./user-decryption-options";

View File

@ -0,0 +1,153 @@
import { Jsonify } from "type-fest";
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response";
/**
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class KeyConnectorUserDecryptionOption {
/** The URL of the key connector configured for this user. */
keyConnectorUrl: string;
/**
* Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object.
* @param response The key connector user decryption option response object.
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish.
*/
static fromResponse(
response: KeyConnectorUserDecryptionOptionResponse,
): KeyConnectorUserDecryptionOption {
const options = new KeyConnectorUserDecryptionOption();
options.keyConnectorUrl = response?.keyConnectorUrl ?? null;
return options;
}
/**
* Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<KeyConnectorUserDecryptionOption>,
): KeyConnectorUserDecryptionOption {
return Object.assign(new KeyConnectorUserDecryptionOption(), obj);
}
}
/**
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class TrustedDeviceUserDecryptionOption {
/** True if an admin has approved an admin auth request previously made from this device. */
hasAdminApproval: boolean;
/** True if the user has a device capable of approving an auth request. */
hasLoginApprovingDevice: boolean;
/** True if the user has manage reset password permission, as these users must be forced to have a master password. */
hasManageResetPasswordPermission: boolean;
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object.
* @param response The trusted device user decryption option response object.
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish.
*/
static fromResponse(
response: TrustedDeviceUserDecryptionOptionResponse,
): TrustedDeviceUserDecryptionOption {
const options = new TrustedDeviceUserDecryptionOption();
options.hasAdminApproval = response?.hasAdminApproval ?? false;
options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false;
options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false;
return options;
}
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<TrustedDeviceUserDecryptionOption>,
): TrustedDeviceUserDecryptionOption {
return Object.assign(new TrustedDeviceUserDecryptionOption(), obj);
}
}
/**
* Represents the decryption options the user has configured on the server. This is intended to be sent
* to the client on authentication, and can be used to determine how to decrypt the user's vault.
*/
export class UserDecryptionOptions {
/** True if the user has a master password configured on the server. */
hasMasterPassword: boolean;
/** {@link TrustedDeviceUserDecryptionOption} */
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
/** {@link KeyConnectorUserDecryptionOption} */
keyConnectorOption?: KeyConnectorUserDecryptionOption;
/**
* Initializes a new instance of the UserDecryptionOptions from a response object.
* @param response user decryption options response object
* @returns A new instance of the UserDecryptionOptions.
* @throws If the response is nullish, this method will throw an error. User decryption options
* are required for client initialization.
*/
// TODO: Change response type to `UserDecryptionOptionsResponse` after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
static fromResponse(response: IdentityTokenResponse): UserDecryptionOptions {
if (response == null) {
throw new Error("User Decryption Options are required for client initialization.");
}
const decryptionOptions = new UserDecryptionOptions();
if (response.userDecryptionOptions) {
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
// the new decryption options.
const responseOptions = response.userDecryptionOptions;
decryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromResponse(
responseOptions.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse(
responseOptions.keyConnectorOption,
);
} else {
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
// we must base our decryption options on the presence of the keyConnectorUrl.
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
const usingKeyConnector = response.keyConnectorUrl != null;
decryptionOptions.hasMasterPassword = !usingKeyConnector;
if (usingKeyConnector) {
decryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption();
decryptionOptions.keyConnectorOption.keyConnectorUrl = response.keyConnectorUrl;
}
}
return decryptionOptions;
}
/**
* Initializes a new instance of the UserDecryptionOptions from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the UserDecryptionOptions. Will initialize even if the JSON object is nullish.
*/
static fromJSON(obj: Jsonify<UserDecryptionOptions>): UserDecryptionOptions {
const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj);
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON(
obj?.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON(
obj?.keyConnectorOption,
);
return decryptionOptions;
}
}

View File

@ -1 +1,2 @@
export * from "./domain"; export * from "./domain";
export * from "./spec";

View File

@ -0,0 +1,38 @@
import {
KeyConnectorUserDecryptionOption,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
} from "../domain";
// To discourage creating new user decryption options, we don't expose a constructor.
// These helpers are for testing purposes only.
/** Testing helper for creating new instances of `UserDecryptionOptions` */
export class FakeUserDecryptionOptions extends UserDecryptionOptions {
constructor(init: Partial<UserDecryptionOptions>) {
super();
Object.assign(this, init);
}
}
/** Testing helper for creating new instances of `KeyConnectorUserDecryptionOption` */
export class FakeKeyConnectorUserDecryptionOption extends KeyConnectorUserDecryptionOption {
constructor(keyConnectorUrl: string) {
super();
this.keyConnectorUrl = keyConnectorUrl;
}
}
/** Testing helper for creating new instances of `TrustedDeviceUserDecryptionOption` */
export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecryptionOption {
constructor(
hasAdminApproval: boolean,
hasLoginApprovingDevice: boolean,
hasManageResetPasswordPermission: boolean,
) {
super();
this.hasAdminApproval = hasAdminApproval;
this.hasLoginApprovingDevice = hasLoginApprovingDevice;
this.hasManageResetPasswordPermission = hasManageResetPasswordPermission;
}
}

View File

@ -0,0 +1 @@
export * from "./fake-user-decryption-options";

View File

@ -1,3 +1,4 @@
export * from "./pin-crypto/pin-crypto.service.implementation"; export * from "./pin-crypto/pin-crypto.service.implementation";
export * from "./login-strategies/login-strategy.service"; export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.service";
export * from "./auth-request/auth-request.service"; export * from "./auth-request/auth-request.service";

View File

@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums";
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { AuthRequestServiceAbstraction } from "../../abstractions"; import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../../abstractions";
import { PasswordLoginCredentials } from "../../models"; import { PasswordLoginCredentials } from "../../models";
import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service";
import { LoginStrategyService } from "./login-strategy.service"; import { LoginStrategyService } from "./login-strategy.service";
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
@ -51,6 +55,7 @@ describe("LoginStrategyService", () => {
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let stateProvider: FakeGlobalStateProvider; let stateProvider: FakeGlobalStateProvider;
@ -74,6 +79,7 @@ describe("LoginStrategyService", () => {
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
stateProvider = new FakeGlobalStateProvider(); stateProvider = new FakeGlobalStateProvider();
@ -95,6 +101,7 @@ describe("LoginStrategyService", () => {
policyService, policyService,
deviceTrustCryptoService, deviceTrustCryptoService,
authRequestService, authRequestService,
userDecryptionOptionsService,
stateProvider, stateProvider,
billingAccountProfileStateService, billingAccountProfileStateService,
); );

View File

@ -40,6 +40,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected policyService: PolicyService, protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected stateProvider: GlobalStateProvider, protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService, protected billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.userDecryptionOptionsService,
this.passwordStrengthService, this.passwordStrengthService,
this.policyService, this.policyService,
this, this,
@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.userDecryptionOptionsService,
this.keyConnectorService, this.keyConnectorService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.userDecryptionOptionsService,
this.environmentService, this.environmentService,
this.keyConnectorService, this.keyConnectorService,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.userDecryptionOptionsService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
); );
@ -420,6 +426,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.userDecryptionOptionsService,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
); );
} }

View File

@ -0,0 +1,94 @@
import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import {
USER_DECRYPTION_OPTIONS,
UserDecryptionOptionsService,
} from "./user-decryption-options.service";
describe("UserDecryptionOptionsService", () => {
let sut: UserDecryptionOptionsService;
const fakeUserId = Utils.newGuid() as UserId;
let fakeAccountService: FakeAccountService;
let fakeStateProvider: FakeStateProvider;
beforeEach(() => {
fakeAccountService = mockAccountServiceWith(fakeUserId);
fakeStateProvider = new FakeStateProvider(fakeAccountService);
sut = new UserDecryptionOptionsService(fakeStateProvider);
});
const userDecryptionOptions = {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
};
describe("userDecryptionOptions$", () => {
it("should return the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.userDecryptionOptions$);
expect(result).toEqual(userDecryptionOptions);
});
});
describe("hasMasterPassword$", () => {
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.hasMasterPassword$);
expect(result).toBe(true);
});
});
describe("userDecryptionOptionsById$", () => {
it("should return the user decryption options for the given user", async () => {
const givenUser = Utils.newGuid() as UserId;
await fakeAccountService.addAccount(givenUser, {
name: "Test User 1",
email: "test1@email.com",
status: AuthenticationStatus.Locked,
});
await fakeStateProvider.setUserState(
USER_DECRYPTION_OPTIONS,
userDecryptionOptions,
givenUser,
);
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
expect(result).toEqual(userDecryptionOptions);
});
});
describe("setUserDecryptionOptions", () => {
it("should set the active user's decryption options", async () => {
await sut.setUserDecryptionOptions(userDecryptionOptions);
const result = await firstValueFrom(
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
);
expect(result).toEqual(userDecryptionOptions);
});
});
});

View File

@ -0,0 +1,47 @@
import { map } from "rxjs";
import {
ActiveUserState,
StateProvider,
USER_DECRYPTION_OPTIONS_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/src/types/guid";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { UserDecryptionOptions } from "../../models";
export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptions>(
USER_DECRYPTION_OPTIONS_DISK,
"decryptionOptions",
{
deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions),
clearOn: ["logout"],
},
);
export class UserDecryptionOptionsService
implements InternalUserDecryptionOptionsServiceAbstraction
{
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
userDecryptionOptions$;
hasMasterPassword$;
constructor(private stateProvider: StateProvider) {
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
}
userDecryptionOptionsById$(userId: UserId) {
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
}
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
}
}

View File

@ -1,8 +1,11 @@
import { Observable } from "rxjs";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { DeviceKey, UserKey } from "../../types/key"; import { DeviceKey, UserKey } from "../../types/key";
import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DeviceResponse } from "../abstractions/devices/responses/device.response";
export abstract class DeviceTrustCryptoServiceAbstraction { export abstract class DeviceTrustCryptoServiceAbstraction {
supportsDeviceTrust$: Observable<boolean>;
/** /**
* @description Retrieves the users choice to trust the device which can only happen after decryption * @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset * Note: this value should only be used once and then reset
@ -20,6 +23,4 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
deviceKey?: DeviceKey, deviceKey?: DeviceKey,
) => Promise<UserKey | null>; ) => Promise<UserKey | null>;
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>; rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
supportsDeviceTrust: () => Promise<boolean>;
} }

View File

@ -8,7 +8,7 @@ export class AuthResult {
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
/** /**
* @deprecated * @deprecated
* Replace with using AccountDecryptionOptions to determine if the user does * Replace with using UserDecryptionOptions to determine if the user does
* not have a master password and is not using Key Connector. * not have a master password and is not using Key Connector.
* */ * */
resetMasterPassword = false; resetMasterPassword = false;

View File

@ -1,3 +0,0 @@
export class KeyConnectorUserDecryptionOption {
constructor(public keyConnectorUrl: string) {}
}

View File

@ -1,7 +0,0 @@
export class TrustedDeviceUserDecryptionOption {
constructor(
public hasAdminApproval: boolean,
public hasLoginApprovingDevice: boolean,
public hasManageResetPasswordPermission: boolean,
) {}
}

View File

@ -1,4 +1,6 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom, map, Observable } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { AppIdService } from "../../platform/abstractions/app-id.service"; import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
@ -21,6 +23,8 @@ import {
} from "../models/request/update-devices-trust.request"; } from "../models/request/update-devices-trust.request";
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
supportsDeviceTrust$: Observable<boolean>;
constructor( constructor(
private keyGenerationService: KeyGenerationService, private keyGenerationService: KeyGenerationService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
@ -31,7 +35,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
private devicesApiService: DevicesApiServiceAbstraction, private devicesApiService: DevicesApiServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
) {} private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
) {
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
map((options) => options?.trustedDeviceOption != null ?? false),
);
}
/** /**
* @description Retrieves the users choice to trust the device which can only happen after decryption * @description Retrieves the users choice to trust the device which can only happen after decryption
@ -203,9 +212,4 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
return null; return null;
} }
} }
async supportsDeviceTrust(): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions();
return decryptionOptions?.trustedDeviceOption != null;
}
} }

View File

@ -1,6 +1,9 @@
import { matches, mock } from "jest-mock-extended"; import { matches, mock } from "jest-mock-extended";
import { of } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
import { DeviceType } from "../../enums"; import { DeviceType } from "../../enums";
import { AppIdService } from "../../platform/abstractions/app-id.service"; import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
@ -34,10 +37,16 @@ describe("deviceTrustCryptoService", () => {
const devicesApiService = mock<DevicesApiServiceAbstraction>(); const devicesApiService = mock<DevicesApiServiceAbstraction>();
const i18nService = mock<I18nService>(); const i18nService = mock<I18nService>();
const platformUtilsService = mock<PlatformUtilsService>(); const platformUtilsService = mock<PlatformUtilsService>();
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
decryptionOptions.next({} as any);
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
deviceTrustCryptoService = new DeviceTrustCryptoService( deviceTrustCryptoService = new DeviceTrustCryptoService(
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -48,6 +57,7 @@ describe("deviceTrustCryptoService", () => {
devicesApiService, devicesApiService,
i18nService, i18nService,
platformUtilsService, platformUtilsService,
userDecryptionOptionsService,
); );
}); });

View File

@ -1,3 +1,7 @@
import { firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction"; import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
@ -33,6 +37,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private cryptoService: CryptoService, private cryptoService: CryptoService,
private i18nService: I18nService, private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction, private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinCryptoService: PinCryptoServiceAbstraction, private pinCryptoService: PinCryptoServiceAbstraction,
private logService: LogService, private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
@ -135,7 +140,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
case VerificationType.MasterPassword: case VerificationType.MasterPassword:
return this.verifyUserByMasterPassword(verification); return this.verifyUserByMasterPassword(verification);
case VerificationType.PIN: case VerificationType.PIN:
return this.verifyUserByPIN(verification);
break; break;
case VerificationType.Biometrics: case VerificationType.Biometrics:
return this.verifyUserByBiometrics(); return this.verifyUserByBiometrics();
@ -210,16 +214,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
* Note: This only checks the server, not the local state * Note: This only checks the server, not the local state
* @param userId The user id to check. If not provided, the current user is used * @param userId The user id to check. If not provided, the current user is used
* @returns True if the user has a master password * @returns True if the user has a master password
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
*/ */
async hasMasterPassword(userId?: string): Promise<boolean> { async hasMasterPassword(userId?: string): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId }); if (userId) {
const decryptionOptions = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
);
if (decryptionOptions?.hasMasterPassword != undefined) { if (decryptionOptions?.hasMasterPassword != undefined) {
return decryptionOptions.hasMasterPassword; return decryptionOptions.hasMasterPassword;
}
} }
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
// TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0
return !(await this.stateService.getUsesKeyConnector({ userId }));
} }
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> { async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {

View File

@ -18,7 +18,7 @@ import { CipherView } from "../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { KdfType } from "../enums"; import { KdfType } from "../enums";
import { ServerConfigData } from "../models/data/server-config.data"; import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountDecryptionOptions } from "../models/domain/account"; import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@ -180,13 +180,6 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>; ) => Promise<void>;
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>; getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>; setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
getAccountDecryptionOptions: (
options?: StorageOptions,
) => Promise<AccountDecryptionOptions | null>;
setAccountDecryptionOptions: (
value: AccountDecryptionOptions,
options?: StorageOptions,
) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>; getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>; setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>; getEmailVerified: (options?: StorageOptions) => Promise<boolean>;

View File

@ -2,9 +2,6 @@ import { Jsonify } from "type-fest";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { GeneratorOptions } from "../../../tools/generator/generator-options";
import { import {
@ -235,103 +232,12 @@ export class AccountTokens {
} }
} }
export class AccountDecryptionOptions {
hasMasterPassword: boolean;
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
keyConnectorOption?: KeyConnectorUserDecryptionOption;
constructor(init?: Partial<AccountDecryptionOptions>) {
if (init) {
Object.assign(this, init);
}
}
// TODO: these nice getters don't work because the Account object is not properly being deserialized out of
// JSON (the Account static fromJSON method is not running) so these getters don't exist on the
// account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on
// get hasTrustedDeviceOption(): boolean {
// return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined;
// }
// get hasKeyConnectorOption(): boolean {
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
// }
static fromResponse(response: IdentityTokenResponse): AccountDecryptionOptions {
if (response == null) {
return null;
}
const accountDecryptionOptions = new AccountDecryptionOptions();
if (response.userDecryptionOptions) {
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
// the new decryption options.
const responseOptions = response.userDecryptionOptions;
accountDecryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
if (responseOptions.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
responseOptions.trustedDeviceOption.hasAdminApproval,
responseOptions.trustedDeviceOption.hasLoginApprovingDevice,
responseOptions.trustedDeviceOption.hasManageResetPasswordPermission,
);
}
if (responseOptions.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
responseOptions.keyConnectorOption.keyConnectorUrl,
);
}
} else {
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
// we must base our decryption options on the presence of the keyConnectorUrl.
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
const usingKeyConnector = response.keyConnectorUrl != null;
accountDecryptionOptions.hasMasterPassword = !usingKeyConnector;
if (usingKeyConnector) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
response.keyConnectorUrl,
);
}
}
return accountDecryptionOptions;
}
static fromJSON(obj: Jsonify<AccountDecryptionOptions>): AccountDecryptionOptions {
if (obj == null) {
return null;
}
const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj);
if (obj.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
obj.trustedDeviceOption.hasAdminApproval,
obj.trustedDeviceOption.hasLoginApprovingDevice,
obj.trustedDeviceOption.hasManageResetPasswordPermission,
);
}
if (obj.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
obj.keyConnectorOption.keyConnectorUrl,
);
}
return accountDecryptionOptions;
}
}
export class Account { export class Account {
data?: AccountData = new AccountData(); data?: AccountData = new AccountData();
keys?: AccountKeys = new AccountKeys(); keys?: AccountKeys = new AccountKeys();
profile?: AccountProfile = new AccountProfile(); profile?: AccountProfile = new AccountProfile();
settings?: AccountSettings = new AccountSettings(); settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens(); tokens?: AccountTokens = new AccountTokens();
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null; adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
constructor(init: Partial<Account>) { constructor(init: Partial<Account>) {
@ -356,10 +262,6 @@ export class Account {
...new AccountTokens(), ...new AccountTokens(),
...init?.tokens, ...init?.tokens,
}, },
decryptionOptions: {
...new AccountDecryptionOptions(),
...init?.decryptionOptions,
},
adminAuthRequest: init?.adminAuthRequest, adminAuthRequest: init?.adminAuthRequest,
}); });
} }
@ -375,7 +277,6 @@ export class Account {
profile: AccountProfile.fromJSON(json?.profile), profile: AccountProfile.fromJSON(json?.profile),
settings: AccountSettings.fromJSON(json?.settings), settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens), tokens: AccountTokens.fromJSON(json?.tokens),
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest), adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
}); });
} }

View File

@ -34,12 +34,7 @@ import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
import { StateFactory } from "../factories/state-factory"; import { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { ServerConfigData } from "../models/data/server-config.data"; import { ServerConfigData } from "../models/data/server-config.data";
import { import { Account, AccountData, AccountSettings } from "../models/domain/account";
Account,
AccountData,
AccountDecryptionOptions,
AccountSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state"; import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state"; import { State } from "../models/domain/state";
@ -817,37 +812,6 @@ export class StateService<
await this.saveAccount(account, options); await this.saveAccount(account, options);
} }
async getAccountDecryptionOptions(
options?: StorageOptions,
): Promise<AccountDecryptionOptions | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.decryptionOptions as AccountDecryptionOptions;
}
async setAccountDecryptionOptions(
value: AccountDecryptionOptions,
options?: StorageOptions,
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.decryptionOptions = value;
await this.saveAccount(account, options);
}
async getEmail(options?: StorageOptions): Promise<string> { async getEmail(options?: StorageOptions): Promise<string> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@ -44,6 +44,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
}); });
export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TOKEN_MEMORY = new StateDefinition("token", "memory");
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
// Autofill // Autofill

View File

@ -1,5 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs"; import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
import {
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy"; import { Policy } from "../../admin-console/models/domain/policy";
@ -8,12 +13,12 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
import { AccountDecryptionOptions } from "../../platform/models/domain/account";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => { describe("VaultTimeoutSettingsService", () => {
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>; let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
@ -21,12 +26,26 @@ describe("VaultTimeoutSettingsService", () => {
const biometricStateService = mock<BiometricStateService>(); const biometricStateService = mock<BiometricStateService>();
let service: VaultTimeoutSettingsService; let service: VaultTimeoutSettingsService;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => { beforeEach(() => {
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>(); tokenService = mock<TokenService>();
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
userDecryptionOptionsSubject = new BehaviorSubject(null);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
service = new VaultTimeoutSettingsService( service = new VaultTimeoutSettingsService(
userDecryptionOptionsService,
cryptoService, cryptoService,
tokenService, tokenService,
policyService, policyService,
@ -49,9 +68,7 @@ describe("VaultTimeoutSettingsService", () => {
}); });
it("contains Lock when the user has a master password", async () => { it("contains Lock when the user has a master password", async () => {
stateService.getAccountDecryptionOptions.mockResolvedValue( userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
new AccountDecryptionOptions({ hasMasterPassword: true }),
);
const result = await firstValueFrom(service.availableVaultTimeoutActions$()); const result = await firstValueFrom(service.availableVaultTimeoutActions$());
@ -83,9 +100,7 @@ describe("VaultTimeoutSettingsService", () => {
}); });
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => { it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
stateService.getAccountDecryptionOptions.mockResolvedValue( userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
new AccountDecryptionOptions({ hasMasterPassword: false }),
);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null); stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
stateService.getProtectedPin.mockResolvedValue(null); stateService.getProtectedPin.mockResolvedValue(null);
biometricStateService.biometricUnlockEnabled$ = of(false); biometricStateService.biometricUnlockEnabled$ = of(false);
@ -107,9 +122,7 @@ describe("VaultTimeoutSettingsService", () => {
`( `(
"returns $expected when policy is $policy, and user preference is $userPreference", "returns $expected when policy is $policy, and user preference is $userPreference",
async ({ policy, userPreference, expected }) => { async ({ policy, userPreference, expected }) => {
stateService.getAccountDecryptionOptions.mockResolvedValue( userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
new AccountDecryptionOptions({ hasMasterPassword: true }),
);
policyService.getAll$.mockReturnValue( policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
); );
@ -136,8 +149,8 @@ describe("VaultTimeoutSettingsService", () => {
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference", "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
async ({ unlockMethod, policy, userPreference, expected }) => { async ({ unlockMethod, policy, userPreference, expected }) => {
biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); biometricStateService.biometricUnlockEnabled$ = of(unlockMethod);
stateService.getAccountDecryptionOptions.mockResolvedValue( userDecryptionOptionsSubject.next(
new AccountDecryptionOptions({ hasMasterPassword: false }), new UserDecryptionOptions({ hasMasterPassword: false }),
); );
policyService.getAll$.mockReturnValue( policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),

View File

@ -1,5 +1,7 @@
import { defer, firstValueFrom } from "rxjs"; import { defer, firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums"; import { PolicyType } from "../../admin-console/enums";
@ -19,6 +21,7 @@ export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT";
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor( constructor(
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private tokenService: TokenService, private tokenService: TokenService,
private policyService: PolicyService, private policyService: PolicyService,
@ -174,12 +177,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
} }
private async userHasMasterPassword(userId: string): Promise<boolean> { private async userHasMasterPassword(userId: string): Promise<boolean> {
const acctDecryptionOpts = await this.stateService.getAccountDecryptionOptions({ if (userId) {
userId: userId, const decryptionOptions = await firstValueFrom(
}); this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
);
if (acctDecryptionOpts?.hasMasterPassword != undefined) { if (decryptionOptions?.hasMasterPassword != undefined) {
return acctDecryptionOpts.hasMasterPassword; return decryptionOptions.hasMasterPassword;
}
} }
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
} }
} }

View File

@ -39,6 +39,7 @@ import { OrganizationMigrator } from "./migrations/40-move-organization-state-to
import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider"; import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider";
import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider"; import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider";
import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider"; import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider";
import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
@ -47,7 +48,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 43; export const CURRENT_VERSION = 44;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -92,7 +93,8 @@ export function createMigrationBuilder() {
.with(OrganizationMigrator, 39, 40) .with(OrganizationMigrator, 39, 40)
.with(EventCollectionMigrator, 40, 41) .with(EventCollectionMigrator, 40, 41)
.with(EnableFaviconMigrator, 41, 42) .with(EnableFaviconMigrator, 41, 42)
.with(AutoConfirmFingerPrintsMigrator, 42, CURRENT_VERSION); .with(AutoConfirmFingerPrintsMigrator, 42, 43)
.with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@ -0,0 +1,238 @@
import { any, MockProxy } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { UserDecryptionOptionsMigrator } from "./44-move-user-decryption-options-to-state-provider";
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
FirstAccount: {
decryptionOptions: {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
},
profile: {
otherStuff: "overStuff2",
},
otherStuff: "otherStuff3",
},
SecondAccount: {
decryptionOptions: {
hasMasterPassword: false,
trustedDeviceOption: {
hasAdminApproval: true,
hasLoginApprovingDevice: true,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://selfhosted.bitwarden.com",
},
},
profile: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
function rollbackJSON() {
return {
user_FirstAccount_decryptionOptions_userDecryptionOptions: {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
},
user_SecondAccount_decryptionOptions_userDecryptionOptions: {
hasMasterPassword: false,
trustedDeviceOption: {
hasAdminApproval: true,
hasLoginApprovingDevice: true,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://selfhosted.bitwarden.com",
},
},
user_ThirdAccount_decryptionOptions_userDecryptionOptions: {},
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
FirstAccount: {
decryptionOptions: {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
},
profile: {
otherStuff: "overStuff2",
},
otherStuff: "otherStuff3",
},
SecondAccount: {
decryptionOptions: {
hasMasterPassword: false,
trustedDeviceOption: {
hasAdminApproval: true,
hasLoginApprovingDevice: true,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://selfhosted.bitwarden.com",
},
},
profile: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
describe("UserDecryptionOptionsMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: UserDecryptionOptionsMigrator;
const keyDefinitionLike = {
key: "decryptionOptions",
stateDefinition: {
name: "userDecryptionOptions",
},
};
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 43);
sut = new UserDecryptionOptionsMigrator(43, 44);
});
it("should remove decryptionOptions from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
profile: {
otherStuff: "overStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
profile: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
});
});
it("should set decryptionOptions provider value for each account", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", keyDefinitionLike, {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
});
expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", keyDefinitionLike, {
hasMasterPassword: false,
trustedDeviceOption: {
hasAdminApproval: true,
hasLoginApprovingDevice: true,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://selfhosted.bitwarden.com",
},
});
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 44);
sut = new UserDecryptionOptionsMigrator(43, 44);
});
it.each(["FirstAccount", "SecondAccount", "ThirdAccount"])(
"should null out new values",
async (userId) => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
},
);
it("should add explicit value back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
decryptionOptions: {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
},
profile: {
otherStuff: "overStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
decryptionOptions: {
hasMasterPassword: false,
trustedDeviceOption: {
hasAdminApproval: true,
hasLoginApprovingDevice: true,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://selfhosted.bitwarden.com",
},
},
profile: {
otherStuff: "otherStuff4",
},
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,57 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type DecryptionOptionsType = {
hasMasterPassword: boolean;
trustedDeviceOption?: {
hasAdminApproval: boolean;
hasLoginApprovingDevice: boolean;
hasManageResetPasswordPermission: boolean;
};
keyConnectorOption?: {
keyConnectorUrl: string;
};
};
type ExpectedAccountType = {
decryptionOptions?: DecryptionOptionsType;
};
const USER_DECRYPTION_OPTIONS: KeyDefinitionLike = {
key: "decryptionOptions",
stateDefinition: {
name: "userDecryptionOptions",
},
};
export class UserDecryptionOptionsMigrator extends Migrator<43, 44> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const value = account?.decryptionOptions;
if (value != null) {
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, value);
delete account.decryptionOptions;
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 value: DecryptionOptionsType = await helper.getFromUser(
userId,
USER_DECRYPTION_OPTIONS,
);
if (account) {
account.decryptionOptions = Object.assign(account.decryptionOptions, value);
await helper.set(userId, account);
}
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, null);
}
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
}
}

View File

@ -1,3 +1,7 @@
import { firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "../../../abstractions/api.service"; import { ApiService } from "../../../abstractions/api.service";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
@ -24,7 +28,6 @@ import { LogService } from "../../../platform/abstractions/log.service";
import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { MessagingService } from "../../../platform/abstractions/messaging.service";
import { StateService } from "../../../platform/abstractions/state.service"; import { StateService } from "../../../platform/abstractions/state.service";
import { sequentialize } from "../../../platform/misc/sequentialize"; import { sequentialize } from "../../../platform/misc/sequentialize";
import { AccountDecryptionOptions } from "../../../platform/models/domain/account";
import { SendData } from "../../../tools/send/models/data/send.data"; import { SendData } from "../../../tools/send/models/data/send.data";
import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendResponse } from "../../../tools/send/models/response/send.response";
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
@ -62,6 +65,7 @@ export class SyncService implements SyncServiceAbstraction {
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private organizationService: InternalOrganizationServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction,
private sendApiService: SendApiService, private sendApiService: SendApiService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private avatarService: AvatarService, private avatarService: AvatarService,
private logoutCallback: (expired: boolean) => Promise<void>, private logoutCallback: (expired: boolean) => Promise<void>,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
@ -353,19 +357,12 @@ export class SyncService implements SyncServiceAbstraction {
); );
} }
const acctDecryptionOpts: AccountDecryptionOptions = const userDecryptionOptions = await firstValueFrom(
await this.stateService.getAccountDecryptionOptions(); this.userDecryptionOptionsService.userDecryptionOptions$,
);
// Account decryption options should never be null or undefined b/c it is always initialized if (userDecryptionOptions === null || userDecryptionOptions === undefined) {
// during the processing of the ID token response, but there might be a state issue
// where it is being overwritten with undefined affecting browser extension + FireFox users.
// TODO: Consider removing this once we figure out the root cause of the state issue or after the state provider refactor.
if (acctDecryptionOpts === null || acctDecryptionOpts === undefined) {
this.logService.error("Sync: Account decryption options are null or undefined."); this.logService.error("Sync: Account decryption options are null or undefined.");
// Early return as a bandaid to allow the rest of the sync to continue so users can access
// their data that they might have added from another device.
// Otherwise, trying to access properties on undefined below will throw an error.
return;
} }
// Even though TDE users should only be in a single org (per single org policy), check // Even though TDE users should only be in a single org (per single org policy), check
@ -384,8 +381,8 @@ export class SyncService implements SyncServiceAbstraction {
} }
if ( if (
acctDecryptionOpts.trustedDeviceOption !== undefined && userDecryptionOptions.trustedDeviceOption !== undefined &&
!acctDecryptionOpts.hasMasterPassword && !userDecryptionOptions.hasMasterPassword &&
hasManageResetPasswordPermission hasManageResetPasswordPermission
) { ) {
// TDE user w/out MP went from having no password reset permission to having it. // TDE user w/out MP went from having no password reset permission to having it.