import { BehaviorSubject } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService, AbstractMemoryStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; import { BrowserComponentState } from "../../models/browserComponentState"; import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState"; import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service"; @browserSession export class BrowserStateService extends BaseStateService implements StateServiceAbstraction { @sessionSync({ initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account initializeAs: "record", }) protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>; @sessionSync({ initializer: (s: string) => s }) protected activeAccountSubject: BehaviorSubject; @sessionSync({ initializer: (b: boolean) => b }) protected activeAccountUnlockedSubject: BehaviorSubject; protected accountDeserializer = Account.fromJSON; constructor( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService, logService: LogService, stateFactory: StateFactory, useAccountCache = true ) { super( storageService, secureStorageService, memoryStorageService, logService, stateFactory, useAccountCache ); // TODO: This is a hack to fix having a disk cache on both the popup and // the background page that can get out of sync. We need to work out the // best way to handle caching with multiple instances of the state service. if (useAccountCache) { BrowserApi.storageChangeListener((changes, namespace) => { if (namespace === "local") { for (const key of Object.keys(changes)) { if (key !== "accountActivity" && this.accountDiskCache.value[key]) { this.deleteDiskCache(key); } } } }); } } async addAccount(account: Account) { // Apply browser overrides to default account values account = new Account(account); await super.addAccount(account); } async getIsAuthenticated(options?: StorageOptions): Promise { // Firefox Private Mode can clash with non-Private Mode because they both read from the same onDiskOptions // Check that there is an account in memory before considering the user authenticated return ( (await super.getIsAuthenticated(options)) && (await this.getAccount(await this.defaultInMemoryOptions())) != null ); } async getBrowserGroupingComponentState( options?: StorageOptions ): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.groupings; } async setBrowserGroupingComponentState( value: BrowserGroupingsComponentState, options?: StorageOptions ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.groupings = value; await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } async getBrowserVaultItemsComponentState( options?: StorageOptions ): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.ciphers; } async setBrowserVaultItemsComponentState( value: BrowserComponentState, options?: StorageOptions ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.ciphers = value; await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } async getBrowserSendComponentState(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.send; } async setBrowserSendComponentState( value: BrowserSendComponentState, options?: StorageOptions ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.send = value; await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } async getBrowserSendTypeComponentState(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.sendType; } async setBrowserSendTypeComponentState( value: BrowserComponentState, options?: StorageOptions ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.sendType = value; await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } // Overriding the base class to prevent deleting the cache on save. We register a storage listener // to delete the cache in the constructor above. protected override async saveAccountToDisk( account: Account, options: StorageOptions ): Promise { const storageLocation = options.useSecureStorage ? this.secureStorageService : this.storageService; await storageLocation.save(`${options.userId}`, account, options); } }