diff --git a/.eslintrc.json b/.eslintrc.json index 47d45f23c3b..f21e2b08725 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -191,6 +191,15 @@ ] } }, + { + "files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] } + ] + } + }, { "files": ["libs/importer/src/**/*.ts"], "rules": { diff --git a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts index d0a4d2db1b5..fa52ca6231c 100644 --- a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts @@ -15,8 +15,8 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { - messagingServiceFactory, MessagingServiceInitOptions, + messagingServiceFactory, } from "../../../platform/background/service-factories/messaging-service.factory"; import { StateServiceInitOptions, diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts index 6b8d3c09e33..5916f38441f 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts @@ -43,6 +43,11 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { + UserDecryptionOptionsServiceInitOptions, + userDecryptionOptionsServiceFactory, +} from "./user-decryption-options-service.factory"; + type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & @@ -54,7 +59,8 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor AppIdServiceInitOptions & DevicesApiServiceInitOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + UserDecryptionOptionsServiceInitOptions; export function deviceTrustCryptoServiceFactory( cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, @@ -75,6 +81,7 @@ export function deviceTrustCryptoServiceFactory( await devicesApiServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await userDecryptionOptionsServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index b0ae87a75f7..2cc4692ca9c 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -9,7 +9,10 @@ import { ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-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 { CryptoServiceInitOptions, cryptoServiceFactory, @@ -70,6 +73,10 @@ import { } from "./key-connector-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; +import { + internalUserDecryptionOptionServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "./user-decryption-options-service.factory"; type LoginStrategyServiceFactoryOptions = FactoryOptions; @@ -90,7 +97,9 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions PasswordStrengthServiceInitOptions & DeviceTrustCryptoServiceInitOptions & AuthRequestServiceInitOptions & - GlobalStateProviderInitOptions; + UserDecryptionOptionsServiceInitOptions & + GlobalStateProviderInitOptions & + BillingAccountProfileStateServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -119,6 +128,7 @@ export function loginStrategyServiceFactory( await policyServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), + await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), ), diff --git a/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts new file mode 100644 index 00000000000..549639a3c78 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts @@ -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 { + return factory( + cache, + "userDecryptionOptionsService", + opts, + async () => new UserDecryptionOptionsService(await stateProviderFactory(cache, opts)), + ); +} + +export async function internalUserDecryptionOptionServiceFactory( + cache: { + userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction; + } & CachedServices, + opts: UserDecryptionOptionsServiceInitOptions, +): Promise { + return (await userDecryptionOptionsServiceFactory( + cache, + opts, + )) as InternalUserDecryptionOptionsServiceAbstraction; +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index ff08ddf689f..e8be9099cac 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -32,6 +32,10 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; +import { + userDecryptionOptionsServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "./user-decryption-options-service.factory"; import { UserVerificationApiServiceInitOptions, userVerificationApiServiceFactory, @@ -44,6 +48,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO CryptoServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & + UserDecryptionOptionsServiceInitOptions & PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & @@ -63,6 +68,7 @@ export function userVerificationServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), + await userDecryptionOptionsServiceFactory(cache, opts), await pinCryptoServiceFactory(cache, opts), await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index ac98966b4ab..ea1cacc7ac5 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -37,6 +38,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent { route: ActivatedRoute, organizationApiService: OrganizationApiServiceAbstraction, organizationUserService: OrganizationUserService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, ) { @@ -55,6 +57,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent { stateService, organizationApiService, organizationUserService, + userDecryptionOptionsService, ssoLoginService, dialogService, ); diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 19d7977819f..7b61a04bfda 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -39,6 +42,7 @@ export class SsoComponent extends BaseSsoComponent { syncService: SyncService, environmentService: EnvironmentService, logService: LogService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, protected authService: AuthService, @Inject(WINDOW) private win: Window, @@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService, passwordGenerationService, logService, + userDecryptionOptionsService, configService, ); diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index d3d7b7f33a8..bad2e4a9e77 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,9 +17,10 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, private activatedRoute: ActivatedRoute, ) { - super(twoFactorService, router, i18nService, platformUtilsService, window); + super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); } close() { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index e511122f9a5..0a950d6c1b5 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -5,7 +5,10 @@ import { filter, first, takeUntil } from "rxjs/operators"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; 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 { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -55,6 +58,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService: TwoFactorService, appIdService: AppIdService, loginService: LoginService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, @@ -75,6 +79,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService, appIdService, loginService, + userDecryptionOptionsService, ssoLoginService, configService, ); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index ec889a51773..36760989c5f 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -625,6 +625,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * Updates the position of the overlay button. */ private updateOverlayButtonPosition() { + // if (!this.overlayButtonElement) { + // this.createAutofillOverlayButton(); + // this.updateCustomElementDefaultStyles(this.overlayButtonElement); + // } + // + // if (!this.isOverlayButtonVisible) { + // this.appendOverlayElementToBody(this.overlayButtonElement); + // this.isOverlayButtonVisible = true; + // this.setOverlayRepositionEventListeners(); + // } + // // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // // eslint-disable-next-line @typescript-eslint/no-floating-promises + // this.sendExtensionMessage("updateAutofillOverlayPosition", { + // overlayElement: AutofillOverlayElement.Button, + // }); + void this.sendExtensionMessage("updateAutofillOverlayPosition", { overlayElement: AutofillOverlayElement.Button, }); @@ -634,6 +650,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * Updates the position of the overlay list. */ private updateOverlayListPosition() { + // if (!this.overlayListElement) { + // this.createAutofillOverlayList(); + // this.updateCustomElementDefaultStyles(this.overlayListElement); + // } + // + // if (!this.isOverlayListVisible) { + // this.appendOverlayElementToBody(this.overlayListElement); + // this.isOverlayListVisible = true; + // } + // + // // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // // eslint-disable-next-line @typescript-eslint/no-floating-promises + // this.sendExtensionMessage("updateAutofillOverlayPosition", { + // overlayElement: AutofillOverlayElement.List, + // }); + void this.sendExtensionMessage("updateAutofillOverlayPosition", { overlayElement: AutofillOverlayElement.List, }); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b3b9be3dcd4..b34f07f657f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -5,6 +5,8 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + InternalUserDecryptionOptionsServiceAbstraction, + UserDecryptionOptionsService, AuthRequestServiceAbstraction, AuthRequestService, } from "@bitwarden/auth/common"; @@ -242,6 +244,7 @@ export default class MainBackground { environmentService: BrowserEnvironmentService; cipherService: CipherServiceAbstraction; folderService: InternalFolderServiceAbstraction; + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; collectionService: CollectionServiceAbstraction; vaultTimeoutService: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; @@ -539,6 +542,8 @@ export default class MainBackground { }; })(); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.keyGenerationService, @@ -550,6 +555,7 @@ export default class MainBackground { this.devicesApiService, this.i18nService, this.platformUtilsService, + this.userDecryptionOptionsService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); @@ -590,6 +596,7 @@ export default class MainBackground { this.policyService, this.deviceTrustCryptoService, this.authRequestService, + this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, ); @@ -631,6 +638,7 @@ export default class MainBackground { this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.userDecryptionOptionsService, this.cryptoService, this.tokenService, this.policyService, @@ -650,6 +658,7 @@ export default class MainBackground { this.cryptoService, this.i18nService, this.userVerificationApiService, + this.userDecryptionOptionsService, this.pinCryptoService, this.logService, this.vaultTimeoutSettingsService, @@ -717,6 +726,7 @@ export default class MainBackground { this.folderApiService, this.organizationService, this.sendApiService, + this.userDecryptionOptionsService, this.avatarService, logoutCallback, this.billingAccountProfileStateService, diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index febc605bc8a..92a1d83dd25 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -9,6 +9,10 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../auth/background/service-factories/token-service.factory"; +import { + userDecryptionOptionsServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "../../auth/background/service-factories/user-decryption-options-service.factory"; import { biometricStateServiceFactory, BiometricStateServiceInitOptions, @@ -30,6 +34,7 @@ import { type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions & + UserDecryptionOptionsServiceInitOptions & CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & @@ -46,6 +51,7 @@ export function vaultTimeoutSettingsServiceFactory( opts, async () => new VaultTimeoutSettingsService( + await userDecryptionOptionsServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index e438f2588b6..d179868448b 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -16,6 +16,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { AccountComponent } from "../auth/popup/account-switching/account.component"; @@ -107,6 +108,7 @@ import "../platform/popup/locales"; AvatarModule, AccountComponent, ButtonModule, + ExportScopeCalloutComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 52de0303fae..5e080adf16b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -82,7 +82,6 @@ import { import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { @@ -96,7 +95,6 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { DialogService } from "@bitwarden/components"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { UnauthGuardService } from "../../auth/popup/services"; @@ -292,11 +290,6 @@ function getBgService(service: keyof MainBackground) { }, deps: [DomSanitizer, ToastrService], }, - { - provide: PasswordStrengthServiceAbstraction, - useFactory: getBgService("passwordStrengthService"), - deps: [], - }, { provide: PasswordGenerationServiceAbstraction, useFactory: getBgService("passwordGenerationService"), @@ -350,11 +343,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("autofillService"), deps: [], }, - { - provide: ImportServiceAbstraction, - useFactory: getBgService("importService"), - deps: [], - }, { provide: VaultExportServiceAbstraction, useFactory: getBgService("exportService"), diff --git a/apps/browser/src/tools/popup/settings/export.component.html b/apps/browser/src/tools/popup/settings/export.component.html index db072d6b504..aae3584f6c7 100644 --- a/apps/browser/src/tools/popup/settings/export.component.html +++ b/apps/browser/src/tools/popup/settings/export.component.html @@ -19,7 +19,7 @@ {{ "personalVaultExportPolicyInEffect" | i18n }} - +
diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index acbdab36852..c8f85a8b7ab 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,11 +22,19 @@ export class CollectionsComponent extends BaseCollectionsComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, cipherService: CipherService, + organizationService: OrganizationService, private route: ActivatedRoute, private location: Location, logService: LogService, ) { - super(collectionService, platformUtilsService, i18nService, cipherService, logService); + super( + collectionService, + platformUtilsService, + i18nService, + cipherService, + organizationService, + logService, + ); } async ngOnInit() { diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index a4176be0b00..694246f59a1 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -20,6 +20,7 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/platform": ["../../libs/platform/src"], diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 95e306bfc0e..55bc46e41e5 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -5,11 +5,13 @@ import { program } from "commander"; import * as jsdom from "jsdom"; import { + InternalUserDecryptionOptionsServiceAbstraction, AuthRequestService, LoginStrategyService, LoginStrategyServiceAbstraction, PinCryptoService, PinCryptoServiceAbstraction, + UserDecryptionOptionsService, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -169,6 +171,7 @@ export class Main { eventUploadService: EventUploadServiceAbstraction; passwordGenerationService: PasswordGenerationServiceAbstraction; passwordStrengthService: PasswordStrengthServiceAbstraction; + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; totpService: TotpService; containerService: ContainerService; auditService: AuditService; @@ -436,6 +439,8 @@ export class Main { this.stateService, ); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.keyGenerationService, @@ -447,6 +452,7 @@ export class Main { this.devicesApiService, this.i18nService, this.platformUtilsService, + this.userDecryptionOptionsService, ); this.authRequestService = new AuthRequestService( @@ -478,6 +484,7 @@ export class Main { this.policyService, this.deviceTrustCryptoService, this.authRequestService, + this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, ); @@ -529,6 +536,7 @@ export class Main { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.userDecryptionOptionsService, this.cryptoService, this.tokenService, this.policyService, @@ -548,6 +556,7 @@ export class Main { this.cryptoService, this.i18nService, this.userVerificationApiService, + this.userDecryptionOptionsService, this.pinCryptoService, this.logService, this.vaultTimeoutSettingsService, @@ -589,6 +598,7 @@ export class Main { this.folderApiService, this.organizationService, this.sendApiService, + this.userDecryptionOptionsService, this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 138394886d3..89170f4cc38 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 2542e4d214a..48536934eea 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -16,7 +16,7 @@ manual_test = [] aes = "=0.8.4" anyhow = "=1.0.80" arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] } -base64 = "=0.21.5" +base64 = "=0.22.0" cbc = { version = "=0.1.2", features = ["alloc"] } napi = { version = "=2.13.3", features = ["async"] } napi-derive = "=2.13.0" diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 7a61a55ccff..b35d3ed2254 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -412,6 +412,23 @@ "enableBrowserIntegrationFingerprintDesc" | i18n }}
+
+
+ +
+ {{ + "enableHardwareAccelerationDesc" | i18n + }} +
- + {{ "couldNotCompleteBiometrics" | i18n }} - + diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index 4f216a76e8c..8cb40d94524 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -20,6 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, ButtonModule, + CalloutModule, FormFieldModule, IconButtonModule, IconModule, @@ -62,6 +63,7 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt IconModule, LinkModule, ButtonModule, + CalloutModule, ], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index c4b5bed1284..1feee6695a9 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -1,3 +1,4 @@ export * from "./pin-crypto.service.abstraction"; export * from "./login-strategy.service"; +export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; diff --git a/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts new file mode 100644 index 00000000000..e46fb09cff6 --- /dev/null +++ b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts @@ -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; + /** + * 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; + + /** + * Returns the user decryption options for the given user id. + * @param userId The user id to check. + */ + abstract userDecryptionOptionsById$(userId: string): Observable; +} + +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; +} diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 18ac9f0bf78..53722cd259c 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; 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 { @@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptions: MockProxy; let deviceTrustCryptoService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptions = mock(); deviceTrustCryptoService = mock(); billingAccountProfileStateService = mock(); @@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptions, deviceTrustCryptoService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 09312226d8b..c42f43e7643 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { @@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 6f3d480f201..ed40797df51 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -28,7 +28,6 @@ import { AccountProfile, AccountTokens, AccountKeys, - AccountDecryptionOptions, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -39,8 +38,10 @@ import { import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key"; -import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service"; -import { PasswordLoginCredentials } from "../models/domain/login-credentials"; +import { LoginStrategyServiceAbstraction } from "../abstractions"; +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"; @@ -108,6 +109,7 @@ describe("LoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -126,7 +128,7 @@ describe("LoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); - + userDecryptionOptionsService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -146,6 +148,7 @@ describe("LoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, @@ -204,9 +207,11 @@ describe("LoginStrategy", () => { ...new AccountTokens(), }, keys: new AccountKeys(), - decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse), }), ); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + UserDecryptionOptions.fromResponse(idTokenResponse), + ); expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); @@ -409,6 +414,7 @@ describe("LoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index f5f28dd0440..eef5626493b 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -30,9 +30,9 @@ import { Account, AccountProfile, AccountTokens, - AccountDecryptionOptions, } from "@bitwarden/common/platform/models/domain/account"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -40,6 +40,7 @@ import { AuthRequestLoginCredentials, WebAuthnLoginCredentials, } from "../models/domain/login-credentials"; +import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; @@ -69,6 +70,7 @@ export abstract class LoginStrategy { protected logService: LogService, protected stateService: StateService, protected twoFactorService: TwoFactorService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, ) {} @@ -203,11 +205,14 @@ export abstract class LoginStrategy { ...new AccountTokens(), }, keys: accountKeys, - decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse), adminAuthRequest: adminAuthRequest?.toJSON(), }), ); + await this.userDecryptionOptionsService.setUserDecryptionOptions( + UserDecryptionOptions.fromResponse(tokenResponse), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 007c33afc6b..470a4ac713c 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 2104595b450..be93d39ebc4 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -84,6 +85,7 @@ export class PasswordLoginStrategy extends LoginStrategy { logService: LogService, protected stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private passwordStrengthService: PasswordStrengthServiceAbstraction, private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, @@ -99,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index c987bcc95a6..d4b0b13eafe 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; 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 { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; let deviceTrustCryptoService: MockProxy; let authRequestService: MockProxy; @@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); keyConnectorService = mock(); deviceTrustCryptoService = mock(); authRequestService = mock(); @@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, keyConnectorService, deviceTrustCryptoService, authRequestService, diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index b8d1df6f577..04f158d30a9 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -21,7 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AuthRequestServiceAbstraction } from "../abstractions"; +import { + InternalUserDecryptionOptionsServiceAbstraction, + AuthRequestServiceAbstraction, +} from "../abstractions"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -84,6 +87,7 @@ export class SsoLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private keyConnectorService: KeyConnectorService, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, @@ -100,6 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 48f6fd32aba..432eeb9aab9 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; 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 { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -35,6 +36,7 @@ describe("UserApiLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); @@ -76,6 +79,7 @@ describe("UserApiLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, environmentService, keyConnectorService, billingAccountProfileStateService, diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 9bb6d8fb125..730243aafc6 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, @@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9ab64170c1d..edc1441361a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-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 { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => { let logService!: MockProxy; let stateService!: MockProxy; let twoFactorService!: MockProxy; + let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index b60342f0b41..a8e67597b82 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -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 { UserKey } from "@bitwarden/common/types/key"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( @@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/models/domain/index.ts b/libs/auth/src/common/models/domain/index.ts index c3166f737d6..b8b83711a4a 100644 --- a/libs/auth/src/common/models/domain/index.ts +++ b/libs/auth/src/common/models/domain/index.ts @@ -1,2 +1,3 @@ export * from "./rotateable-key-set"; export * from "./login-credentials"; +export * from "./user-decryption-options"; diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts new file mode 100644 index 00000000000..c600c8be476 --- /dev/null +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -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 { + 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 { + 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 { + const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj); + + decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON( + obj?.trustedDeviceOption, + ); + + decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON( + obj?.keyConnectorOption, + ); + + return decryptionOptions; + } +} diff --git a/libs/auth/src/common/models/index.ts b/libs/auth/src/common/models/index.ts index 7886141dc94..e816ee4b883 100644 --- a/libs/auth/src/common/models/index.ts +++ b/libs/auth/src/common/models/index.ts @@ -1 +1,2 @@ export * from "./domain"; +export * from "./spec"; diff --git a/libs/auth/src/common/models/spec/fake-user-decryption-options.ts b/libs/auth/src/common/models/spec/fake-user-decryption-options.ts new file mode 100644 index 00000000000..fe4a1203c67 --- /dev/null +++ b/libs/auth/src/common/models/spec/fake-user-decryption-options.ts @@ -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) { + 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; + } +} diff --git a/libs/auth/src/common/models/spec/index.ts b/libs/auth/src/common/models/spec/index.ts new file mode 100644 index 00000000000..d05bc2bf2c8 --- /dev/null +++ b/libs/auth/src/common/models/spec/index.ts @@ -0,0 +1 @@ +export * from "./fake-user-decryption-options"; diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 2b131c7afc2..12215cf6b4d 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -1,3 +1,4 @@ export * from "./pin-crypto/pin-crypto.service.implementation"; export * from "./login-strategies/login-strategy.service"; +export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 3d4c1b7b7d5..981e4d81ac7 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums"; import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { AuthRequestServiceAbstraction } from "../../abstractions"; +import { + AuthRequestServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, +} from "../../abstractions"; import { PasswordLoginCredentials } from "../../models"; +import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service"; import { LoginStrategyService } from "./login-strategy.service"; import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; @@ -51,6 +55,7 @@ describe("LoginStrategyService", () => { let policyService: MockProxy; let deviceTrustCryptoService: MockProxy; let authRequestService: MockProxy; + let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; let stateProvider: FakeGlobalStateProvider; @@ -74,6 +79,7 @@ describe("LoginStrategyService", () => { policyService = mock(); deviceTrustCryptoService = mock(); authRequestService = mock(); + userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); @@ -95,6 +101,7 @@ describe("LoginStrategyService", () => { policyService, deviceTrustCryptoService, authRequestService, + userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 5c0e4140446..5dbc3397cf4 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -40,6 +40,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { MasterKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; @@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected policyService: PolicyService, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, ) { @@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.passwordStrengthService, this.policyService, this, @@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.keyConnectorService, this.deviceTrustCryptoService, this.authRequestService, @@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, @@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.deviceTrustCryptoService, this.billingAccountProfileStateService, ); @@ -420,6 +426,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.billingAccountProfileStateService, ); } diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts new file mode 100644 index 00000000000..e8bb1b38cee --- /dev/null +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -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); + }); + }); +}); diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts new file mode 100644 index 00000000000..6651ffd9e51 --- /dev/null +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -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( + USER_DECRYPTION_OPTIONS_DISK, + "decryptionOptions", + { + deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions), + clearOn: ["logout"], + }, +); + +export class UserDecryptionOptionsService + implements InternalUserDecryptionOptionsServiceAbstraction +{ + private userDecryptionOptionsState: ActiveUserState; + + 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 { + await this.userDecryptionOptionsState.update((_) => userDecryptionOptions); + } +} diff --git a/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts new file mode 100644 index 00000000000..2328165e4b2 --- /dev/null +++ b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts @@ -0,0 +1,22 @@ +import { Observable } from "rxjs"; + +/** + * Manages the state of a single organization management preference. + * Can be used to subscribe to or update a given property. + */ +export class OrganizationManagementPreference { + state$: Observable; + set: (value: T) => Promise; + + constructor(state$: Observable, setFn: (value: T) => Promise) { + this.state$ = state$; + this.set = setFn; + } +} + +/** + * Publishes state of a given user's personal settings relating to the user experience of managing an organization. + */ +export abstract class OrganizationManagementPreferencesService { + autoConfirmFingerPrints: OrganizationManagementPreference; +} diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts new file mode 100644 index 00000000000..0d16e770eae --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts @@ -0,0 +1,44 @@ +import { MockProxy } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { UserId } from "../../../types/guid"; + +import { DefaultOrganizationManagementPreferencesService } from "./default-organization-management-preferences.service"; + +describe("OrganizationManagementPreferencesService", () => { + let stateProvider: FakeStateProvider; + let organizationManagementPreferencesService: MockProxy; + + beforeEach(() => { + const accountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(accountService); + organizationManagementPreferencesService = new DefaultOrganizationManagementPreferencesService( + stateProvider, + ); + }); + + describe("autoConfirmFingerPrints", () => { + it("returns false by default", async () => { + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + it("returns true if set", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(true); + }); + it("can be unset", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(false); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + }); +}); diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts new file mode 100644 index 00000000000..e257b691638 --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts @@ -0,0 +1,71 @@ +import { map } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { + ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { + OrganizationManagementPreference, + OrganizationManagementPreferencesService, +} from "../../abstractions/organization-management-preferences/organization-management-preferences.service"; + +/** + * This helper function can be used to quickly create `KeyDefinitions` that + * target the `ORGANIZATION_MANAGEMENT_PREFERENCES_DISK` `StateDefinition` + * and that have the default deserializer and `clearOn` options. Any + * contenders for options to add to this service will likely use these same + * options. + */ +function buildKeyDefinition(key: string): UserKeyDefinition { + return new UserKeyDefinition(ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, key, { + deserializer: (obj: Jsonify) => obj as T, + clearOn: ["logout"], + }); +} + +export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition("autoConfirmFingerPrints"); + +export class DefaultOrganizationManagementPreferencesService + implements OrganizationManagementPreferencesService +{ + constructor(private stateProvider: StateProvider) {} + + autoConfirmFingerPrints = this.buildOrganizationManagementPreference( + AUTO_CONFIRM_FINGERPRINTS, + false, + ); + + /** + * Returns an `OrganizationManagementPreference` object for the provided + * `KeyDefinition`. This object can then be used by callers to subscribe to + * a given key, or set its value in state. + */ + private buildOrganizationManagementPreference( + keyDefinition: UserKeyDefinition, + defaultValue: T, + ) { + return new OrganizationManagementPreference( + this.getKeyFromState(keyDefinition).state$.pipe(map((x) => x ?? defaultValue)), + this.setKeyInStateFn(keyDefinition), + ); + } + + /** + * Returns the full `ActiveUserState` value for a given `keyDefinition` + * The returned value can then be called for subscription || set operations + */ + private getKeyFromState(keyDefinition: UserKeyDefinition) { + return this.stateProvider.getActive(keyDefinition); + } + + /** + * Returns a function that can be called to set the given `keyDefinition` in state + */ + private setKeyInStateFn(keyDefinition: UserKeyDefinition) { + return async (value: T) => { + await this.getKeyFromState(keyDefinition).update(() => value); + }; + } +} diff --git a/libs/common/src/auth/abstractions/avatar.service.ts b/libs/common/src/auth/abstractions/avatar.service.ts index 1192ef745df..7da92ac7fdb 100644 --- a/libs/common/src/auth/abstractions/avatar.service.ts +++ b/libs/common/src/auth/abstractions/avatar.service.ts @@ -15,6 +15,17 @@ export abstract class AvatarService { * @returns a promise that resolves when the avatar color is set */ abstract setAvatarColor(color: string): Promise; + /** + * Sets the avatar color for the given user, meant to be used via sync. + * + * @remarks This is meant to be used for getting an updated avatar color from + * the sync endpoint. If the user is changing their avatar color + * on device, you should instead call {@link setAvatarColor}. + * + * @param userId The user id for the user to set the avatar color for + * @param color The color to set the avatar color to + */ + abstract setSyncAvatarColor(userId: UserId, color: string): Promise; /** * Gets the avatar color of the specified user. * diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts index 5f37e7d956f..415355cfc77 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts @@ -1,8 +1,11 @@ +import { Observable } from "rxjs"; + import { EncString } from "../../platform/models/domain/enc-string"; import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; export abstract class DeviceTrustCryptoServiceAbstraction { + supportsDeviceTrust$: Observable; /** * @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 @@ -20,6 +23,4 @@ export abstract class DeviceTrustCryptoServiceAbstraction { deviceKey?: DeviceKey, ) => Promise; rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise; - - supportsDeviceTrust: () => Promise; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 16f58c64593..993ce08d589 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -8,7 +8,7 @@ export class AuthResult { // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal /** * @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. * */ resetMasterPassword = false; diff --git a/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts b/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts deleted file mode 100644 index 3422c078e46..00000000000 --- a/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class KeyConnectorUserDecryptionOption { - constructor(public keyConnectorUrl: string) {} -} diff --git a/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts b/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts deleted file mode 100644 index 6d2dc9d6334..00000000000 --- a/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class TrustedDeviceUserDecryptionOption { - constructor( - public hasAdminApproval: boolean, - public hasLoginApprovingDevice: boolean, - public hasManageResetPasswordPermission: boolean, - ) {} -} diff --git a/libs/common/src/auth/services/avatar.service.ts b/libs/common/src/auth/services/avatar.service.ts index b770dc39b98..9b8c83968dd 100644 --- a/libs/common/src/auth/services/avatar.service.ts +++ b/libs/common/src/auth/services/avatar.service.ts @@ -27,6 +27,10 @@ export class AvatarService implements AvatarServiceAbstraction { await this.stateProvider.setUserState(AVATAR_COLOR, avatarColor); } + async setSyncAvatarColor(userId: UserId, color: string): Promise { + await this.stateProvider.getUser(userId, AVATAR_COLOR).update(() => color); + } + getUserAvatarColor$(userId: UserId): Observable { return this.stateProvider.getUser(userId, AVATAR_COLOR).state$; } diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts index 9aa558ba537..71f83f07c3b 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts @@ -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 { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; @@ -21,6 +23,8 @@ import { } from "../models/request/update-devices-trust.request"; export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { + supportsDeviceTrust$: Observable; + constructor( private keyGenerationService: KeyGenerationService, private cryptoFunctionService: CryptoFunctionService, @@ -31,7 +35,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac private devicesApiService: DevicesApiServiceAbstraction, private i18nService: I18nService, 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 @@ -203,9 +212,4 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac return null; } } - - async supportsDeviceTrust(): Promise { - const decryptionOptions = await this.stateService.getAccountDecryptionOptions(); - return decryptionOptions?.trustedDeviceOption != null; - } } diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts index 8c0a62b125e..1d33223dddb 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts @@ -1,6 +1,9 @@ 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 { AppIdService } from "../../platform/abstractions/app-id.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; @@ -34,10 +37,16 @@ describe("deviceTrustCryptoService", () => { const devicesApiService = mock(); const i18nService = mock(); const platformUtilsService = mock(); + const userDecryptionOptionsService = mock(); + + const decryptionOptions = new BehaviorSubject(null); beforeEach(() => { jest.clearAllMocks(); + decryptionOptions.next({} as any); + userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; + deviceTrustCryptoService = new DeviceTrustCryptoService( keyGenerationService, cryptoFunctionService, @@ -48,6 +57,7 @@ describe("deviceTrustCryptoService", () => { devicesApiService, i18nService, platformUtilsService, + userDecryptionOptionsService, ); }); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 431348c7fc9..03e267d9db5 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -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 { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -33,6 +37,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private cryptoService: CryptoService, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private pinCryptoService: PinCryptoServiceAbstraction, private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, @@ -135,7 +140,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti case VerificationType.MasterPassword: return this.verifyUserByMasterPassword(verification); case VerificationType.PIN: - return this.verifyUserByPIN(verification); break; case VerificationType.Biometrics: return this.verifyUserByBiometrics(); @@ -210,16 +214,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti * 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 * @returns True if the user has a master password + * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead */ async hasMasterPassword(userId?: string): Promise { - const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId }); + if (userId) { + const decryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); - if (decryptionOptions?.hasMasterPassword != undefined) { - return decryptionOptions.hasMasterPassword; + if (decryptionOptions?.hasMasterPassword != undefined) { + return decryptionOptions.hasMasterPassword; + } } - - // TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0 - return !(await this.stateService.getUsesKeyConnector({ userId })); + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 3413afe1825..e0228ee062c 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -18,7 +18,7 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType } from "../enums"; 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 { StorageOptions } from "../models/domain/storage-options"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -54,9 +54,6 @@ export abstract class StateService { setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; getAlwaysShowDock: (options?: StorageOptions) => Promise; setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; - - getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; - setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; @@ -183,13 +180,6 @@ export abstract class StateService { ) => Promise; getShouldTrustDevice: (options?: StorageOptions) => Promise; setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise; - getAccountDecryptionOptions: ( - options?: StorageOptions, - ) => Promise; - setAccountDecryptionOptions: ( - value: AccountDecryptionOptions, - options?: StorageOptions, - ) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; getEmailVerified: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 07efb505a5e..c8d903cfea3 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -2,9 +2,6 @@ import { Jsonify } from "type-fest"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; -import { 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 { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -187,7 +184,6 @@ export class AccountProfile { } export class AccountSettings { - autoConfirmFingerPrints?: boolean; defaultUriMatch?: UriMatchStrategySetting; disableGa?: boolean; enableAlwaysOnTop?: boolean; @@ -236,103 +232,12 @@ export class AccountTokens { } } -export class AccountDecryptionOptions { - hasMasterPassword: boolean; - trustedDeviceOption?: TrustedDeviceUserDecryptionOption; - keyConnectorOption?: KeyConnectorUserDecryptionOption; - - constructor(init?: Partial) { - 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 { - 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 { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); tokens?: AccountTokens = new AccountTokens(); - decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions(); adminAuthRequest?: Jsonify = null; constructor(init: Partial) { @@ -357,10 +262,6 @@ export class Account { ...new AccountTokens(), ...init?.tokens, }, - decryptionOptions: { - ...new AccountDecryptionOptions(), - ...init?.decryptionOptions, - }, adminAuthRequest: init?.adminAuthRequest, }); } @@ -376,7 +277,6 @@ export class Account { profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), tokens: AccountTokens.fromJSON(json?.tokens), - decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions), adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest), }); } diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 86f7c3798f0..fbb6a852937 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -1,5 +1,5 @@ import * as bigInt from "big-integer"; -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, filter, firstValueFrom, map } from "rxjs"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -100,7 +100,9 @@ export class CryptoService implements CryptoServiceAbstraction { // User Asymmetric Key Pair this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY); this.activeUserPrivateKeyState = stateProvider.getDerived( - this.activeUserEncryptedPrivateKeyState.combinedState$, + this.activeUserEncryptedPrivateKeyState.combinedState$.pipe( + filter(([_userId, key]) => key != null), + ), USER_PRIVATE_KEY, { encryptService: this.encryptService, @@ -109,7 +111,7 @@ export class CryptoService implements CryptoServiceAbstraction { ); this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null this.activeUserPublicKeyState = stateProvider.getDerived( - this.activeUserPrivateKey$, + this.activeUserPrivateKey$.pipe(filter((key) => key != null)), USER_PUBLIC_KEY, { cryptoFunctionService: this.cryptoFunctionService, @@ -122,7 +124,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_ENCRYPTED_ORGANIZATION_KEYS, ); this.activeUserOrgKeysState = stateProvider.getDerived( - this.activeUserEncryptedOrgKeysState.state$, + this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)), USER_ORGANIZATION_KEYS, { cryptoService: this }, ); @@ -133,7 +135,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_ENCRYPTED_PROVIDER_KEYS, ); this.activeUserProviderKeysState = stateProvider.getDerived( - this.activeUserEncryptedProviderKeysState.state$, + this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), USER_PROVIDER_KEYS, { encryptService: this.encryptService, cryptoService: this }, ); diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 31d69e868bf..fc548b562ea 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -34,12 +34,7 @@ import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { ServerConfigData } from "../models/data/server-config.data"; -import { - Account, - AccountData, - AccountDecryptionOptions, - AccountSettings, -} from "../models/domain/account"; +import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; import { State } from "../models/domain/state"; @@ -300,24 +295,6 @@ export class StateService< ); } - async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.autoConfirmFingerPrints ?? false - ); - } - - async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.autoConfirmFingerPrints = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -835,37 +812,6 @@ export class StateService< await this.saveAccount(account, options); } - async getAccountDecryptionOptions( - options?: StorageOptions, - ): Promise { - 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 { - 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 { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 858be39855a..15dc9ff7574 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -2,9 +2,9 @@ * Default storage location options. * * `disk` generally means state that is accessible between restarts of the application, - * with the exception of the web client. In web this means `sessionStorage`. The data is - * through refreshes of the page but not available once that tab is closed or from any - * other tabs. + * with the exception of the web client. In web this means `sessionStorage`. The data + * persists through refreshes of the page but not available once that tab is closed or + * from any other tabs. * * `memory` means that the information stored there goes away during application * restarts. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 8115555b2ed..edc5fd9a16f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -22,6 +22,13 @@ import { StateDefinition } from "./state-definition"; export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk"); export const POLICIES_DISK = new StateDefinition("policies", "disk"); export const PROVIDERS_DISK = new StateDefinition("providers", "disk"); +export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition( + "organizationManagementPreferences", + "disk", + { + web: "disk-local", + }, +); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); @@ -37,6 +44,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { }); export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); +export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); // Autofill @@ -67,8 +75,9 @@ export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory"); +export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk"); export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk"); -export const THEMING_DISK = new StateDefinition("theming", "disk"); +export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" }); export const TRANSLATION_DISK = new StateDefinition("translation", "disk"); // Secrets Manager diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 7eee4567754..4ce0159121c 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -1,5 +1,10 @@ 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 { 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 { StateService } from "../../platform/abstractions/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 { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; describe("VaultTimeoutSettingsService", () => { + let userDecryptionOptionsService: MockProxy; let cryptoService: MockProxy; let tokenService: MockProxy; let policyService: MockProxy; @@ -21,12 +26,26 @@ describe("VaultTimeoutSettingsService", () => { const biometricStateService = mock(); let service: VaultTimeoutSettingsService; + let userDecryptionOptionsSubject: BehaviorSubject; + beforeEach(() => { + userDecryptionOptionsService = mock(); cryptoService = mock(); tokenService = mock(); policyService = mock(); stateService = mock(); + + userDecryptionOptionsSubject = new BehaviorSubject(null); + userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe( + map((options) => options?.hasMasterPassword ?? false), + ); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); + service = new VaultTimeoutSettingsService( + userDecryptionOptionsService, cryptoService, tokenService, policyService, @@ -49,9 +68,7 @@ describe("VaultTimeoutSettingsService", () => { }); it("contains Lock when the user has a master password", async () => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: true }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); 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 () => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: false }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false })); stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null); stateService.getProtectedPin.mockResolvedValue(null); biometricStateService.biometricUnlockEnabled$ = of(false); @@ -107,9 +122,7 @@ describe("VaultTimeoutSettingsService", () => { `( "returns $expected when policy is $policy, and user preference is $userPreference", async ({ policy, userPreference, expected }) => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: true }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); policyService.getAll$.mockReturnValue( 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", async ({ unlockMethod, policy, userPreference, expected }) => { biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: false }), + userDecryptionOptionsSubject.next( + new UserDecryptionOptions({ hasMasterPassword: false }), ); policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index e8897d82b7d..4eb9e776992 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -1,5 +1,7 @@ import { defer, firstValueFrom } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; @@ -19,6 +21,7 @@ export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT"; export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { constructor( + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private cryptoService: CryptoService, private tokenService: TokenService, private policyService: PolicyService, @@ -174,12 +177,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } private async userHasMasterPassword(userId: string): Promise { - const acctDecryptionOpts = await this.stateService.getAccountDecryptionOptions({ - userId: userId, - }); + if (userId) { + const decryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); - if (acctDecryptionOpts?.hasMasterPassword != undefined) { - return acctDecryptionOpts.hasMasterPassword; + if (decryptionOptions?.hasMasterPassword != undefined) { + return decryptionOptions.hasMasterPassword; + } } + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 000f08b392b..6c2a3084667 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -38,6 +38,8 @@ import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been- import { OrganizationMigrator } from "./migrations/40-move-organization-state-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 { 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 { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -46,7 +48,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 42; +export const CURRENT_VERSION = 44; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -90,7 +92,9 @@ export function createMigrationBuilder() { .with(MoveBillingAccountProfileMigrator, 38, 39) .with(OrganizationMigrator, 39, 40) .with(EventCollectionMigrator, 40, 41) - .with(EnableFaviconMigrator, 41, 42); + .with(EnableFaviconMigrator, 41, 42) + .with(AutoConfirmFingerPrintsMigrator, 42, 43) + .with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts index 64a7fd8efa1..499ceae5fa1 100644 --- a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts @@ -87,12 +87,12 @@ describe("VaultSettingsKeyMigrator", () => { expect(helper.setToUser).toHaveBeenCalledWith( "user-1", { ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" }, - true, + false, ); expect(helper.setToUser).toHaveBeenCalledWith( "user-1", { ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" }, - true, + false, ); }); }); diff --git a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts index 572e074cf1c..8e86507a3b8 100644 --- a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts @@ -30,7 +30,7 @@ export class VaultSettingsKeyMigrator extends Migrator<35, 36> { await helper.setToUser( userId, { ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" }, - accountSettings.dontShowCardsCurrentTab, + !accountSettings.dontShowCardsCurrentTab, ); delete account.settings.dontShowCardsCurrentTab; updateAccount = true; @@ -40,7 +40,7 @@ export class VaultSettingsKeyMigrator extends Migrator<35, 36> { await helper.setToUser( userId, { ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" }, - accountSettings.dontShowIdentitiesCurrentTab, + !accountSettings.dontShowIdentitiesCurrentTab, ); delete account.settings.dontShowIdentitiesCurrentTab; updateAccount = true; diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts new file mode 100644 index 00000000000..359f582b8c0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts @@ -0,0 +1,102 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper, runMigrator } from "../migration-helper.spec"; + +import { AutoConfirmFingerPrintsMigrator } from "./43-move-auto-confirm-finger-prints-to-state-provider"; + +function rollbackJSON() { + return { + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }; +} + +describe("AutoConfirmFingerPrintsMigrator", () => { + const migrator = new AutoConfirmFingerPrintsMigrator(42, 43); + + it("should migrate the autoConfirmFingerPrints property from the account settings object to a user StorageKey", async () => { + const output = await runMigrator(migrator, { + authenticatedAccounts: ["user-1", "user-2"] as const, + "user-1": { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + autoConfirmFingerPrints: false, + extra: "data", + }, + extra: "data", + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }); + }); + + describe("rollback", () => { + let helper: MockProxy; + let sut: AutoConfirmFingerPrintsMigrator; + + const keyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: { + name: "organizationManagementPreferences", + }, + }; + + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 43); + sut = new AutoConfirmFingerPrintsMigrator(42, 43); + }); + + it("should null the autoConfirmFingerPrints user StorageKey for each account", async () => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, null); + }); + + it("should add the autoConfirmFingerPrints property back to the account settings object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts new file mode 100644 index 00000000000..246e3cf4365 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts @@ -0,0 +1,63 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountState = { + settings?: { autoConfirmFingerPrints?: boolean }; +}; + +const ORGANIZATION_MANAGEMENT_PREFERENCES: StateDefinitionLike = { + name: "organizationManagementPreferences", +}; + +const AUTO_CONFIRM_FINGERPRINTS: KeyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: ORGANIZATION_MANAGEMENT_PREFERENCES, +}; + +export class AutoConfirmFingerPrintsMigrator extends Migrator<42, 43> { + async migrate(helper: MigrationHelper): Promise { + const legacyAccounts = await helper.getAccounts(); + + await Promise.all( + legacyAccounts.map(async ({ userId, account }) => { + if (account?.settings?.autoConfirmFingerPrints != null) { + await helper.setToUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + account.settings.autoConfirmFingerPrints, + ); + delete account?.settings?.autoConfirmFingerPrints; + await helper.set(userId, account); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string, account: ExpectedAccountState) { + let updatedAccount = false; + const autoConfirmFingerPrints = await helper.getFromUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + ); + + if (autoConfirmFingerPrints) { + if (!account) { + account = {}; + } + + updatedAccount = true; + account.settings.autoConfirmFingerPrints = autoConfirmFingerPrints; + await helper.setToUser(userId, AUTO_CONFIRM_FINGERPRINTS, null); + } + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + + await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account))); + } +} diff --git a/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts new file mode 100644 index 00000000000..90254f1c435 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts @@ -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; + 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()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts new file mode 100644 index 00000000000..708b096280c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts @@ -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 { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + 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 { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + 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))]); + } +} diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index 848633e501d..cd1cab8b422 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -68,6 +68,7 @@ describe("Collection", () => { organizationId: "orgId", readOnly: false, manage: true, + assigned: true, }); }); }); diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 1177d23220b..74d369380bf 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject { readOnly: boolean = null; hidePasswords: boolean = null; manage: boolean = null; + assigned: boolean = null; constructor(c?: Collection | CollectionAccessDetailsResponse) { if (!c) { @@ -30,7 +31,29 @@ export class CollectionView implements View, ITreeNodeObject { this.readOnly = c.readOnly; this.hidePasswords = c.hidePasswords; this.manage = c.manage; + this.assigned = true; } + if (c instanceof CollectionAccessDetailsResponse) { + this.assigned = c.assigned; + } + } + + canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean { + if (org != null && org.id !== this.organizationId) { + throw new Error( + "Id of the organization provided does not match the org id of the collection.", + ); + } + + if (org?.flexibleCollections) { + return ( + org?.canEditAllCiphers(v1FlexibleCollections) || + this.manage || + (this.assigned && !this.readOnly) + ); + } + + return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned); } // For editing collection details, not the items within it. diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index b3deed7c0ca..8a86d9aa050 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -79,7 +79,12 @@ export class CipherService implements CipherServiceAbstraction { } async setDecryptedCipherCache(value: CipherView[]) { - await this.stateService.setDecryptedCiphers(value); + // Sometimes we might prematurely decrypt the vault and that will result in no ciphers + // if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again. + // We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption. + if (value == null || value.length !== 0) { + await this.stateService.setDecryptedCiphers(value); + } if (this.searchService != null) { if (value == null) { this.searchService.clearIndex(); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 1b3e63d0012..3e8bd92a7ac 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -1,3 +1,7 @@ +import { firstValueFrom } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + import { ApiService } from "../../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -24,11 +28,11 @@ import { LogService } from "../../../platform/abstractions/log.service"; import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { sequentialize } from "../../../platform/misc/sequentialize"; -import { AccountDecryptionOptions } from "../../../platform/models/domain/account"; import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "../../../tools/send/services/send.service.abstraction"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; @@ -61,6 +65,7 @@ export class SyncService implements SyncServiceAbstraction { private folderApiService: FolderApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private sendApiService: SendApiService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, @@ -313,7 +318,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setPrivateKey(response.privateKey); await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.avatarService.setAvatarColor(response.avatarColor); + await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); @@ -352,19 +357,12 @@ export class SyncService implements SyncServiceAbstraction { ); } - const acctDecryptionOpts: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); + const userDecryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); - // Account decryption options should never be null or undefined b/c it is always initialized - // 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) { + if (userDecryptionOptions === null || userDecryptionOptions === 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 @@ -383,8 +381,8 @@ export class SyncService implements SyncServiceAbstraction { } if ( - acctDecryptionOpts.trustedDeviceOption !== undefined && - !acctDecryptionOpts.hasMasterPassword && + userDecryptionOptions.trustedDeviceOption !== undefined && + !userDecryptionOptions.hasMasterPassword && hasManageResetPasswordPermission ) { // TDE user w/out MP went from having no password reset permission to having it. diff --git a/libs/shared/tsconfig.libs.json b/libs/shared/tsconfig.libs.json index 079a49fbd5d..713d34a10e4 100644 --- a/libs/shared/tsconfig.libs.json +++ b/libs/shared/tsconfig.libs.json @@ -10,6 +10,7 @@ "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["../tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../importer/src"], "@bitwarden/importer/ui": ["../importer/src/components"], "@bitwarden/platform": ["../platform/src"], diff --git a/libs/tools/export/vault-export/README.md b/libs/tools/export/vault-export/README.md index 45d9e08e5e9..e6bce1c525f 100644 --- a/libs/tools/export/vault-export/README.md +++ b/libs/tools/export/vault-export/README.md @@ -13,3 +13,5 @@ Currently in use by the Bitwarden Web Vault, CLI, desktop app and browser extens ## vault-export-ui Package name: `@bitwarden/vault-export-ui` + +Contains all UI components used for the vault-export diff --git a/libs/tools/export/vault-export/vault-export-ui/jest.config.js b/libs/tools/export/vault-export/vault-export-ui/jest.config.js new file mode 100644 index 00000000000..955b8e7763c --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/jest.config.js @@ -0,0 +1,13 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("../../../../shared/tsconfig.libs"); + +/** @type {import('jest').Config} */ +module.exports = { + testMatch: ["**/+(*.)+(spec).+(ts)"], + preset: "ts-jest", + testEnvironment: "jsdom", + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/../../../", + }), +}; diff --git a/libs/tools/export/vault-export/vault-export-ui/package.json b/libs/tools/export/vault-export/vault-export-ui/package.json new file mode 100644 index 00000000000..e27140f3657 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@bitwarden/vault-export-ui", + "version": "0.0.0", + "description": "Angular components for the Bitwarden vault exporter", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch" + }, + "dependencies": { + "@bitwarden/common": "file:../../../../common", + "@bitwarden/angular": "file:../../../../angular", + "@bitwarden/vault-export-core": "file:../vault-export-core" + } +} diff --git a/libs/angular/src/tools/export/components/export-scope-callout.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html similarity index 59% rename from libs/angular/src/tools/export/components/export-scope-callout.component.html rename to libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html index c6b5e1e8523..a660219499f 100644 --- a/libs/angular/src/tools/export/components/export-scope-callout.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html @@ -1,5 +1,5 @@ - + {{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }} - + diff --git a/libs/angular/src/tools/export/components/export-scope-callout.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts similarity index 86% rename from libs/angular/src/tools/export/components/export-scope-callout.component.ts rename to libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts index 545dfe4560a..0f246c3a341 100644 --- a/libs/angular/src/tools/export/components/export-scope-callout.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts @@ -1,11 +1,16 @@ +import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { CalloutModule } from "@bitwarden/components"; @Component({ - selector: "app-export-scope-callout", + selector: "tools-export-scope-callout", templateUrl: "export-scope-callout.component.html", + standalone: true, + imports: [CommonModule, JslibModule, CalloutModule], }) export class ExportScopeCalloutComponent implements OnInit { show = false; diff --git a/libs/tools/export/vault-export/vault-export-ui/src/index.ts b/libs/tools/export/vault-export/vault-export-ui/src/index.ts new file mode 100644 index 00000000000..4165ee4558a --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/src/index.ts @@ -0,0 +1 @@ +export { ExportScopeCalloutComponent } from "./components/export-scope-callout.component"; diff --git a/libs/tools/export/vault-export/vault-export-ui/tsconfig.json b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json new file mode 100644 index 00000000000..5cb90260371 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../shared/tsconfig.libs", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json b/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json new file mode 100644 index 00000000000..fc8520e7376 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/package-lock.json b/package-lock.json index 53f31340b33..ba95074d898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,7 +140,7 @@ "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.14.3", + "eslint-plugin-tailwindcss": "3.15.1", "gulp": "4.0.2", "gulp-filter": "9.0.1", "gulp-if": "3.0.0", @@ -18826,9 +18826,9 @@ } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.14.3.tgz", - "integrity": "sha512-1MKT8CrVuqVJleHxb7ICHsF2QwO0G+VJ28athTtlcOkccp0qmwK7nCUa1C9paCZ+VVgQU4fonsjLz/wUxoMHJQ==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.15.1.tgz", + "integrity": "sha512-4RXRMIaMG07C2TBEW1k0VM4+dDazz1kxcZhkK4zirvmHGZTA4jnlSO2kq5mamuSPi+Wo17dh2SlC8IyFBuCd7Q==", "dev": true, "dependencies": { "fast-glob": "^3.2.5", diff --git a/package.json b/package.json index ec0f1d64747..d201eebcda5 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.14.3", + "eslint-plugin-tailwindcss": "3.15.1", "gulp": "4.0.2", "gulp-filter": "9.0.1", "gulp-if": "3.0.0", diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index c8144b97b9f..19d35b28969 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -22,6 +22,7 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/platform": ["./libs/platform/src"], diff --git a/tsconfig.json b/tsconfig.json index 4aaf670dbc2..ab3f8861a9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/platform": ["./libs/platform/src"],