1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-06 09:20:43 +01:00

[Bug] [Account Switching] Improve State Management Performance (#611)

* [bug] Improve state management performance

Large vaults see a clear degrade in performance using the state service, especially when multiple vaults are authed and unlocked at the same time.

Some changes made to address this:
1. Clearing in memory decrypted data for non active accounts. This really should have been something we were doing anyway, but letting go of that memory burden has a noticable performance boost.
2. Not loading a bunch of unecsassary data from disk accounts into memory on application startup. This was being done to initilize in memory accounts, but brought a lot of extra baggage with it like storing encrypted data in memory, even though it is never referenced that way.
3. Breaking the on disk state object up into seperate keys for accounts instead of storing everything together under a "state" key. This ensures there is less information fetched from disk each time we call for an account.

There were some restructuring changes needed to facilitate these items:
1. We need to be able to construct an account in the StateService, but typescript doesn't allow for new() constraints on generics so a factory needs to be created and passed into the StateService for this to work.
2. Since we can't reference an all-knowing "accounts" object for on disk state anymore we have to maintain a list of authenticated accounts, and this has been added.
3. The StateMigration service needed to be updated to break up the accounts object, so current dev and QA state will be broken and need to be reset.

Some other general refactorings that were helpful gettings this working:
1. Added a constant for keys to the StateService and StateMigrationService.
2. Bundling everything needed to deauthenticate a user into a dedicated method.
3. Bundling all the disk storage clear methods (that should be refactored later into client specific state services) into one helper method.
4. Bundling everything needed to dynamically select a new active user into a dedicated method.

* [bug] Set environmentUrls appropriatly on account add

* [bug] Stop tracking activity without an active user

* [bug] Remove lastActive from globalState and globalState migration

* [style] Ran prettier
This commit is contained in:
Addison Beck 2022-01-19 10:51:10 -05:00 committed by GitHub
parent cc285e5ea7
commit ccd715d7b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 160 deletions

View File

@ -76,6 +76,8 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service"; import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service"; import { ValidationService } from "./validation.service";
import { Account, AccountFactory } from "jslib-common/models/domain/account";
@NgModule({ @NgModule({
declarations: [], declarations: [],
providers: [ providers: [
@ -325,7 +327,19 @@ import { ValidationService } from "./validation.service";
}, },
{ {
provide: StateServiceAbstraction, provide: StateServiceAbstraction,
useClass: StateService, useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new AccountFactory(Account)
),
deps: [ deps: [
StorageServiceAbstraction, StorageServiceAbstraction,
"SECURE_STORAGE", "SECURE_STORAGE",

View File

@ -183,3 +183,15 @@ export class 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);
}
}

View File

@ -4,7 +4,6 @@ import { EnvironmentUrls } from "./environmentUrls";
export class GlobalState { export class GlobalState {
enableAlwaysOnTop?: boolean; enableAlwaysOnTop?: boolean;
installedVersion?: string; installedVersion?: string;
lastActive?: number;
locale?: string = "en"; locale?: string = "en";
openAtLogin?: boolean; openAtLogin?: boolean;
organizationInvitation?: any; organizationInvitation?: any;

View File

@ -5,4 +5,5 @@ export class State<TAccount extends Account = Account> {
accounts: { [userId: string]: TAccount } = {}; accounts: { [userId: string]: TAccount } = {};
globals: GlobalState = new GlobalState(); globals: GlobalState = new GlobalState();
activeUserId: string; activeUserId: string;
authenticatedAccounts: string[] = [];
} }

View File

@ -1,12 +1,6 @@
import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
import { import { Account, AccountData, AccountFactory } from "../models/domain/account";
Account,
AccountData,
AccountKeys,
AccountProfile,
AccountTokens,
} from "../models/domain/account";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { StorageService } from "../abstractions/storage.service"; import { StorageService } from "../abstractions/storage.service";
@ -43,6 +37,18 @@ import { BehaviorSubject } from "rxjs";
import { StateMigrationService } from "../abstractions/stateMigration.service"; import { StateMigrationService } from "../abstractions/stateMigration.service";
import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { EnvironmentUrls } from "../models/domain/environmentUrls";
const keys = {
global: "global",
authenticatedAccounts: "authenticatedAccounts",
activeUserId: "activeUserId",
};
const partialKeys = {
autoKey: "_masterkey_auto",
biometricKey: "_masterkey_biometric",
masterKey: "_masterkey",
};
export class StateService<TAccount extends Account = Account> export class StateService<TAccount extends Account = Account>
implements StateServiceAbstraction<TAccount> implements StateServiceAbstraction<TAccount>
{ {
@ -55,32 +61,47 @@ export class StateService<TAccount extends Account = Account>
protected storageService: StorageService, protected storageService: StorageService,
protected secureStorageService: StorageService, protected secureStorageService: StorageService,
protected logService: LogService, protected logService: LogService,
protected stateMigrationService: StateMigrationService protected stateMigrationService: StateMigrationService,
protected accountFactory: AccountFactory<TAccount>
) {} ) {}
async init(): Promise<void> { async init(): Promise<void> {
if (await this.stateMigrationService.needsMigration()) { if (await this.stateMigrationService.needsMigration()) {
await this.stateMigrationService.migrate(); await this.stateMigrationService.migrate();
} }
if (this.state.activeUserId == null) {
await this.loadStateFromDisk(); await this.initAccountState();
}
} }
async loadStateFromDisk() { async initAccountState() {
if ((await this.getActiveUserIdFromStorage()) != null) { this.state.authenticatedAccounts =
const diskState = await this.storageService.get<State<TAccount>>( (await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? [];
"state", for (const i in this.state.authenticatedAccounts) {
await this.defaultOnDiskOptions() if (i != null) {
); await this.syncAccountFromDisk(this.state.authenticatedAccounts[i]);
this.state = diskState;
await this.pruneInMemoryAccounts();
await this.pushAccounts();
} }
} }
const storedActiveUser = await this.storageService.get<string>(keys.activeUserId);
if (storedActiveUser != null) {
this.state.activeUserId = storedActiveUser;
}
await this.pushAccounts();
this.activeAccount.next(this.state.activeUserId);
}
async syncAccountFromDisk(userId: string) {
if (userId == null) {
return;
}
this.state.accounts[userId] = this.createAccount();
const diskAccount = await this.getAccountFromDisk({ userId: userId });
this.state.accounts[userId].profile = diskAccount.profile;
}
async addAccount(account: TAccount) { async addAccount(account: TAccount) {
await this.setAccountEnvironmentUrls(account); account = await this.setAccountEnvironmentUrls(account);
this.state.authenticatedAccounts.push(account.profile.userId);
this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts);
this.state.accounts[account.profile.userId] = account; this.state.accounts[account.profile.userId] = account;
await this.scaffoldNewAccountStorage(account); await this.scaffoldNewAccountStorage(account);
await this.setActiveUser(account.profile.userId); await this.setActiveUser(account.profile.userId);
@ -88,36 +109,21 @@ export class StateService<TAccount extends Account = Account>
} }
async setActiveUser(userId: string): Promise<void> { async setActiveUser(userId: string): Promise<void> {
this.clearDecryptedDataForActiveUser();
this.state.activeUserId = userId; this.state.activeUserId = userId;
const storedState = await this.storageService.get<State<TAccount>>( await this.storageService.save(keys.activeUserId, userId);
"state",
await this.defaultOnDiskOptions()
);
storedState.activeUserId = userId;
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
await this.pushAccounts();
this.activeAccount.next(this.state.activeUserId); this.activeAccount.next(this.state.activeUserId);
await this.pushAccounts();
} }
async clean(options?: StorageOptions): Promise<void> { async clean(options?: StorageOptions): Promise<void> {
// Find and set the next active user if any exists options = this.reconcileOptions(options, this.defaultInMemoryOptions);
await this.setAccessToken(null, { userId: options?.userId }); await this.deAuthenticateAccount(options.userId);
if (options?.userId == null || options.userId === (await this.getUserId())) { if (options.userId === this.state.activeUserId) {
for (const userId in this.state.accounts) { await this.dynamicallySetActiveUser();
if (userId == null) {
continue;
}
if (await this.getIsAuthenticated({ userId: userId })) {
await this.setActiveUser(userId);
break;
}
await this.setActiveUser(null);
}
} }
await this.removeAccountFromSessionStorage(options?.userId); await this.removeAccountFromDisk(options?.userId);
await this.removeAccountFromLocalStorage(options?.userId);
await this.removeAccountFromSecureStorage(options?.userId);
this.removeAccountFromMemory(options?.userId); this.removeAccountFromMemory(options?.userId);
await this.pushAccounts(); await this.pushAccounts();
} }
@ -425,7 +431,7 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return null; return null;
} }
return await this.secureStorageService.get(`${options.userId}_masterkey_auto`, options); return await this.secureStorageService.get(`${options.userId}${partialKeys.autoKey}`, options);
} }
async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> { async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> {
@ -436,7 +442,7 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return; return;
} }
await this.secureStorageService.save(`${options.userId}_masterkey_auto`, value, options); await this.secureStorageService.save(`${options.userId}${partialKeys.autoKey}`, value, options);
} }
async getCryptoMasterKeyB64(options?: StorageOptions): Promise<string> { async getCryptoMasterKeyB64(options?: StorageOptions): Promise<string> {
@ -444,7 +450,10 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return null; return null;
} }
return await this.secureStorageService.get(`${options?.userId}_masterkey`, options); return await this.secureStorageService.get(
`${options?.userId}${partialKeys.masterKey}`,
options
);
} }
async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise<void> { async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise<void> {
@ -452,7 +461,11 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return; return;
} }
await this.secureStorageService.save(`${options.userId}_masterkey`, value, options); await this.secureStorageService.save(
`${options.userId}${partialKeys.masterKey}`,
value,
options
);
} }
async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise<string> { async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise<string> {
@ -463,7 +476,10 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return null; return null;
} }
return await this.secureStorageService.get(`${options.userId}_masterkey_biometric`, options); return await this.secureStorageService.get(
`${options.userId}${partialKeys.biometricKey}`,
options
);
} }
async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise<boolean> { async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise<boolean> {
@ -474,7 +490,10 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return false; return false;
} }
return await this.secureStorageService.has(`${options.userId}_masterkey_biometric`, options); return await this.secureStorageService.has(
`${options.userId}${partialKeys.biometricKey}`,
options
);
} }
async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise<void> { async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise<void> {
@ -485,7 +504,11 @@ export class StateService<TAccount extends Account = Account>
if (options?.userId == null) { if (options?.userId == null) {
return; return;
} }
await this.secureStorageService.save(`${options.userId}_masterkey_biometric`, value, options); await this.secureStorageService.save(
`${options.userId}${partialKeys.biometricKey}`,
value,
options
);
} }
async getDecodedToken(options?: StorageOptions): Promise<any> { async getDecodedToken(options?: StorageOptions): Promise<any> {
@ -866,20 +889,16 @@ export class StateService<TAccount extends Account = Account>
} }
async getEmail(options?: StorageOptions): Promise<string> { async getEmail(options?: StorageOptions): Promise<string> {
return ( return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) ?.profile?.email;
)?.profile?.email;
} }
async setEmail(value: string, options?: StorageOptions): Promise<void> { async setEmail(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount( const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()) this.reconcileOptions(options, this.defaultInMemoryOptions)
); );
account.profile.email = value; account.profile.email = value;
await this.saveAccount( await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
} }
async getEmailVerified(options?: StorageOptions): Promise<boolean> { async getEmailVerified(options?: StorageOptions): Promise<boolean> {
@ -1526,14 +1545,9 @@ export class StateService<TAccount extends Account = Account>
} }
async getLastActive(options?: StorageOptions): Promise<number> { async getLastActive(options?: StorageOptions): Promise<number> {
const lastActive = ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.lastActive; )?.profile?.lastActive;
return (
lastActive ??
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.lastActive
);
} }
async setLastActive(value: number, options?: StorageOptions): Promise<void> { async setLastActive(value: number, options?: StorageOptions): Promise<void> {
@ -1547,15 +1561,6 @@ export class StateService<TAccount extends Account = Account>
this.reconcileOptions(options, await this.defaultOnDiskOptions()) this.reconcileOptions(options, await this.defaultOnDiskOptions())
); );
} }
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.lastActive = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
} }
async getLastSync(options?: StorageOptions): Promise<string> { async getLastSync(options?: StorageOptions): Promise<string> {
@ -2091,7 +2096,7 @@ export class StateService<TAccount extends Account = Account>
} }
protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> { protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
return (await this.storageService.get<State<TAccount>>("state", options))?.globals; return await this.storageService.get<GlobalState>(keys.global, options);
} }
protected saveGlobalsToMemory(globals: GlobalState): void { protected saveGlobalsToMemory(globals: GlobalState): void {
@ -2100,15 +2105,9 @@ export class StateService<TAccount extends Account = Account>
protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> { protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
if (options.useSecureStorage) { if (options.useSecureStorage) {
const state = await this.secureStorageService.save(keys.global, globals, options);
(await this.secureStorageService.get<State<TAccount>>("state", options)) ?? new State();
state.globals = globals;
await this.secureStorageService.save("state", state, options);
} else { } else {
const state = await this.storageService.save(keys.global, globals, options);
(await this.storageService.get<State<TAccount>>("state", options)) ?? new State();
state.globals = globals;
await this.saveStateToStorage(state, options);
} }
} }
@ -2147,15 +2146,15 @@ export class StateService<TAccount extends Account = Account>
return null; return null;
} }
const state = options?.useSecureStorage const account = options?.useSecureStorage
? (await this.secureStorageService.get<State<TAccount>>("state", options)) ?? ? (await this.secureStorageService.get<TAccount>(options.userId, options)) ??
(await this.storageService.get<State<TAccount>>( (await this.storageService.get<TAccount>(
"state", options.userId,
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }) this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })
)) ))
: await this.storageService.get<State<TAccount>>("state", options); : await this.storageService.get<TAccount>(options.userId, options);
return state?.accounts[options?.userId ?? this.state.activeUserId]; return account;
} }
protected useMemory(storageLocation: StorageLocation) { protected useMemory(storageLocation: StorageLocation) {
@ -2183,11 +2182,7 @@ export class StateService<TAccount extends Account = Account>
? this.secureStorageService ? this.secureStorageService
: this.storageService; : this.storageService;
const state = await storageLocation.save(`${options.userId}`, account, options);
(await storageLocation.get<State<TAccount>>("state", options)) ?? new State<TAccount>();
state.accounts[account.profile.userId] = account;
await storageLocation.save("state", state, options);
} }
protected async saveAccountToMemory(account: TAccount): Promise<void> { protected async saveAccountToMemory(account: TAccount): Promise<void> {
@ -2203,47 +2198,57 @@ export class StateService<TAccount extends Account = Account>
await this.scaffoldNewAccountMemoryStorage(account); await this.scaffoldNewAccountMemoryStorage(account);
} }
// 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> { protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
const storedState = const storedAccount = await this.storageService.get<TAccount>(
(await this.storageService.get<State<TAccount>>( account.profile.userId,
"state",
await this.defaultOnDiskLocalOptions() await this.defaultOnDiskLocalOptions()
)) ?? new State<TAccount>(); );
const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount?.settings != null) {
if (storedAccount != null) { // EnvironmentUrls are set before authenticating and should override whatever is stored from last session
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} }
storedState.accounts[account.profile.userId] = account; await this.storageService.save(
await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions()); account.profile.userId,
account,
await this.defaultOnDiskLocalOptions()
);
} }
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> { protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
const storedState = const storedAccount = await this.storageService.get<TAccount>(
(await this.storageService.get<State<TAccount>>( account.profile.userId,
"state",
await this.defaultOnDiskMemoryOptions() await this.defaultOnDiskMemoryOptions()
)) ?? new State<TAccount>(); );
const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount?.settings != null) {
if (storedAccount != null) { storedAccount.settings.environmentUrls = account.settings.environmentUrls;
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} }
storedState.accounts[account.profile.userId] = account; await this.storageService.save(
await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions()); account.profile.userId,
account,
await this.defaultOnDiskMemoryOptions()
);
} }
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> { protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
const storedState = const storedAccount = await this.storageService.get<TAccount>(
(await this.storageService.get<State<TAccount>>( account.profile.userId,
"state",
await this.defaultOnDiskOptions() await this.defaultOnDiskOptions()
)) ?? new State<TAccount>(); );
const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount?.settings != null) {
if (storedAccount != null) { storedAccount.settings.environmentUrls = account.settings.environmentUrls;
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} }
storedState.accounts[account.profile.userId] = account; await this.storageService.save(
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); account.profile.userId,
account,
await this.defaultOnDiskOptions()
);
} }
//
protected async pushAccounts(): Promise<void> { protected async pushAccounts(): Promise<void> {
await this.pruneInMemoryAccounts(); await this.pruneInMemoryAccounts();
@ -2313,34 +2318,33 @@ export class StateService<TAccount extends Account = Account>
} }
protected async getActiveUserIdFromStorage(): Promise<string> { protected async getActiveUserIdFromStorage(): Promise<string> {
const state = await this.storageService.get<State<TAccount>>("state"); return await this.storageService.get<string>(keys.activeUserId);
return state?.activeUserId;
} }
protected async removeAccountFromLocalStorage( protected async removeAccountFromLocalStorage(
userId: string = this.state.activeUserId userId: string = this.state.activeUserId
): Promise<void> { ): Promise<void> {
const state = await this.storageService.get<State<TAccount>>("state", { const storedAccount = await this.storageService.get<TAccount>(userId, {
htmlStorageLocation: HtmlStorageLocation.Local, htmlStorageLocation: HtmlStorageLocation.Local,
}); });
if (state?.accounts[userId] == null) { await this.storageService.save(
return; userId,
} this.resetAccount(storedAccount),
state.accounts[userId] = this.resetAccount(state.accounts[userId]); await this.defaultOnDiskLocalOptions()
await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions()); );
} }
protected async removeAccountFromSessionStorage( protected async removeAccountFromSessionStorage(
userId: string = this.state.activeUserId userId: string = this.state.activeUserId
): Promise<void> { ): Promise<void> {
const state = await this.storageService.get<State<TAccount>>("state", { const storedAccount = await this.storageService.get<TAccount>(userId, {
htmlStorageLocation: HtmlStorageLocation.Session, htmlStorageLocation: HtmlStorageLocation.Session,
}); });
if (state?.accounts[userId] == null) { await this.storageService.save(
return; userId,
} this.resetAccount(storedAccount),
state.accounts[userId] = this.resetAccount(state.accounts[userId]); await this.defaultOnDiskOptions()
await this.saveStateToStorage(state, await this.defaultOnDiskOptions()); );
} }
protected async removeAccountFromSecureStorage( protected async removeAccountFromSecureStorage(
@ -2355,13 +2359,6 @@ export class StateService<TAccount extends Account = Account>
delete this.state.accounts[userId]; delete this.state.accounts[userId];
} }
protected async saveStateToStorage(
state: State<TAccount>,
options: StorageOptions
): Promise<void> {
await this.storageService.save("state", state, options);
}
protected async pruneInMemoryAccounts() { protected async pruneInMemoryAccounts() {
// We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state // We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state
for (const userId in this.state.accounts) { for (const userId in this.state.accounts) {
@ -2371,13 +2368,10 @@ export class StateService<TAccount extends Account = Account>
} }
} }
// settings persist even on reset // settings persist even on reset, and are not effected by this method
protected resetAccount(account: TAccount) { protected resetAccount(account: TAccount) {
account.data = new AccountData(); const persistentAccountInformation = { settings: account.settings };
account.keys = new AccountKeys(); return Object.assign(this.createAccount(), persistentAccountInformation);
account.profile = new AccountProfile();
account.tokens = new AccountTokens();
return account;
} }
protected async setAccountEnvironmentUrls(account: TAccount): Promise<TAccount> { protected async setAccountEnvironmentUrls(account: TAccount): Promise<TAccount> {
@ -2389,4 +2383,44 @@ export class StateService<TAccount extends Account = Account>
options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls(); return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
} }
protected clearDecryptedDataForActiveUser() {
const userId = this.state.activeUserId;
if (userId == null) {
return;
}
this.state.accounts[userId].data = new AccountData();
}
protected createAccount(init: Partial<TAccount> = null): TAccount {
return this.accountFactory.create(init);
}
protected async deAuthenticateAccount(userId: string) {
await this.setAccessToken(null, { userId: userId });
const index = this.state.authenticatedAccounts.indexOf(userId);
if (index > -1) {
this.state.authenticatedAccounts.splice(index, 1);
await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts);
}
}
protected async removeAccountFromDisk(userId: string) {
await this.removeAccountFromSessionStorage(userId);
await this.removeAccountFromLocalStorage(userId);
await this.removeAccountFromSecureStorage(userId);
}
protected async dynamicallySetActiveUser() {
for (const userId in this.state.accounts) {
if (userId == null) {
continue;
}
if (await this.getIsAuthenticated({ userId: userId })) {
await this.setActiveUser(userId);
break;
}
await this.setActiveUser(null);
}
}
} }

View File

@ -113,6 +113,18 @@ const v1KeyPrefixes = {
settings: "settings_", settings: "settings_",
}; };
const keys = {
global: "global",
authenticatedAccounts: "authenticatedAccounts",
activeUserId: "activeUserId",
};
const partialKeys = {
autoKey: "_masterkey_auto",
biometricKey: "_masterkey_biometric",
masterKey: "_masterkey",
};
export class StateMigrationService { export class StateMigrationService {
constructor( constructor(
protected storageService: StorageService, protected storageService: StorageService,
@ -121,17 +133,16 @@ export class StateMigrationService {
async needsMigration(): Promise<boolean> { async needsMigration(): Promise<boolean> {
const currentStateVersion = ( const currentStateVersion = (
await this.storageService.get<State<Account>>("state", { await this.storageService.get<GlobalState>(keys.global, {
htmlStorageLocation: HtmlStorageLocation.Local, htmlStorageLocation: HtmlStorageLocation.Local,
}) })
)?.globals?.stateVersion; )?.stateVersion;
return currentStateVersion == null || currentStateVersion < StateVersion.Latest; return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
} }
async migrate(): Promise<void> { async migrate(): Promise<void> {
let currentStateVersion = let currentStateVersion =
(await this.storageService.get<State<Account>>("state"))?.globals?.stateVersion ?? (await this.storageService.get<GlobalState>(keys.global))?.stateVersion ?? StateVersion.One;
StateVersion.One;
while (currentStateVersion < StateVersion.Latest) { while (currentStateVersion < StateVersion.Latest) {
switch (currentStateVersion) { switch (currentStateVersion) {
case StateVersion.One: case StateVersion.One:
@ -152,8 +163,10 @@ export class StateMigrationService {
globals: new GlobalState(), globals: new GlobalState(),
accounts: {}, accounts: {},
activeUserId: null, activeUserId: null,
authenticatedAccounts: [],
} }
: { : {
authenticatedAccounts: [userId],
activeUserId: userId, activeUserId: userId,
globals: { globals: {
biometricAwaitingAcceptance: await this.storageService.get<boolean>( biometricAwaitingAcceptance: await this.storageService.get<boolean>(
@ -182,7 +195,6 @@ export class StateMigrationService {
v1Keys.installedVersion, v1Keys.installedVersion,
options options
), ),
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
locale: await this.storageService.get<string>(v1Keys.locale, options), locale: await this.storageService.get<string>(v1Keys.locale, options),
loginRedirect: null, loginRedirect: null,
mainWindowSize: null, mainWindowSize: null,
@ -490,12 +502,19 @@ export class StateMigrationService {
initialState.globals.environmentUrls = initialState.globals.environmentUrls =
(await this.storageService.get<EnvironmentUrls>(v1Keys.environmentUrls, options)) ?? (await this.storageService.get<EnvironmentUrls>(v1Keys.environmentUrls, options)) ??
new EnvironmentUrls(); new EnvironmentUrls();
await this.storageService.save(keys.global, initialState.globals, options);
await this.storageService.save("state", initialState, options); await this.storageService.save(keys.activeUserId, initialState.activeUserId, options);
if (initialState.activeUserId != null) {
await this.storageService.save(
initialState.activeUserId,
initialState.accounts[initialState.activeUserId]
);
}
await this.storageService.save(keys.authenticatedAccounts, initialState.authenticatedAccounts);
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) { if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) {
await this.secureStorageService.save( await this.secureStorageService.save(
`${userId}_masterkey_biometric`, `${userId}${partialKeys.biometricKey}`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }), await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }),
{ keySuffix: "biometric" } { keySuffix: "biometric" }
); );
@ -504,7 +523,7 @@ export class StateMigrationService {
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) { if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) {
await this.secureStorageService.save( await this.secureStorageService.save(
`${userId}_masterkey_auto`, `${userId}${partialKeys.autoKey}`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }), await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }),
{ keySuffix: "auto" } { keySuffix: "auto" }
); );
@ -513,7 +532,7 @@ export class StateMigrationService {
if (await this.secureStorageService.has(v1Keys.key)) { if (await this.secureStorageService.has(v1Keys.key)) {
await this.secureStorageService.save( await this.secureStorageService.save(
`${userId}_masterkey`, `${userId}${partialKeys.masterKey}`,
await this.secureStorageService.get(v1Keys.key) await this.secureStorageService.get(v1Keys.key)
); );
await this.secureStorageService.remove(v1Keys.key); await this.secureStorageService.remove(v1Keys.key);