diff --git a/common/src/abstractions/state.service.ts b/common/src/abstractions/state.service.ts index ebf94df3..a90088a2 100644 --- a/common/src/abstractions/state.service.ts +++ b/common/src/abstractions/state.service.ts @@ -3,6 +3,7 @@ import { BehaviorSubject } from "rxjs"; import { KdfType } from "../enums/kdfType"; import { ThemeType } from "../enums/themeType"; import { UriMatchType } from "../enums/uriMatchType"; +import { EncryptedOrganizationKeyStore } from "../interfaces/encryptedOrganizationKeyStore"; import { CipherData } from "../models/data/cipherData"; import { CollectionData } from "../models/data/collectionData"; import { EventData } from "../models/data/eventData"; @@ -23,7 +24,6 @@ import { CipherView } from "../models/view/cipherView"; import { CollectionView } from "../models/view/collectionView"; import { FolderView } from "../models/view/folderView"; import { SendView } from "../models/view/sendView"; - export abstract class StateService { accounts: BehaviorSubject<{ [userId: string]: T }>; activeAccount: BehaviorSubject; @@ -184,9 +184,11 @@ export abstract class StateService { value: { [id: string]: FolderData }, options?: StorageOptions ) => Promise; - getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise; + getEncryptedOrganizationKeys: ( + options?: StorageOptions + ) => Promise<{ [orgId: string]: EncryptedOrganizationKeyStore }>; setEncryptedOrganizationKeys: ( - value: Map, + value: { [orgId: string]: EncryptedOrganizationKeyStore }, options?: StorageOptions ) => Promise; getEncryptedPasswordGenerationHistory: ( diff --git a/common/src/enums/stateVersion.ts b/common/src/enums/stateVersion.ts index 5aeb02e5..285d973e 100644 --- a/common/src/enums/stateVersion.ts +++ b/common/src/enums/stateVersion.ts @@ -3,5 +3,6 @@ export enum StateVersion { Two = 2, // Move to a typed State object Three = 3, // Fix migration of users' premium status Four = 4, // Fix 'Never Lock' option by removing stale data - Latest = Four, + Five = 5, + Latest = Five, } diff --git a/common/src/interfaces/encryptedOrganizationKeyStore.ts b/common/src/interfaces/encryptedOrganizationKeyStore.ts new file mode 100644 index 00000000..c1cab66a --- /dev/null +++ b/common/src/interfaces/encryptedOrganizationKeyStore.ts @@ -0,0 +1,4 @@ +export interface EncryptedOrganizationKeyStore { + key: string; + providerId?: string; +} diff --git a/common/src/models/domain/account.ts b/common/src/models/domain/account.ts index e03f9be3..23381148 100644 --- a/common/src/models/domain/account.ts +++ b/common/src/models/domain/account.ts @@ -1,6 +1,7 @@ import { AuthenticationStatus } from "../../enums/authenticationStatus"; import { KdfType } from "../../enums/kdfType"; import { UriMatchType } from "../../enums/uriMatchType"; +import { EncryptedOrganizationKeyStore } from "../../interfaces/encryptedOrganizationKeyStore"; import { CipherData } from "../data/cipherData"; import { CollectionData } from "../data/collectionData"; import { EventData } from "../data/eventData"; @@ -20,6 +21,7 @@ import { GeneratedPasswordHistory } from "./generatedPasswordHistory"; import { Policy } from "./policy"; import { SymmetricCryptoKey } from "./symmetricCryptoKey"; + export class EncryptionPair { encrypted?: TEncrypted; decrypted?: TDecrypted; @@ -65,8 +67,11 @@ export class AccountKeys { string, SymmetricCryptoKey >(); - organizationKeys?: EncryptionPair> = new EncryptionPair< - any, + organizationKeys?: EncryptionPair< + { [orgId: string]: EncryptedOrganizationKeyStore }, + Map + > = new EncryptionPair< + { [orgId: string]: EncryptedOrganizationKeyStore }, Map >(); providerKeys?: EncryptionPair> = new EncryptionPair< diff --git a/common/src/services/crypto.service.ts b/common/src/services/crypto.service.ts index c49d7763..f0054375 100644 --- a/common/src/services/crypto.service.ts +++ b/common/src/services/crypto.service.ts @@ -1,5 +1,7 @@ import * as bigInt from "big-integer"; +import { EncryptedOrganizationKeyStore } from 'jslib-common/interfaces/encryptedOrganizationKeyStore'; + import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; import { LogService } from "../abstractions/log.service"; @@ -59,20 +61,22 @@ export class CryptoService implements CryptoServiceAbstraction { orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[] ): Promise { - const orgKeys: any = {}; + const orgKeyStore: { [orgId: string]: EncryptedOrganizationKeyStore } = {}; orgs.forEach((org) => { - orgKeys[org.id] = org.key; + orgKeyStore[org.id] = { key: org.key }; }); for (const providerOrg of providerOrgs) { - // Convert provider encrypted keys to user encrypted. - const providerKey = await this.getProviderKey(providerOrg.providerId); - const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey); - orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString; + // Store providerId with the key later so it can be decrypted using the provider's key + // We can't decrypt it yet because we may not have all the required encryption keys available + orgKeyStore[providerOrg.id] = { + key: providerOrg.key, + providerId: providerOrg.providerId, + } } await this.stateService.setDecryptedOrganizationKeys(null); - return await this.stateService.setEncryptedOrganizationKeys(orgKeys); + return await this.stateService.setEncryptedOrganizationKeys(orgKeyStore); } async setProviderKeys(providers: ProfileProviderResponse[]): Promise { @@ -215,20 +219,35 @@ export class CryptoService implements CryptoServiceAbstraction { return decryptedOrganizationKeys; } - const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys(); - if (encOrgKeys == null) { + const encOrgKeyStore = await this.stateService.getEncryptedOrganizationKeys(); + if (encOrgKeyStore == null) { return null; } let setKey = false; - for (const orgId in encOrgKeys) { + for (const orgId in encOrgKeyStore) { // eslint-disable-next-line - if (!encOrgKeys.hasOwnProperty(orgId)) { + if (!encOrgKeyStore.hasOwnProperty(orgId)) { continue; } - const decValue = await this.rsaDecrypt(encOrgKeys[orgId]); + if (orgKeys.has(orgId)) { + continue; + } + + const encOrgKey = encOrgKeyStore[orgId].key; + const providerId = encOrgKeyStore[orgId].providerId; + + let decValue = null; + if (providerId == null) { + // User encrypted + decValue = await this.rsaDecrypt(encOrgKey); + } else { + // Provider encrypted + const providerKey = await this.getProviderKey(providerId); + decValue = await this.decryptToBytes(new EncString(encOrgKey), providerKey); + } orgKeys.set(orgId, new SymmetricCryptoKey(decValue)); setKey = true; } diff --git a/common/src/services/state.service.ts b/common/src/services/state.service.ts index 9d035fc7..0a7e614e 100644 --- a/common/src/services/state.service.ts +++ b/common/src/services/state.service.ts @@ -1,5 +1,7 @@ import { BehaviorSubject } from "rxjs"; +import { EncryptedOrganizationKeyStore } from "jslib-common/interfaces/encryptedOrganizationKeyStore"; + import { LogService } from "../abstractions/log.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateMigrationService } from "../abstractions/stateMigration.service"; @@ -1213,14 +1215,16 @@ export class StateService< ); } - async getEncryptedOrganizationKeys(options?: StorageOptions): Promise { + async getEncryptedOrganizationKeys( + options?: StorageOptions + ): Promise<{ [orgId: string]: EncryptedOrganizationKeyStore }> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) )?.keys?.organizationKeys.encrypted; } async setEncryptedOrganizationKeys( - value: Map, + value: { [orgId: string]: EncryptedOrganizationKeyStore }, options?: StorageOptions ): Promise { const account = await this.getAccount( diff --git a/common/src/services/stateMigration.service.ts b/common/src/services/stateMigration.service.ts index 2b299898..ec834c6c 100644 --- a/common/src/services/stateMigration.service.ts +++ b/common/src/services/stateMigration.service.ts @@ -155,6 +155,9 @@ export class StateMigrationService< case StateVersion.Three: await this.migrateStateFrom3To4(); break; + case StateVersion.Four: + await this.migrateStateFrom4To5(); + break; } currentStateVersion += 1; @@ -488,6 +491,19 @@ export class StateMigrationService< await this.set(keys.global, globals); } + protected async migrateStateFrom4To5(): Promise { + const authenticatedUserIds = await this.get(keys.authenticatedAccounts); + for (const userId in authenticatedUserIds) { + const account = await this.get(userId); + const encryptedOrgKeys = account.keys.organizationKeys?.encrypted; + if (encryptedOrgKeys != null) { + // TODO, iterate through KVPs and update value + } + } + + await this.setCurrentStateVersion(StateVersion.Five); + } + protected get options(): StorageOptions { return { htmlStorageLocation: HtmlStorageLocation.Local }; } @@ -510,4 +526,10 @@ export class StateMigrationService< protected async getCurrentStateVersion(): Promise { return (await this.getGlobals())?.stateVersion ?? StateVersion.One; } + + protected async setCurrentStateVersion(newVersion: StateVersion): Promise { + const globals = await this.getGlobals(); + globals.stateVersion = newVersion; + await this.set(keys.global, globals); + } }