mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-24677] Slim StateService down so it can be moved to state lib (#16021)
* Slim StateService down so it can be moved to state lib * Fix accidental import changes * Add `switchAccount` assertion * Needs to use mock
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
import { HttpOperations } from "@bitwarden/common/services/api.service";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
@@ -33,7 +32,6 @@ export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken<
|
||||
>("OBSERVABLE_DISK_LOCAL_STORAGE");
|
||||
export const MEMORY_STORAGE = new SafeInjectionToken<AbstractStorageService>("MEMORY_STORAGE");
|
||||
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
|
||||
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
|
||||
export const LOGOUT_CALLBACK = new SafeInjectionToken<
|
||||
(logoutReason: LogoutReason, userId?: string) => Promise<void>
|
||||
>("LOGOUT_CALLBACK");
|
||||
|
||||
@@ -196,13 +196,10 @@ import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.serv
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import {
|
||||
@@ -228,13 +225,13 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
import {
|
||||
ActiveUserAccessor,
|
||||
ActiveUserStateProvider,
|
||||
DefaultStateService,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
@@ -371,12 +368,10 @@ import {
|
||||
LOCKED_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
LOGOUT_CALLBACK,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
|
||||
SECURE_STORAGE,
|
||||
STATE_FACTORY,
|
||||
SUPPORTS_SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
@@ -414,10 +409,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useFactory: (window: Window) => window.navigator.language,
|
||||
deps: [WINDOW],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
}),
|
||||
// TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService
|
||||
safeProvider({
|
||||
provide: LOGOUT_CALLBACK,
|
||||
@@ -530,7 +521,6 @@ const safeProviders: SafeProvider[] = [
|
||||
apiService: ApiServiceAbstraction,
|
||||
i18nService: I18nServiceAbstraction,
|
||||
searchService: SearchServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
encryptService: EncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
@@ -547,7 +537,6 @@ const safeProviders: SafeProvider[] = [
|
||||
apiService,
|
||||
i18nService,
|
||||
searchService,
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
fileUploadService,
|
||||
@@ -564,7 +553,6 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
EncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
@@ -801,7 +789,6 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalSendService,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
ProviderServiceAbstraction,
|
||||
FolderApiServiceAbstraction,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
@@ -849,6 +836,7 @@ const safeProviders: SafeProvider[] = [
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsService,
|
||||
StateEventRunnerService,
|
||||
@@ -868,24 +856,10 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateServiceAbstraction,
|
||||
useClass: StateService,
|
||||
deps: [
|
||||
AbstractStorageService,
|
||||
SECURE_STORAGE,
|
||||
MEMORY_STORAGE,
|
||||
LogService,
|
||||
STATE_FACTORY,
|
||||
AccountServiceAbstraction,
|
||||
EnvironmentService,
|
||||
TokenServiceAbstraction,
|
||||
MigrationRunner,
|
||||
],
|
||||
useClass: DefaultStateService,
|
||||
deps: [AbstractStorageService, SECURE_STORAGE, ActiveUserAccessor],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IndividualVaultExportServiceAbstraction,
|
||||
|
||||
@@ -32,7 +32,6 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import {
|
||||
@@ -243,18 +242,8 @@ describe("LoginStrategy", () => {
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: userId,
|
||||
name: name,
|
||||
email: email,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
||||
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
UserDecryptionOptions.fromResponse(idTokenResponse),
|
||||
);
|
||||
@@ -388,7 +377,8 @@ describe("LoginStrategy", () => {
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(stateService.addAccount).not.toHaveBeenCalled();
|
||||
expect(environmentService.seedUserEnvironment).not.toHaveBeenCalled();
|
||||
expect(accountService.mock.switchAccount).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).not.toHaveBeenCalled();
|
||||
expect(tokenService.clearTwoFactorToken).toHaveBeenCalled();
|
||||
|
||||
@@ -422,7 +412,7 @@ describe("LoginStrategy", () => {
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(stateService.addAccount).not.toHaveBeenCalled();
|
||||
expect(environmentService.seedUserEnvironment).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).not.toHaveBeenCalled();
|
||||
|
||||
const expected = new AuthResult();
|
||||
|
||||
@@ -32,7 +32,6 @@ 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 { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
KeyService,
|
||||
@@ -198,19 +197,6 @@ export abstract class LoginStrategy {
|
||||
|
||||
await this.accountService.switchAccount(userId);
|
||||
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId,
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await this.verifyAccountAdded(userId);
|
||||
|
||||
// We must set user decryption options before retrieving vault timeout settings
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
mockVaultTimeoutAction,
|
||||
mockVaultTimeout,
|
||||
);
|
||||
expect(stateService.addAccount).toHaveBeenCalled();
|
||||
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets the encrypted user key and private key from the identity token response", async () => {
|
||||
|
||||
@@ -12,15 +12,16 @@ import { LogoutReason } from "@bitwarden/auth/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
import { StateService } from "@bitwarden/state";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { TaskSchedulerService } from "../../../platform/scheduling";
|
||||
import { StateEventRunnerService } from "../../../platform/state";
|
||||
@@ -45,6 +46,7 @@ describe("VaultTimeoutService", () => {
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let searchService: MockProxy<SearchService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
|
||||
@@ -71,6 +73,7 @@ describe("VaultTimeoutService", () => {
|
||||
messagingService = mock();
|
||||
searchService = mock();
|
||||
stateService = mock();
|
||||
tokenService = mock();
|
||||
authService = mock();
|
||||
vaultTimeoutSettingsService = mock();
|
||||
stateEventRunnerService = mock();
|
||||
@@ -99,6 +102,7 @@ describe("VaultTimeoutService", () => {
|
||||
messagingService,
|
||||
searchService,
|
||||
stateService,
|
||||
tokenService,
|
||||
authService,
|
||||
vaultTimeoutSettingsService,
|
||||
stateEventRunnerService,
|
||||
@@ -141,9 +145,8 @@ describe("VaultTimeoutService", () => {
|
||||
authService.getAuthStatus.mockImplementation((userId) => {
|
||||
return Promise.resolve(accounts[userId]?.authStatus);
|
||||
});
|
||||
stateService.getIsAuthenticated.mockImplementation((options) => {
|
||||
// Just like actual state service, if no userId is given fallback to active userId
|
||||
return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated);
|
||||
tokenService.hasAccessToken$.mockImplementation((userId) => {
|
||||
return of(accounts[userId]?.isAuthenticated ?? false);
|
||||
});
|
||||
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation((userId) => {
|
||||
@@ -201,7 +204,7 @@ describe("VaultTimeoutService", () => {
|
||||
|
||||
const expectUserToHaveLocked = (userId: string) => {
|
||||
// This does NOT assert all the things that the lock process does
|
||||
expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId });
|
||||
expect(tokenService.hasAccessToken$).toHaveBeenCalledWith(userId);
|
||||
expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId);
|
||||
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId });
|
||||
expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||
@@ -43,6 +44,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
private messagingService: MessagingService,
|
||||
private searchService: SearchService,
|
||||
private stateService: StateService,
|
||||
private tokenService: TokenService,
|
||||
private authService: AuthService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
@@ -108,7 +110,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
async lock(userId?: UserId): Promise<void> {
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
|
||||
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
const lockingUserId =
|
||||
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
|
||||
const authed = await firstValueFrom(this.tokenService.hasAccessToken$(lockingUserId));
|
||||
if (!authed) {
|
||||
return;
|
||||
}
|
||||
@@ -121,12 +126,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
await this.logOut(userId);
|
||||
}
|
||||
|
||||
const currentUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const lockingUserId = userId ?? currentUserId;
|
||||
|
||||
// HACK: Start listening for the transition of the locking user from something to the locked state.
|
||||
// This is very much a hack to ensure that the authentication status to retrievable right after
|
||||
// it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead
|
||||
|
||||
@@ -1,63 +1 @@
|
||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
|
||||
/**
|
||||
* Options for customizing the initiation behavior.
|
||||
*/
|
||||
export type InitOptions = {
|
||||
/**
|
||||
* Whether or not to run state migrations as part of the init process. Defaults to true.
|
||||
*
|
||||
* If false, the init method will instead wait for migrations to complete before doing its
|
||||
* other init operations. Make sure migrations have either already completed, or will complete
|
||||
* before calling {@link StateService.init} with `runMigrations: false`.
|
||||
*/
|
||||
runMigrations?: boolean;
|
||||
};
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
abstract addAccount(account: T): Promise<void>;
|
||||
abstract clean(options?: StorageOptions): Promise<void>;
|
||||
abstract init(initOptions?: InitOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the user's auto key
|
||||
*/
|
||||
abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise<string>;
|
||||
/**
|
||||
* Sets the user's auto key
|
||||
*/
|
||||
abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise<void>;
|
||||
/**
|
||||
* Gets the user's biometric key
|
||||
*/
|
||||
abstract getUserKeyBiometric(options?: StorageOptions): Promise<string>;
|
||||
/**
|
||||
* Checks if the user has a biometric key available
|
||||
*/
|
||||
abstract hasUserKeyBiometric(options?: StorageOptions): Promise<boolean>;
|
||||
/**
|
||||
* Sets the user's biometric key
|
||||
*/
|
||||
abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void>;
|
||||
/**
|
||||
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
|
||||
*/
|
||||
abstract setEnableDuckDuckGoBrowserIntegration(
|
||||
value: boolean,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string>;
|
||||
abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead.
|
||||
*/
|
||||
abstract getIsAuthenticated(options?: StorageOptions): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* @deprecated Use `AccountService.activeAccount$` instead.
|
||||
*/
|
||||
abstract getUserId(options?: StorageOptions): Promise<string>;
|
||||
}
|
||||
export { StateService } from "@bitwarden/state";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Account } from "../models/domain/account";
|
||||
|
||||
export class AccountFactory<T extends Account = Account> {
|
||||
private accountConstructor: new (init: Partial<T>) => T;
|
||||
|
||||
constructor(accountConstructor: new (init: Partial<T>) => T) {
|
||||
this.accountConstructor = accountConstructor;
|
||||
}
|
||||
|
||||
create(args: Partial<T>) {
|
||||
return new this.accountConstructor(args);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
|
||||
export class GlobalStateFactory<T extends GlobalState = GlobalState> {
|
||||
private globalStateConstructor: new (init: Partial<T>) => T;
|
||||
|
||||
constructor(globalStateConstructor: new (init: Partial<T>) => T) {
|
||||
this.globalStateConstructor = globalStateConstructor;
|
||||
}
|
||||
|
||||
create(args?: Partial<T>) {
|
||||
return new this.globalStateConstructor(args);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Account } from "../models/domain/account";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
|
||||
import { AccountFactory } from "./account-factory";
|
||||
import { GlobalStateFactory } from "./global-state-factory";
|
||||
|
||||
export class StateFactory<
|
||||
TGlobal extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> {
|
||||
private globalStateFactory: GlobalStateFactory<TGlobal>;
|
||||
private accountFactory: AccountFactory<TAccount>;
|
||||
|
||||
constructor(
|
||||
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
|
||||
accountConstructor: new (init: Partial<TAccount>) => TAccount,
|
||||
) {
|
||||
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
|
||||
this.accountFactory = new AccountFactory(accountConstructor);
|
||||
}
|
||||
|
||||
createGlobal(args: Partial<TGlobal>): TGlobal {
|
||||
return this.globalStateFactory.create(args);
|
||||
}
|
||||
|
||||
createAccount(args: Partial<TAccount>): TAccount {
|
||||
return this.accountFactory.create(args);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { AccountKeys, EncryptionPair } from "./account";
|
||||
|
||||
describe("AccountKeys", () => {
|
||||
describe("toJSON", () => {
|
||||
it("should serialize itself", () => {
|
||||
const keys = new AccountKeys();
|
||||
const buffer = makeStaticByteArray(64);
|
||||
keys.publicKey = buffer;
|
||||
|
||||
const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString");
|
||||
keys.toJSON();
|
||||
expect(bufferSpy).toHaveBeenCalledWith(buffer);
|
||||
});
|
||||
|
||||
it("should serialize public key as a string", () => {
|
||||
const keys = new AccountKeys();
|
||||
keys.publicKey = Utils.fromByteStringToArray("hello");
|
||||
const json = JSON.stringify(keys);
|
||||
expect(json).toContain('"publicKey":"hello"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize public key to a buffer", () => {
|
||||
const keys = AccountKeys.fromJSON({
|
||||
publicKey: "hello",
|
||||
});
|
||||
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello"));
|
||||
});
|
||||
|
||||
it("should deserialize privateKey", () => {
|
||||
const spy = jest.spyOn(EncryptionPair, "fromJSON");
|
||||
AccountKeys.fromJSON({
|
||||
privateKey: { encrypted: "encrypted", decrypted: "decrypted" },
|
||||
} as any);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AccountProfile } from "./account";
|
||||
|
||||
describe("AccountProfile", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize to an instance of itself", () => {
|
||||
expect(AccountProfile.fromJSON({})).toBeInstanceOf(AccountProfile);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Account, AccountKeys, AccountProfile } from "./account";
|
||||
|
||||
describe("Account", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize to an instance of itself", () => {
|
||||
expect(Account.fromJSON({})).toBeInstanceOf(Account);
|
||||
});
|
||||
|
||||
it("should call all the sub-fromJSONs", () => {
|
||||
const keysSpy = jest.spyOn(AccountKeys, "fromJSON");
|
||||
const profileSpy = jest.spyOn(AccountProfile, "fromJSON");
|
||||
|
||||
Account.fromJSON({});
|
||||
|
||||
expect(keysSpy).toHaveBeenCalled();
|
||||
expect(profileSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
decrypted?: TDecrypted;
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
encrypted: this.encrypted,
|
||||
decrypted:
|
||||
this.decrypted instanceof ArrayBuffer
|
||||
? Utils.fromBufferToByteString(this.decrypted)
|
||||
: this.decrypted,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON<TEncrypted, TDecrypted>(
|
||||
obj: { encrypted?: Jsonify<TEncrypted>; decrypted?: string | Jsonify<TDecrypted> },
|
||||
decryptedFromJson?: (decObj: Jsonify<TDecrypted> | string) => TDecrypted,
|
||||
encryptedFromJson?: (encObj: Jsonify<TEncrypted>) => TEncrypted,
|
||||
) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pair = new EncryptionPair<TEncrypted, TDecrypted>();
|
||||
if (obj?.encrypted != null) {
|
||||
pair.encrypted = encryptedFromJson
|
||||
? encryptedFromJson(obj.encrypted)
|
||||
: (obj.encrypted as TEncrypted);
|
||||
}
|
||||
if (obj?.decrypted != null) {
|
||||
pair.decrypted = decryptedFromJson
|
||||
? decryptedFromJson(obj.decrypted)
|
||||
: (obj.decrypted as TDecrypted);
|
||||
}
|
||||
return pair;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountKeys {
|
||||
publicKey?: Uint8Array;
|
||||
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
|
||||
toJSON() {
|
||||
// If you pass undefined into fromBufferToByteString, you will get an empty string back
|
||||
// which will cause all sorts of headaches down the line when you try to getPublicKey
|
||||
// and expect a Uint8Array and get an empty string instead.
|
||||
return Utils.merge(this, {
|
||||
publicKey: this.publicKey ? Utils.fromBufferToByteString(this.publicKey) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static fromJSON(obj: DeepJsonify<AccountKeys>): AccountKeys {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return Object.assign(new AccountKeys(), obj, {
|
||||
cryptoSymmetricKey: EncryptionPair.fromJSON(
|
||||
obj?.cryptoSymmetricKey,
|
||||
SymmetricCryptoKey.fromJSON,
|
||||
),
|
||||
publicKey: Utils.fromByteStringToArray(obj?.publicKey),
|
||||
});
|
||||
}
|
||||
|
||||
static initRecordEncryptionPairsFromJSON(obj: any) {
|
||||
return EncryptionPair.fromJSON(obj, (decObj: any) => {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record: Record<string, SymmetricCryptoKey> = {};
|
||||
for (const id in decObj) {
|
||||
record[id] = SymmetricCryptoKey.fromJSON(decObj[id]);
|
||||
}
|
||||
return record;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountProfile {
|
||||
name?: string;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
userId?: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountProfile>): AccountProfile {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new AccountProfile(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
export class Account {
|
||||
keys?: AccountKeys = new AccountKeys();
|
||||
profile?: AccountProfile = new AccountProfile();
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
Object.assign(this, {
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...init?.keys,
|
||||
},
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...init?.profile,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static fromJSON(json: Jsonify<Account>): Account {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new Account({}), json, {
|
||||
keys: AccountKeys.fromJSON(json?.keys),
|
||||
profile: AccountProfile.fromJSON(json?.profile),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { EncryptionPair } from "./account";
|
||||
|
||||
describe("EncryptionPair", () => {
|
||||
describe("toJSON", () => {
|
||||
it("should populate decryptedSerialized for buffer arrays", () => {
|
||||
const pair = new EncryptionPair<string, ArrayBuffer>();
|
||||
pair.decrypted = Utils.fromByteStringToArray("hello").buffer;
|
||||
const json = pair.toJSON();
|
||||
expect(json.decrypted).toEqual("hello");
|
||||
});
|
||||
|
||||
it("should populate decryptedSerialized for TypesArrays", () => {
|
||||
const pair = new EncryptionPair<string, Uint8Array>();
|
||||
pair.decrypted = Utils.fromByteStringToArray("hello");
|
||||
const json = pair.toJSON();
|
||||
expect(json.decrypted).toEqual(new Uint8Array([104, 101, 108, 108, 111]));
|
||||
});
|
||||
|
||||
it("should serialize encrypted and decrypted", () => {
|
||||
const pair = new EncryptionPair<string, string>();
|
||||
pair.encrypted = "hello";
|
||||
pair.decrypted = "world";
|
||||
const json = pair.toJSON();
|
||||
expect(json.encrypted).toEqual("hello");
|
||||
expect(json.decrypted).toEqual("world");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize encrypted and decrypted", () => {
|
||||
const pair = EncryptionPair.fromJSON({
|
||||
encrypted: "hello",
|
||||
decrypted: "world",
|
||||
});
|
||||
expect(pair.encrypted).toEqual("hello");
|
||||
expect(pair.decrypted).toEqual("world");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Account } from "./account";
|
||||
import { State } from "./state";
|
||||
|
||||
describe("state", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize to an instance of itself", () => {
|
||||
expect(State.fromJSON({}, () => new Account({}))).toBeInstanceOf(State);
|
||||
});
|
||||
|
||||
it("should always assign an object to accounts", () => {
|
||||
const state = State.fromJSON({}, () => new Account({}));
|
||||
expect(state.accounts).not.toBeNull();
|
||||
expect(state.accounts).toEqual({});
|
||||
});
|
||||
|
||||
it("should build an account map", () => {
|
||||
const accountsSpy = jest.spyOn(Account, "fromJSON");
|
||||
const state = State.fromJSON(
|
||||
{
|
||||
accounts: {
|
||||
userId: {},
|
||||
},
|
||||
},
|
||||
Account.fromJSON,
|
||||
);
|
||||
|
||||
expect(state.accounts["userId"]).toBeInstanceOf(Account);
|
||||
expect(accountsSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Account } from "./account";
|
||||
import { GlobalState } from "./global-state";
|
||||
|
||||
export class State<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> {
|
||||
accounts: { [userId: string]: TAccount } = {};
|
||||
globals: TGlobalState;
|
||||
|
||||
constructor(globals: TGlobalState) {
|
||||
this.globals = globals;
|
||||
}
|
||||
|
||||
// TODO, make Jsonify<State,TGlobalState,TAccount> work. It currently doesn't because Globals doesn't implement Jsonify.
|
||||
static fromJSON<TGlobalState extends GlobalState, TAccount extends Account>(
|
||||
obj: any,
|
||||
accountDeserializer: (json: Jsonify<TAccount>) => TAccount,
|
||||
): State<TGlobalState, TAccount> {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new State(null), obj, {
|
||||
accounts: State.buildAccountMapFromJSON(obj?.accounts, accountDeserializer),
|
||||
});
|
||||
}
|
||||
|
||||
private static buildAccountMapFromJSON<TAccount extends Account>(
|
||||
jsonAccounts: { [userId: string]: Jsonify<TAccount> },
|
||||
accountDeserializer: (json: Jsonify<TAccount>) => TAccount,
|
||||
) {
|
||||
if (!jsonAccounts) {
|
||||
return {};
|
||||
}
|
||||
const accounts: { [userId: string]: TAccount } = {};
|
||||
for (const userId in jsonAccounts) {
|
||||
accounts[userId] = accountDeserializer(jsonAccounts[userId]);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
@@ -1,659 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify, JsonValue } from "type-fest";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { TokenService } from "../../auth/abstractions/token.service";
|
||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { EnvironmentService } from "../abstractions/environment.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import {
|
||||
InitOptions,
|
||||
StateService as StateServiceAbstraction,
|
||||
} from "../abstractions/state.service";
|
||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation, StorageLocation } from "../enums";
|
||||
import { StateFactory } from "../factories/state-factory";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
|
||||
import { MigrationRunner } from "./migration-runner";
|
||||
|
||||
const keys = {
|
||||
state: "state",
|
||||
stateVersion: "stateVersion",
|
||||
global: "global",
|
||||
tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
userAutoKey: "_user_auto",
|
||||
userBiometricKey: "_user_biometric",
|
||||
|
||||
autoKey: "_masterkey_auto",
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
|
||||
|
||||
export class StateService<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> implements StateServiceAbstraction<TAccount>
|
||||
{
|
||||
private hasBeenInited = false;
|
||||
protected isRecoveredSession = false;
|
||||
|
||||
// default account serializer, must be overridden by child class
|
||||
protected accountDeserializer = Account.fromJSON as (json: Jsonify<TAccount>) => TAccount;
|
||||
|
||||
constructor(
|
||||
protected storageService: AbstractStorageService,
|
||||
protected secureStorageService: AbstractStorageService,
|
||||
protected memoryStorageService: AbstractStorageService,
|
||||
protected logService: LogService,
|
||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||
protected accountService: AccountService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected tokenService: TokenService,
|
||||
private migrationRunner: MigrationRunner,
|
||||
) {}
|
||||
|
||||
async init(initOptions: InitOptions = {}): Promise<void> {
|
||||
// Deconstruct and apply defaults
|
||||
const { runMigrations = true } = initOptions;
|
||||
if (this.hasBeenInited) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runMigrations) {
|
||||
await this.migrationRunner.run();
|
||||
} else {
|
||||
// It may have been requested to not run the migrations but we should defensively not
|
||||
// continue this method until migrations have a chance to be completed elsewhere.
|
||||
await this.migrationRunner.waitForCompletion();
|
||||
}
|
||||
|
||||
await this.state().then(async (state) => {
|
||||
if (state == null) {
|
||||
await this.setState(new State<TGlobalState, TAccount>(this.createGlobals()));
|
||||
} else {
|
||||
this.isRecoveredSession = true;
|
||||
}
|
||||
});
|
||||
await this.initAccountState();
|
||||
|
||||
this.hasBeenInited = true;
|
||||
}
|
||||
|
||||
async initAccountState() {
|
||||
if (this.isRecoveredSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all likely authenticated accounts
|
||||
const authenticatedAccounts = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))),
|
||||
);
|
||||
|
||||
await this.updateState(async (state) => {
|
||||
for (const i in authenticatedAccounts) {
|
||||
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
async syncAccountFromDisk(userId: string): Promise<State<TGlobalState, TAccount>> {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
||||
const state = await this.updateState(async (state) => {
|
||||
if (state.accounts == null) {
|
||||
state.accounts = {};
|
||||
}
|
||||
state.accounts[userId] = this.createAccount();
|
||||
|
||||
if (diskAccount == null) {
|
||||
// Return early because we can't set the diskAccount.profile
|
||||
// if diskAccount itself is null
|
||||
return state;
|
||||
}
|
||||
|
||||
state.accounts[userId].profile = diskAccount.profile;
|
||||
return state;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async addAccount(account: TAccount) {
|
||||
await this.updateState(async (state) => {
|
||||
state.accounts[account.profile.userId] = account;
|
||||
return state;
|
||||
});
|
||||
await this.scaffoldNewAccountStorage(account);
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
await this.deAuthenticateAccount(options.userId);
|
||||
|
||||
await this.removeAccountFromDisk(options?.userId);
|
||||
await this.removeAccountFromMemory(options?.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* user key when using the "never" option of vault timeout
|
||||
*/
|
||||
async getUserKeyAutoUnlock(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "auto" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}${partialKeys.userAutoKey}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* user key when using the "never" option of vault timeout
|
||||
*/
|
||||
async setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "auto" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* User's encrypted symmetric key when using biometrics
|
||||
*/
|
||||
async getUserKeyBiometric(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "biometric" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}${partialKeys.userBiometricKey}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async hasUserKeyBiometric(options?: StorageOptions): Promise<boolean> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "biometric" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return false;
|
||||
}
|
||||
return await this.secureStorageService.has(
|
||||
`${options.userId}${partialKeys.userBiometricKey}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(
|
||||
this.reconcileOptions(options, { keySuffix: "biometric" }),
|
||||
await this.defaultSecureStorageOptions(),
|
||||
);
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
|
||||
}
|
||||
|
||||
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(DDG_SHARED_KEY, options);
|
||||
}
|
||||
|
||||
async setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
value == null
|
||||
? await this.secureStorageService.remove(DDG_SHARED_KEY, options)
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async setEnableDuckDuckGoBrowserIntegration(
|
||||
value: boolean,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
globals.enableDuckDuckGoBrowserIntegration = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UserKey instead
|
||||
*/
|
||||
async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.keys.cryptoSymmetricKey.encrypted;
|
||||
}
|
||||
|
||||
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
|
||||
(await this.getUserId(options)) != null
|
||||
);
|
||||
}
|
||||
|
||||
async getUserId(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.profile?.userId;
|
||||
}
|
||||
|
||||
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||
let globals: TGlobalState;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
globals = await this.getGlobalsFromMemory();
|
||||
}
|
||||
|
||||
if (this.useDisk && globals == null) {
|
||||
globals = await this.getGlobalsFromDisk(options);
|
||||
}
|
||||
|
||||
if (globals == null) {
|
||||
globals = this.createGlobals();
|
||||
}
|
||||
|
||||
return globals;
|
||||
}
|
||||
|
||||
protected async saveGlobals(globals: TGlobalState, options: StorageOptions) {
|
||||
return this.useMemory(options.storageLocation)
|
||||
? this.saveGlobalsToMemory(globals)
|
||||
: await this.saveGlobalsToDisk(globals, options);
|
||||
}
|
||||
|
||||
protected async getGlobalsFromMemory(): Promise<TGlobalState> {
|
||||
return (await this.state()).globals;
|
||||
}
|
||||
|
||||
protected async getGlobalsFromDisk(options: StorageOptions): Promise<TGlobalState> {
|
||||
return await this.storageService.get<TGlobalState>(keys.global, options);
|
||||
}
|
||||
|
||||
protected async saveGlobalsToMemory(globals: TGlobalState): Promise<void> {
|
||||
await this.updateState(async (state) => {
|
||||
state.globals = globals;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
protected async saveGlobalsToDisk(globals: TGlobalState, options: StorageOptions): Promise<void> {
|
||||
if (options.useSecureStorage) {
|
||||
await this.secureStorageService.save(keys.global, globals, options);
|
||||
} else {
|
||||
await this.storageService.save(keys.global, globals, options);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAccount(options: StorageOptions): Promise<TAccount> {
|
||||
try {
|
||||
let account: TAccount;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
account = await this.getAccountFromMemory(options);
|
||||
}
|
||||
|
||||
if (this.useDisk(options.storageLocation) && account == null) {
|
||||
account = await this.getAccountFromDisk(options);
|
||||
}
|
||||
|
||||
return account;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAccountFromMemory(options: StorageOptions): Promise<TAccount> {
|
||||
const userId =
|
||||
options.userId ??
|
||||
(await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
));
|
||||
|
||||
return await this.state().then(async (state) => {
|
||||
if (state.accounts == null) {
|
||||
return null;
|
||||
}
|
||||
return state.accounts[userId];
|
||||
});
|
||||
}
|
||||
|
||||
protected async getAccountFromDisk(options: StorageOptions): Promise<TAccount> {
|
||||
const userId =
|
||||
options.userId ??
|
||||
(await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
));
|
||||
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = options?.useSecureStorage
|
||||
? ((await this.secureStorageService.get<TAccount>(options.userId, options)) ??
|
||||
(await this.storageService.get<TAccount>(
|
||||
options.userId,
|
||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
|
||||
)))
|
||||
: await this.storageService.get<TAccount>(options.userId, options);
|
||||
return account;
|
||||
}
|
||||
|
||||
protected useMemory(storageLocation: StorageLocation) {
|
||||
return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both;
|
||||
}
|
||||
|
||||
protected useDisk(storageLocation: StorageLocation) {
|
||||
return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both;
|
||||
}
|
||||
|
||||
protected async saveAccount(
|
||||
account: TAccount,
|
||||
options: StorageOptions = {
|
||||
storageLocation: StorageLocation.Both,
|
||||
useSecureStorage: false,
|
||||
},
|
||||
) {
|
||||
return this.useMemory(options.storageLocation)
|
||||
? await this.saveAccountToMemory(account)
|
||||
: await this.saveAccountToDisk(account, options);
|
||||
}
|
||||
|
||||
protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise<void> {
|
||||
const storageLocation = options.useSecureStorage
|
||||
? this.secureStorageService
|
||||
: this.storageService;
|
||||
|
||||
await storageLocation.save(`${options.userId}`, account, options);
|
||||
}
|
||||
|
||||
protected async saveAccountToMemory(account: TAccount): Promise<void> {
|
||||
if ((await this.getAccountFromMemory({ userId: account.profile.userId })) !== null) {
|
||||
await this.updateState((state) => {
|
||||
return new Promise((resolve) => {
|
||||
state.accounts[account.profile.userId] = account;
|
||||
resolve(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountStorage(account: TAccount): Promise<void> {
|
||||
// We don't want to manipulate the referenced in memory account
|
||||
const deepClone = JSON.parse(JSON.stringify(account));
|
||||
await this.scaffoldNewAccountLocalStorage(deepClone);
|
||||
await this.scaffoldNewAccountSessionStorage(deepClone);
|
||||
await this.scaffoldNewAccountMemoryStorage(deepClone);
|
||||
}
|
||||
|
||||
// TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService.
|
||||
// For now these methods exist with some redundancy to facilitate this special web requirement.
|
||||
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(
|
||||
{ userId: account.profile.userId },
|
||||
await this.defaultOnDiskLocalOptions(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
||||
await this.storageService.save(
|
||||
account.profile.userId,
|
||||
account,
|
||||
await this.defaultOnDiskMemoryOptions(),
|
||||
);
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(
|
||||
{ userId: account.profile.userId },
|
||||
await this.defaultOnDiskMemoryOptions(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
||||
await this.storageService.save(
|
||||
account.profile.userId,
|
||||
account,
|
||||
await this.defaultOnDiskMemoryOptions(),
|
||||
);
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
protected reconcileOptions(
|
||||
requestedOptions: StorageOptions,
|
||||
defaultOptions: StorageOptions,
|
||||
): StorageOptions {
|
||||
if (requestedOptions == null) {
|
||||
return defaultOptions;
|
||||
}
|
||||
requestedOptions.userId = requestedOptions?.userId ?? defaultOptions.userId;
|
||||
requestedOptions.storageLocation =
|
||||
requestedOptions?.storageLocation ?? defaultOptions.storageLocation;
|
||||
requestedOptions.useSecureStorage =
|
||||
requestedOptions?.useSecureStorage ?? defaultOptions.useSecureStorage;
|
||||
requestedOptions.htmlStorageLocation =
|
||||
requestedOptions?.htmlStorageLocation ?? defaultOptions.htmlStorageLocation;
|
||||
requestedOptions.keySuffix = requestedOptions?.keySuffix ?? defaultOptions.keySuffix;
|
||||
return requestedOptions;
|
||||
}
|
||||
|
||||
protected async defaultInMemoryOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Memory,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Memory,
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
useSecureStorage: true,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getActiveUserIdFromStorage(): Promise<string> {
|
||||
return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
}
|
||||
|
||||
protected async removeAccountFromLocalStorage(userId: string = null): Promise<void> {
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
const storedAccount = await this.getAccount(
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
await this.saveAccount(
|
||||
this.resetAccount(storedAccount),
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
protected async removeAccountFromSessionStorage(userId: string = null): Promise<void> {
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
const storedAccount = await this.getAccount(
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
await this.saveAccount(
|
||||
this.resetAccount(storedAccount),
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
protected async removeAccountFromSecureStorage(userId: string = null): Promise<void> {
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
await this.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.setUserKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
|
||||
protected async removeAccountFromMemory(userId: string = null): Promise<void> {
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
await this.updateState(async (state) => {
|
||||
delete state.accounts[userId];
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
// settings persist even on reset, and are not affected by this method
|
||||
protected resetAccount(account: TAccount) {
|
||||
// All settings have been moved to StateProviders
|
||||
return this.createAccount();
|
||||
}
|
||||
|
||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||
return this.stateFactory.createAccount(init);
|
||||
}
|
||||
|
||||
protected createGlobals(init: Partial<TGlobalState> = null): TGlobalState {
|
||||
return this.stateFactory.createGlobal(init);
|
||||
}
|
||||
|
||||
protected async deAuthenticateAccount(userId: string): Promise<void> {
|
||||
// We must have a manual call to clear tokens as we can't leverage state provider to clean
|
||||
// up our data as we have secure storage in the mix.
|
||||
await this.tokenService.clearTokens(userId as UserId);
|
||||
}
|
||||
|
||||
protected async removeAccountFromDisk(userId: string) {
|
||||
await this.removeAccountFromSessionStorage(userId);
|
||||
await this.removeAccountFromLocalStorage(userId);
|
||||
await this.removeAccountFromSecureStorage(userId);
|
||||
}
|
||||
|
||||
protected async saveSecureStorageKey<T extends JsonValue>(
|
||||
key: string,
|
||||
value: T | null,
|
||||
options?: StorageOptions,
|
||||
) {
|
||||
return value == null
|
||||
? await this.secureStorageService.remove(`${options.userId}${key}`, options)
|
||||
: await this.secureStorageService.save(`${options.userId}${key}`, value, options);
|
||||
}
|
||||
|
||||
protected async state(): Promise<State<TGlobalState, TAccount>> {
|
||||
let state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state);
|
||||
if (this.memoryStorageService.valuesRequireDeserialization) {
|
||||
state = State.fromJSON(state, this.accountDeserializer);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private async setState(
|
||||
state: State<TGlobalState, TAccount>,
|
||||
): Promise<State<TGlobalState, TAccount>> {
|
||||
await this.memoryStorageService.save(keys.state, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
protected async updateState(
|
||||
stateUpdater: (state: State<TGlobalState, TAccount>) => Promise<State<TGlobalState, TAccount>>,
|
||||
): Promise<State<TGlobalState, TAccount>> {
|
||||
return await this.state().then(async (state) => {
|
||||
const updatedState = await stateUpdater(state);
|
||||
if (updatedState == null) {
|
||||
throw new Error("Attempted to update state to null value");
|
||||
}
|
||||
|
||||
return await this.setState(updatedState);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { TokenService } from "../../auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import {
|
||||
SyncCipherNotification,
|
||||
@@ -26,7 +27,6 @@ import { SyncService } from "../../vault/abstractions/sync/sync.service.abstract
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { MessageSender } from "../messaging";
|
||||
import { StateProvider, SYNC_DISK, UserKeyDefinition } from "../state";
|
||||
|
||||
@@ -44,7 +44,7 @@ export abstract class CoreSyncService implements SyncService {
|
||||
syncInProgress = false;
|
||||
|
||||
constructor(
|
||||
protected readonly stateService: StateService,
|
||||
readonly tokenService: TokenService,
|
||||
protected readonly folderService: InternalFolderService,
|
||||
protected readonly folderApiService: FolderApiServiceAbstraction,
|
||||
protected readonly messageSender: MessageSender,
|
||||
@@ -256,7 +256,13 @@ export abstract class CoreSyncService implements SyncService {
|
||||
|
||||
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
if (await this.stateService.getIsAuthenticated()) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
if (
|
||||
activeUserId != null &&
|
||||
(await firstValueFrom(this.tokenService.hasAccessToken$(activeUserId)))
|
||||
) {
|
||||
await this.sendService.delete(notification.id);
|
||||
this.messageSender.send("syncedDeletedSend", { sendId: notification.id });
|
||||
// TODO: Update syncCompleted userId when send service allows modification of non-active users
|
||||
|
||||
@@ -36,7 +36,6 @@ 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";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { MessageSender } from "../messaging";
|
||||
import { StateProvider } from "../state";
|
||||
|
||||
@@ -57,7 +56,6 @@ describe("DefaultSyncService", () => {
|
||||
let sendService: MockProxy<InternalSendService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let providerService: MockProxy<ProviderService>;
|
||||
let folderApiService: MockProxy<FolderApiServiceAbstraction>;
|
||||
let organizationService: MockProxy<InternalOrganizationServiceAbstraction>;
|
||||
@@ -86,7 +84,6 @@ describe("DefaultSyncService", () => {
|
||||
sendService = mock();
|
||||
logService = mock();
|
||||
keyConnectorService = mock();
|
||||
stateService = mock();
|
||||
providerService = mock();
|
||||
folderApiService = mock();
|
||||
organizationService = mock();
|
||||
@@ -113,7 +110,6 @@ describe("DefaultSyncService", () => {
|
||||
sendService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
stateService,
|
||||
providerService,
|
||||
folderApiService,
|
||||
organizationService,
|
||||
|
||||
@@ -53,7 +53,6 @@ import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { CipherResponse } from "../../vault/models/response/cipher.response";
|
||||
import { FolderResponse } from "../../vault/models/response/folder.response";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { MessageSender } from "../messaging";
|
||||
import { StateProvider } from "../state";
|
||||
|
||||
@@ -87,7 +86,6 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
sendService: InternalSendService,
|
||||
logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
stateService: StateService,
|
||||
private providerService: ProviderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
@@ -96,12 +94,12 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
private avatarService: AvatarService,
|
||||
private logoutCallback: (logoutReason: LogoutReason, userId?: UserId) => Promise<void>,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private tokenService: TokenService,
|
||||
tokenService: TokenService,
|
||||
authService: AuthService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(
|
||||
stateService,
|
||||
tokenService,
|
||||
folderService,
|
||||
folderApiService,
|
||||
messageSender,
|
||||
|
||||
@@ -20,7 +20,6 @@ import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
@@ -94,7 +93,6 @@ let accountService: FakeAccountService;
|
||||
|
||||
describe("Cipher Service", () => {
|
||||
const keyService = mock<KeyService>();
|
||||
const stateService = mock<StateService>();
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const apiService = mock<ApiService>();
|
||||
@@ -127,7 +125,6 @@ describe("Cipher Service", () => {
|
||||
apiService,
|
||||
i18nService,
|
||||
searchService,
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
cipherFileUploadService,
|
||||
@@ -470,8 +467,6 @@ describe("Cipher Service", () => {
|
||||
|
||||
searchService.indexedEntityId$.mockReturnValue(of(null));
|
||||
|
||||
stateService.getUserId.mockResolvedValue(mockUserId);
|
||||
|
||||
const keys = { userKey: originalUserKey } as CipherDecryptionKeys;
|
||||
keyService.cipherDecryptionKeys$.mockReturnValue(of(keys));
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import { ListResponse } from "../../models/response/list.response";
|
||||
import { View } from "../../models/view/view";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import Domain from "../../platform/models/domain/domain-base";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
@@ -110,7 +109,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private searchService: SearchService,
|
||||
private stateService: StateService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private encryptService: EncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService,
|
||||
|
||||
@@ -732,7 +732,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
|
||||
protected async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: UserId,
|
||||
userId: UserId,
|
||||
): Promise<UserKey | null> {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
export * from "./core";
|
||||
export * from "./state-migrations";
|
||||
export * from "./types/state";
|
||||
export * from "./legacy";
|
||||
|
||||
107
libs/state/src/legacy/default-state.service.ts
Normal file
107
libs/state/src/legacy/default-state.service.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { StorageService } from "@bitwarden/storage-core";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { ActiveUserAccessor } from "../core";
|
||||
|
||||
import { GlobalState } from "./global-state";
|
||||
import { RequiredUserId, StateService } from "./state.service";
|
||||
|
||||
const keys = {
|
||||
global: "global",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
userAutoKey: "_user_auto",
|
||||
userBiometricKey: "_user_biometric",
|
||||
};
|
||||
|
||||
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
|
||||
|
||||
export class DefaultStateService implements StateService {
|
||||
constructor(
|
||||
private readonly storageService: StorageService,
|
||||
private readonly secureStorageService: StorageService,
|
||||
private readonly activeUserAccessor: ActiveUserAccessor,
|
||||
) {}
|
||||
|
||||
async clean(options: RequiredUserId): Promise<void> {
|
||||
await this.setUserKeyAutoUnlock(null, options);
|
||||
await this.clearUserKeyBiometric(options.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* user key when using the "never" option of vault timeout
|
||||
*/
|
||||
async getUserKeyAutoUnlock(options: RequiredUserId): Promise<string | null> {
|
||||
if (options.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}${partialKeys.userAutoKey}`,
|
||||
{
|
||||
userId: options.userId,
|
||||
keySuffix: "auto",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* user key when using the "never" option of vault timeout
|
||||
*/
|
||||
async setUserKeyAutoUnlock(value: string | null, options: RequiredUserId): Promise<void> {
|
||||
if (options.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options.userId, "auto");
|
||||
}
|
||||
|
||||
private async clearUserKeyBiometric(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.saveSecureStorageKey(partialKeys.userBiometricKey, null, userId, "biometric");
|
||||
}
|
||||
|
||||
async getDuckDuckGoSharedKey(): Promise<string | null> {
|
||||
const userId = await this.getActiveUserIdFromStorage();
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(DDG_SHARED_KEY);
|
||||
}
|
||||
|
||||
async setDuckDuckGoSharedKey(value: string): Promise<void> {
|
||||
const userId = await this.getActiveUserIdFromStorage();
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
value == null
|
||||
? await this.secureStorageService.remove(DDG_SHARED_KEY)
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value);
|
||||
}
|
||||
|
||||
async setEnableDuckDuckGoBrowserIntegration(value: boolean): Promise<void> {
|
||||
const globals = (await this.storageService.get<GlobalState>(keys.global)) ?? new GlobalState();
|
||||
globals.enableDuckDuckGoBrowserIntegration = value;
|
||||
await this.storageService.save(keys.global, globals);
|
||||
}
|
||||
|
||||
private async getActiveUserIdFromStorage(): Promise<UserId | null> {
|
||||
return await firstValueFrom(this.activeUserAccessor.activeUserId$);
|
||||
}
|
||||
|
||||
private async saveSecureStorageKey(
|
||||
key: string,
|
||||
value: string | null,
|
||||
userId: UserId,
|
||||
keySuffix: string,
|
||||
) {
|
||||
return value == null
|
||||
? await this.secureStorageService.remove(`${userId}${key}`, { keySuffix: keySuffix })
|
||||
: await this.secureStorageService.save(`${userId}${key}`, value, {
|
||||
keySuffix: keySuffix,
|
||||
});
|
||||
}
|
||||
}
|
||||
2
libs/state/src/legacy/index.ts
Normal file
2
libs/state/src/legacy/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { StateService } from "./state.service";
|
||||
export { DefaultStateService } from "./default-state.service";
|
||||
25
libs/state/src/legacy/state.service.ts
Normal file
25
libs/state/src/legacy/state.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export type RequiredUserId = { userId: UserId };
|
||||
|
||||
/**
|
||||
* This class exists for various legacy reasons, there are likely better things to use than this service.
|
||||
*/
|
||||
export abstract class StateService {
|
||||
abstract clean(options: RequiredUserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the user's auto key
|
||||
*/
|
||||
abstract getUserKeyAutoUnlock(options: RequiredUserId): Promise<string | null>;
|
||||
/**
|
||||
* Sets the user's auto key
|
||||
*/
|
||||
abstract setUserKeyAutoUnlock(value: string | null, options: RequiredUserId): Promise<void>;
|
||||
/**
|
||||
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
|
||||
*/
|
||||
abstract setEnableDuckDuckGoBrowserIntegration(value: boolean): Promise<void>;
|
||||
abstract getDuckDuckGoSharedKey(): Promise<string | null>;
|
||||
abstract setDuckDuckGoSharedKey(value: string): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user