import { StorageService } from "../abstractions/storage.service"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; import { GlobalState } from "../models/domain/globalState"; import { StorageOptions } from "../models/domain/storageOptions"; import { CipherData } from "../models/data/cipherData"; import { CollectionData } from "../models/data/collectionData"; import { EventData } from "../models/data/eventData"; import { FolderData } from "../models/data/folderData"; import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; import { KdfType } from "../enums/kdfType"; import { StateVersion } from "../enums/stateVersion"; import { ThemeType } from "../enums/themeType"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GlobalStateFactory } from "../factories/globalStateFactory"; import { StateFactory } from "../factories/stateFactory"; import { Account, AccountSettings } from "../models/domain/account"; // Originally (before January 2022) storage was handled as a flat key/value pair store. // With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration. const v1Keys: { [key: string]: string } = { accessToken: "accessToken", alwaysShowDock: "alwaysShowDock", autoConfirmFingerprints: "autoConfirmFingerprints", autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault", biometricAwaitingAcceptance: "biometricAwaitingAcceptance", biometricFingerprintValidated: "biometricFingerprintValidated", biometricText: "biometricText", biometricUnlock: "biometric", clearClipboard: "clearClipboardKey", clientId: "apikey_clientId", clientSecret: "apikey_clientSecret", collapsedGroupings: "collapsedGroupings", convertAccountToKeyConnector: "convertAccountToKeyConnector", defaultUriMatch: "defaultUriMatch", disableAddLoginNotification: "disableAddLoginNotification", disableAutoBiometricsPrompt: "noAutoPromptBiometrics", disableAutoTotpCopy: "disableAutoTotpCopy", disableBadgeCounter: "disableBadgeCounter", disableChangedPasswordNotification: "disableChangedPasswordNotification", disableContextMenuItem: "disableContextMenuItem", disableFavicon: "disableFavicon", disableGa: "disableGa", dontShowCardsCurrentTab: "dontShowCardsCurrentTab", dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab", emailVerified: "emailVerified", enableAlwaysOnTop: "enableAlwaysOnTopKey", enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad", enableBiometric: "enabledBiometric", enableBrowserIntegration: "enableBrowserIntegration", enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint", enableCloseToTray: "enableCloseToTray", enableFullWidth: "enableFullWidth", enableGravatars: "enableGravatars", enableMinimizeToTray: "enableMinimizeToTray", enableStartToTray: "enableStartToTrayKey", enableTray: "enableTray", encKey: "encKey", // Generated Symmetric Key encOrgKeys: "encOrgKeys", encPrivate: "encPrivateKey", encProviderKeys: "encProviderKeys", entityId: "entityId", entityType: "entityType", environmentUrls: "environmentUrls", equivalentDomains: "equivalentDomains", eventCollection: "eventCollection", forcePasswordReset: "forcePasswordReset", history: "generatedPasswordHistory", installedVersion: "installedVersion", kdf: "kdf", kdfIterations: "kdfIterations", key: "key", // Master Key keyHash: "keyHash", lastActive: "lastActive", localData: "sitesLocalData", locale: "locale", mainWindowSize: "mainWindowSize", minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey", neverDomains: "neverDomains", noAutoPromptBiometricsText: "noAutoPromptBiometricsText", openAtLogin: "openAtLogin", passwordGenerationOptions: "passwordGenerationOptions", pinProtected: "pinProtectedKey", protectedPin: "protectedPin", refreshToken: "refreshToken", ssoCodeVerifier: "ssoCodeVerifier", ssoIdentifier: "ssoOrgIdentifier", ssoState: "ssoState", stamp: "securityStamp", theme: "theme", userEmail: "userEmail", userId: "userId", usesConnector: "usesKeyConnector", vaultTimeoutAction: "vaultTimeoutAction", vaultTimeout: "lockOption", rememberedEmail: "rememberedEmail", }; const v1KeyPrefixes: { [key: string]: string } = { ciphers: "ciphers_", collections: "collections_", folders: "folders_", lastSync: "lastSync_", policies: "policies_", twoFactorToken: "twoFactorToken_", organizations: "organizations_", providers: "providers_", sends: "sends_", settings: "settings_", }; const keys = { global: "global", authenticatedAccounts: "authenticatedAccounts", activeUserId: "activeUserId", tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication accountActivity: "accountActivity", }; const partialKeys = { autoKey: "_masterkey_auto", biometricKey: "_masterkey_biometric", masterKey: "_masterkey", }; export class StateMigrationService< TGlobalState extends GlobalState = GlobalState, TAccount extends Account = Account > { constructor( protected storageService: StorageService, protected secureStorageService: StorageService, protected stateFactory: StateFactory ) {} async needsMigration(): Promise { const currentStateVersion = await this.getCurrentStateVersion(); return currentStateVersion == null || currentStateVersion < StateVersion.Latest; } async migrate(): Promise { let currentStateVersion = await this.getCurrentStateVersion(); while (currentStateVersion < StateVersion.Latest) { switch (currentStateVersion) { case StateVersion.One: await this.migrateStateFrom1To2(); break; } currentStateVersion += 1; } } protected async migrateStateFrom1To2(): Promise { const clearV1Keys = async (clearingUserId?: string) => { for (const key in v1Keys) { if (key == null) { continue; } await this.set(v1Keys[key], null); } if (clearingUserId != null) { for (const keyPrefix in v1KeyPrefixes) { if (keyPrefix == null) { continue; } await this.set(v1KeyPrefixes[keyPrefix] + userId, null); } } }; // Some processes, like biometrics, may have already defined a value before migrations are run. // We don't want to null out those values if they don't exist in the old storage scheme (like for new installs) // So, the OOO for migration is that we: // 1. Check for an existing storage value from the old storage structure OR // 2. Check for a value already set by processes that run before migration OR // 3. Assign the default value const globals = (await this.get(keys.global)) ?? this.stateFactory.createGlobal(null); globals.stateVersion = StateVersion.Two; globals.environmentUrls = (await this.get(v1Keys.environmentUrls)) ?? globals.environmentUrls; globals.locale = (await this.get(v1Keys.locale)) ?? globals.locale; globals.noAutoPromptBiometrics = (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? globals.noAutoPromptBiometrics; globals.noAutoPromptBiometricsText = (await this.get(v1Keys.noAutoPromptBiometricsText)) ?? globals.noAutoPromptBiometricsText; globals.ssoCodeVerifier = (await this.get(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier; globals.ssoOrganizationIdentifier = (await this.get(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier; globals.ssoState = (await this.get(v1Keys.ssoState)) ?? globals.ssoState; globals.rememberedEmail = (await this.get(v1Keys.rememberedEmail)) ?? globals.rememberedEmail; globals.theme = (await this.get(v1Keys.theme)) ?? globals.theme; globals.vaultTimeout = (await this.get(v1Keys.vaultTimeout)) ?? globals.vaultTimeout; globals.vaultTimeoutAction = (await this.get(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction; globals.window = (await this.get(v1Keys.mainWindowSize)) ?? globals.window; globals.enableTray = (await this.get(v1Keys.enableTray)) ?? globals.enableTray; globals.enableMinimizeToTray = (await this.get(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray; globals.enableCloseToTray = (await this.get(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray; globals.enableStartToTray = (await this.get(v1Keys.enableStartToTray)) ?? globals.enableStartToTray; globals.openAtLogin = (await this.get(v1Keys.openAtLogin)) ?? globals.openAtLogin; globals.alwaysShowDock = (await this.get(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock; globals.enableBrowserIntegration = (await this.get(v1Keys.enableBrowserIntegration)) ?? globals.enableBrowserIntegration; globals.enableBrowserIntegrationFingerprint = (await this.get(v1Keys.enableBrowserIntegrationFingerprint)) ?? globals.enableBrowserIntegrationFingerprint; const userId = (await this.get(v1Keys.userId)) ?? (await this.get(v1Keys.entityId)); const defaultAccount = this.stateFactory.createAccount(null); const accountSettings: AccountSettings = { autoConfirmFingerPrints: (await this.get(v1Keys.autoConfirmFingerprints)) ?? defaultAccount.settings.autoConfirmFingerPrints, autoFillOnPageLoadDefault: (await this.get(v1Keys.autoFillOnPageLoadDefault)) ?? defaultAccount.settings.autoFillOnPageLoadDefault, biometricLocked: null, biometricUnlock: (await this.get(v1Keys.biometricUnlock)) ?? defaultAccount.settings.biometricUnlock, clearClipboard: (await this.get(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard, defaultUriMatch: (await this.get(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch, disableAddLoginNotification: (await this.get(v1Keys.disableAddLoginNotification)) ?? defaultAccount.settings.disableAddLoginNotification, disableAutoBiometricsPrompt: (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? defaultAccount.settings.disableAutoBiometricsPrompt, disableAutoTotpCopy: (await this.get(v1Keys.disableAutoTotpCopy)) ?? defaultAccount.settings.disableAutoTotpCopy, disableBadgeCounter: (await this.get(v1Keys.disableBadgeCounter)) ?? defaultAccount.settings.disableBadgeCounter, disableChangedPasswordNotification: (await this.get(v1Keys.disableChangedPasswordNotification)) ?? defaultAccount.settings.disableChangedPasswordNotification, disableContextMenuItem: (await this.get(v1Keys.disableContextMenuItem)) ?? defaultAccount.settings.disableContextMenuItem, disableGa: (await this.get(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa, dontShowCardsCurrentTab: (await this.get(v1Keys.dontShowCardsCurrentTab)) ?? defaultAccount.settings.dontShowCardsCurrentTab, dontShowIdentitiesCurrentTab: (await this.get(v1Keys.dontShowIdentitiesCurrentTab)) ?? defaultAccount.settings.dontShowIdentitiesCurrentTab, enableAlwaysOnTop: (await this.get(v1Keys.enableAlwaysOnTop)) ?? defaultAccount.settings.enableAlwaysOnTop, enableAutoFillOnPageLoad: (await this.get(v1Keys.enableAutoFillOnPageLoad)) ?? defaultAccount.settings.enableAutoFillOnPageLoad, enableBiometric: (await this.get(v1Keys.enableBiometric)) ?? defaultAccount.settings.enableBiometric, enableFullWidth: (await this.get(v1Keys.enableFullWidth)) ?? defaultAccount.settings.enableFullWidth, enableGravitars: (await this.get(v1Keys.enableGravatars)) ?? defaultAccount.settings.enableGravitars, environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls, equivalentDomains: (await this.get(v1Keys.equivalentDomains)) ?? defaultAccount.settings.equivalentDomains, minimizeOnCopyToClipboard: (await this.get(v1Keys.minimizeOnCopyToClipboard)) ?? defaultAccount.settings.minimizeOnCopyToClipboard, neverDomains: (await this.get(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains, passwordGenerationOptions: (await this.get(v1Keys.passwordGenerationOptions)) ?? defaultAccount.settings.passwordGenerationOptions, pinProtected: { decrypted: null, encrypted: await this.get(v1Keys.pinProtected), }, protectedPin: await this.get(v1Keys.protectedPin), settings: userId == null ? null : await this.get(v1KeyPrefixes.settings + userId), vaultTimeout: (await this.get(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout, vaultTimeoutAction: (await this.get(v1Keys.vaultTimeoutAction)) ?? defaultAccount.settings.vaultTimeoutAction, }; // (userId == null) = no logged in user (so no known userId) and we need to temporarily store account specific settings in state to migrate on first auth // (userId != null) = we have a currently authed user (so known userId) with encrypted data and other key settings we can move, no need to temporarily store account settings if (userId == null) { await this.set(keys.tempAccountSettings, accountSettings); await this.set(keys.global, globals); await this.set(keys.authenticatedAccounts, []); await this.set(keys.activeUserId, null); await clearV1Keys(); return; } globals.twoFactorToken = await this.get(v1KeyPrefixes.twoFactorToken + userId); await this.set(keys.global, globals); await this.set(userId, { data: { addEditCipherInfo: null, ciphers: { decrypted: null, encrypted: await this.get<{ [id: string]: CipherData }>(v1KeyPrefixes.ciphers + userId), }, collapsedGroupings: null, collections: { decrypted: null, encrypted: await this.get<{ [id: string]: CollectionData }>( v1KeyPrefixes.collections + userId ), }, eventCollection: await this.get(v1Keys.eventCollection), folders: { decrypted: null, encrypted: await this.get<{ [id: string]: FolderData }>(v1KeyPrefixes.folders + userId), }, localData: null, organizations: await this.get<{ [id: string]: OrganizationData }>( v1KeyPrefixes.organizations + userId ), passwordGenerationHistory: { decrypted: null, encrypted: await this.get(v1Keys.history), }, policies: { decrypted: null, encrypted: await this.get<{ [id: string]: PolicyData }>(v1KeyPrefixes.policies + userId), }, providers: await this.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId), sends: { decrypted: null, encrypted: await this.get<{ [id: string]: SendData }>(v1KeyPrefixes.sends + userId), }, }, keys: { apiKeyClientSecret: await this.get(v1Keys.clientSecret), cryptoMasterKey: null, cryptoMasterKeyAuto: null, cryptoMasterKeyB64: null, cryptoMasterKeyBiometric: null, cryptoSymmetricKey: { encrypted: await this.get(v1Keys.encKey), decrypted: null, }, legacyEtmKey: null, organizationKeys: { decrypted: null, encrypted: await this.get(v1Keys.encOrgKeys + userId), }, privateKey: { decrypted: null, encrypted: await this.get(v1Keys.encPrivate), }, providerKeys: { decrypted: null, encrypted: await this.get(v1Keys.encProviderKeys + userId), }, publicKey: null, }, profile: { apiKeyClientId: await this.get(v1Keys.clientId), authenticationStatus: null, convertAccountToKeyConnector: await this.get(v1Keys.convertAccountToKeyConnector), email: await this.get(v1Keys.userEmail), emailVerified: await this.get(v1Keys.emailVerified), entityId: null, entityType: null, everBeenUnlocked: null, forcePasswordReset: null, hasPremiumPersonally: null, kdfIterations: await this.get(v1Keys.kdfIterations), kdfType: await this.get(v1Keys.kdf), keyHash: await this.get(v1Keys.keyHash), lastSync: null, userId: userId, usesKeyConnector: null, }, settings: accountSettings, tokens: { accessToken: await this.get(v1Keys.accessToken), decodedToken: null, refreshToken: await this.get(v1Keys.refreshToken), securityStamp: null, }, }); await this.set(keys.authenticatedAccounts, [userId]); await this.set(keys.activeUserId, userId); const accountActivity: { [userId: string]: number } = { [userId]: await this.get(v1Keys.lastActive), }; accountActivity[userId] = await this.get(v1Keys.lastActive); await this.set(keys.accountActivity, accountActivity); await clearV1Keys(userId); if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) { await this.secureStorageService.save( `${userId}${partialKeys.biometricKey}`, await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }), { keySuffix: "biometric" } ); await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" }); } if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) { await this.secureStorageService.save( `${userId}${partialKeys.autoKey}`, await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }), { keySuffix: "auto" } ); await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" }); } if (await this.secureStorageService.has(v1Keys.key)) { await this.secureStorageService.save( `${userId}${partialKeys.masterKey}`, await this.secureStorageService.get(v1Keys.key) ); await this.secureStorageService.remove(v1Keys.key); } } protected get options(): StorageOptions { return { htmlStorageLocation: HtmlStorageLocation.Local }; } protected get(key: string): Promise { return this.storageService.get(key, this.options); } protected set(key: string, value: any): Promise { if (value == null) { return this.storageService.remove(key, this.options); } return this.storageService.save(key, value, this.options); } protected async getGlobals(): Promise { return await this.get(keys.global); } protected async getCurrentStateVersion(): Promise { return (await this.getGlobals())?.stateVersion ?? StateVersion.One; } }