1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-19 09:43:28 +00:00

[bug] Scaffold memory storage for web

Not properly creating storage objects on signin was creating weird behavior when logging out, locking, and logging back in.
Namely, encrypted data that was recently synced had nowhere to save to and was lost.
This commit is contained in:
addison
2021-11-18 16:40:21 -05:00
parent 94125ab420
commit 4b07927d9e
3 changed files with 168 additions and 128 deletions

View File

@@ -33,101 +33,101 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
}
export class AccountData {
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<CipherData, CipherView>();
folders?: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<FolderData, FolderView>();
localData?: any;
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
collections?: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<CollectionData, CollectionView>();
kdfIterations?: number;
kdfType?: KdfType;
policies?: DataEncryptionPair<PolicyData, Policy> = new DataEncryptionPair<PolicyData, Policy>();
passwordGenerationHistory?: EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
addEditCipherInfo?: any;
collapsedGroupings?: Set<string>;
eventCollection?: EventData[];
organizations?: { [id: string]: OrganizationData };
providers?: { [id: string]: ProviderData };
ciphers: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<CipherData, CipherView>();
folders: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<FolderData, FolderView>();
localData: any;
sends: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
collections: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<CollectionData, CollectionView>();
policies: DataEncryptionPair<PolicyData, Policy> = new DataEncryptionPair<PolicyData, Policy>();
passwordGenerationHistory: EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
addEditCipherInfo: any;
collapsedGroupings: Set<string>;
eventCollection: EventData[];
organizations: { [id: string]: OrganizationData };
providers: { [id: string]: ProviderData };
}
export class AccountKeys {
cryptoMasterKey?: SymmetricCryptoKey;
cryptoMasterKeyAuto?: SymmetricCryptoKey;
cryptoMasterKeyB64?: string;
cryptoMasterKeyBiometric?: SymmetricCryptoKey;
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<string, SymmetricCryptoKey>();
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
keyHash?: string;
legacyEtmKey?: SymmetricCryptoKey;
publicKey?: ArrayBuffer;
cryptoMasterKey: SymmetricCryptoKey;
cryptoMasterKeyAuto: SymmetricCryptoKey;
cryptoMasterKeyB64: string;
cryptoMasterKeyBiometric: SymmetricCryptoKey;
cryptoSymmetricKey: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<string, SymmetricCryptoKey>();
organizationKeys: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
providerKeys: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
privateKey: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
legacyEtmKey: SymmetricCryptoKey;
publicKey: ArrayBuffer;
apiKeyClientSecret: string;
}
export class AccountProfile {
apiKeyClientId?: string;
apiKeyClientSecret?: string;
authenticationStatus?: AuthenticationStatus;
convertAccountToKeyConnector?: boolean;
email?: string;
emailVerified?: boolean;
entityId?: string;
entityType?: string;
everBeenUnlocked?: boolean;
forcePasswordReset?: boolean;
hasPremiumPersonally?: boolean;
lastActive?: number;
lastSync?: string;
ssoCodeVerifier?: string;
ssoOrganizationIdentifier?: string;
ssoState?: string;
userId?: string;
usesKeyConnector?: boolean;
apiKeyClientId: string;
authenticationStatus: AuthenticationStatus;
convertAccountToKeyConnector: boolean;
email: string;
emailVerified: boolean;
entityId: string;
entityType: string;
everBeenUnlocked: boolean;
forcePasswordReset: boolean;
hasPremiumPersonally: boolean;
lastActive: number;
lastSync: string;
ssoCodeVerifier: string;
ssoOrganizationIdentifier: string;
ssoState: string;
userId: string;
usesKeyConnector: boolean;
keyHash: string;
kdfIterations: number;
kdfType: KdfType;
}
export class AccountSettings {
alwaysShowDock?: boolean;
autoConfirmFingerPrints?: boolean;
autoFillOnPageLoadDefault?: boolean;
biometricLocked?: boolean;
biometricText?: string;
biometricUnlock?: boolean;
clearClipboard?: number;
defaultUriMatch?: UriMatchType;
disableAddLoginNotification?: boolean;
disableAutoBiometricsPrompt?: boolean;
disableAutoTotpCopy?: boolean;
disableBadgeCounter?: boolean;
disableChangedPasswordNotification?: boolean;
disableContextMenuItem?: boolean;
disableGa?: boolean;
dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean;
enableAutoFillOnPageLoad?: boolean;
enableBiometric?: boolean;
enableBiometrics?: boolean;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableCloseToTray?: boolean;
enableFullWidth?: boolean;
enableGravitars?: boolean;
enableMinimizeToTray?: boolean;
enableStartToTray?: boolean;
enableTray?: boolean;
environmentUrls?: any;
alwaysShowDock: boolean;
autoConfirmFingerPrints: boolean;
autoFillOnPageLoadDefault: boolean;
biometricLocked: boolean;
biometricText: string;
biometricUnlock: boolean;
clearClipboard: number;
defaultUriMatch: UriMatchType;
disableAddLoginNotification: boolean;
disableAutoBiometricsPrompt: boolean;
disableAutoTotpCopy: boolean;
disableBadgeCounter: boolean;
disableChangedPasswordNotification: boolean;
disableContextMenuItem: boolean;
disableGa: boolean;
dontShowCardsCurrentTab: boolean;
dontShowIdentitiesCurrentTab: boolean;
enableAlwaysOnTop: boolean;
enableAutoFillOnPageLoad: boolean;
enableBiometric: boolean;
enableBiometrics: boolean;
enableBrowserIntegration: boolean;
enableBrowserIntegrationFingerprint: boolean;
enableCloseToTray: boolean;
enableFullWidth: boolean;
enableGravitars: boolean;
enableMinimizeToTray: boolean;
enableStartToTray: boolean;
enableTray: boolean;
environmentUrls: any;
equivalentDomains?: any;
locale?: string;
minimizeOnCopyToClipboard?: boolean;
neverDomains?: { [id: string]: any };
noAutoPromptBiometrics?: boolean;
noAutoPromptBiometricsText?: string;
openAtLogin?: boolean;
passwordGenerationOptions?: any;
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
protectedPin?: string;
settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
vaultTimeoutAction?: string;
locale: string;
minimizeOnCopyToClipboard: boolean;
neverDomains: { [id: string]: any };
noAutoPromptBiometrics: boolean;
noAutoPromptBiometricsText: string;
openAtLogin: boolean;
passwordGenerationOptions: any;
pinProtected: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
protectedPin: string;
settings: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout: number;
vaultTimeoutAction: string;
get serverUrl(): string {
return this.environmentUrls?.base ?? 'bitwarden.com';
@@ -152,23 +152,23 @@ export class Account {
Object.assign(this, {
data: {
...new AccountData(),
...init.data,
...init?.data,
},
keys: {
...new AccountKeys(),
...init.keys,
...init?.keys,
},
profile: {
...new AccountProfile(),
...init.profile,
...init?.profile,
},
settings: {
...new AccountSettings(),
...init.settings,
...init?.settings,
},
tokens: {
...new AccountTokens(),
...init.tokens,
...init?.tokens,
},
});
}

View File

@@ -2,7 +2,7 @@ import { HashPurpose } from '../enums/hashPurpose';
import { KdfType } from '../enums/kdfType';
import { TwoFactorProviderType } from '../enums/twoFactorProviderType';
import { Account } from '../models/domain/account';
import { Account, AccountData, AccountProfile, AccountTokens } from '../models/domain/account';
import { AuthResult } from '../models/domain/authResult';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
@@ -355,20 +355,24 @@ export class AuthService implements AuthServiceAbstraction {
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
await this.stateService.addAccount({
profile: {
...new AccountProfile(),
...{
userId: accountInformation.sub,
email: accountInformation.email,
apiKeyClientId: clientId,
apiKeyClientSecret: clientSecret,
hasPremiumPersonally: accountInformation.premium,
},
data: {
kdfIterations: tokenResponse.kdfIterations,
kdfType: tokenResponse.kdf,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
});
if (tokenResponse.twoFactorToken != null) {

View File

@@ -120,12 +120,12 @@ export class StateService implements StateServiceAbstraction {
}
async getApiKeyClientSecret(options?: StorageOptions): Promise<string> {
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.apiKeyClientSecret;
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.apiKeyClientSecret;
}
async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
account.profile.apiKeyClientId = value;
account.keys.apiKeyClientSecret = value;
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
}
@@ -619,7 +619,7 @@ export class StateService implements StateServiceAbstraction {
}
async getEnableGravitars(options?: StorageOptions): Promise<boolean> {
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableGravitars ?? true;
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableGravitars ?? false;
}
async setEnableGravitars(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()));
@@ -831,30 +831,30 @@ export class StateService implements StateServiceAbstraction {
}
async getKdfIterations(options?: StorageOptions): Promise<number> {
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.kdfIterations;
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfIterations;
}
async setKdfIterations(value: number, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
account.data.kdfIterations = value;
account.profile.kdfIterations = value;
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
}
async getKdfType(options?: StorageOptions): Promise<KdfType> {
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.kdfType;
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfType;
}
async setKdfType(value: KdfType, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
account.data.kdfType = value;
account.profile.kdfType = value;
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
}
async getKeyHash(options?: StorageOptions): Promise<string> {
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.keyHash;
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.keyHash;
}
async setKeyHash(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
account.keys.keyHash = value;
account.profile.keyHash = value;
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
}
@@ -1284,7 +1284,14 @@ export class StateService implements StateServiceAbstraction {
}
private async scaffoldNewAccountStorage(account: Account): Promise<void> {
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskOptions()) ?? new State();
await this.scaffoldNewAccountLocalStorage(account);
await this.scaffoldNewAccountSessionStorage(account);
await this.scaffoldNewAccountMemoryStorage(account);
await this.scaffoldNewAccountSecureStorage(account);
}
private async scaffoldNewAccountLocalStorage(account: Account): Promise<void> {
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskLocalOptions()) ?? new State();
const storedAccount = storedState.accounts[account.profile.userId];
if (storedAccount != null) {
storedAccount.tokens.accessToken = account.tokens.accessToken;
@@ -1293,10 +1300,30 @@ export class StateService implements StateServiceAbstraction {
}
storedState.accounts[account.profile.userId] = account;
await this.storageService.save('state', storedState, await this.defaultOnDiskLocalOptions());
await this.storageService.save('state', storedState, await this.defaultOnDiskMemoryOptions());
await this.storageService.save('state', storedState, await this.defaultOnDiskOptions());
}
await this.scaffoldNewAccountSecureStorage(account);
private async scaffoldNewAccountMemoryStorage(account: Account): Promise<void> {
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskMemoryOptions()) ?? new State();
const storedAccount = storedState.accounts[account.profile.userId];
if (storedAccount != null) {
storedAccount.tokens.accessToken = account.tokens.accessToken;
storedAccount.tokens.refreshToken = account.tokens.refreshToken;
account = storedAccount;
}
storedState.accounts[account.profile.userId] = account;
await this.storageService.save('state', storedState, await this.defaultOnDiskMemoryOptions());
}
private async scaffoldNewAccountSessionStorage(account: Account): Promise<void> {
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskOptions()) ?? new State();
const storedAccount = storedState.accounts[account.profile.userId];
if (storedAccount != null) {
storedAccount.tokens.accessToken = account.tokens.accessToken;
storedAccount.tokens.refreshToken = account.tokens.refreshToken;
account = storedAccount;
}
storedState.accounts[account.profile.userId] = account;
await this.storageService.save('state', storedState, await this.defaultOnDiskOptions());
}
private async scaffoldNewAccountSecureStorage(account: Account): Promise<void> {
@@ -1368,7 +1395,7 @@ export class StateService implements StateServiceAbstraction {
return {
storageLocation: StorageLocation.Disk,
htmlStorageLocation: HtmlStorageLocation.Memory,
userId: await this.storageService.get('activeUserId'),
userId: await this.getUserId(),
useSecureStorage: false,
};
}
@@ -1386,38 +1413,47 @@ export class StateService implements StateServiceAbstraction {
return state?.activeUserId;
}
private async removeAccountFromLocalStorage(userId: string): Promise<void> {
private async removeAccountFromLocalStorage(userId: string = this.state.activeUserId): Promise<void> {
const state = await this.secureStorageService.get<State>('state', { htmlStorageLocation: HtmlStorageLocation.Local });
if (state?.accounts[userId ?? this.state.activeUserId] == null) {
if (state?.accounts[userId] == null) {
return;
}
delete state.accounts[userId ?? this.state.activeUserId];
state.accounts[userId] = new Account({
profile: state.accounts[userId].profile,
settings: state.accounts[userId].settings,
data: state.accounts[userId].data,
});
await this.storageService.save('state', state, { htmlStorageLocation: HtmlStorageLocation.Local });
}
private async removeAccountFromSessionStorage(userId: string): Promise<void> {
private async removeAccountFromSessionStorage(userId: string = this.state.activeUserId): Promise<void> {
const state = await this.secureStorageService.get<State>('state', { htmlStorageLocation: HtmlStorageLocation.Session });
const account = state?.accounts[userId ?? this.state.activeUserId];
if (account == null) {
if (state?.accounts[userId] == null) {
return;
}
delete state.accounts[userId ?? this.state.activeUserId];
state.accounts[userId] = new Account({
profile: state.accounts[userId].profile,
settings: state.accounts[userId].settings,
data: state.accounts[userId].data,
});
await this.storageService.save('state', state, { htmlStorageLocation: HtmlStorageLocation.Session });
}
private async removeAccountFromSecureStorage(userId: string): Promise<void> {
private async removeAccountFromSecureStorage(userId: string = this.state.activeUserId): Promise<void> {
const state = await this.secureStorageService.get<State>('state');
if (state?.accounts[userId ?? this.state.activeUserId] == null) {
if (state?.accounts[userId] == null) {
return;
}
delete state.accounts[userId ?? this.state.activeUserId];
state.accounts[userId] = null;
await this.secureStorageService.save('state', state);
}
private removeAccountFromMemory(userId: string): void {
if (this.state?.accounts[userId ?? this.state.activeUserId] == null) {
private removeAccountFromMemory(userId: string = this.state.activeUserId): void {
if (this.state?.accounts[userId] == null) {
return;
}
delete this.state.accounts[userId ?? this.state.activeUserId];