diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index dd98ce2b44..242a748095 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -175,7 +175,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } // At this point of rotating their keys, they should still have their old user key in state - const oldUserKey = await firstValueFrom(this.cryptoService.activeUserKey$); + const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId)); const deviceIdentifier = await this.appIdService.getAppId(); const secretVerificationRequest = new SecretVerificationRequest(); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index f61bce563f..1527870cb4 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -595,7 +595,7 @@ describe("deviceTrustService", () => { const fakeNewUserKeyData = new Uint8Array(64); fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1); fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey; - cryptoService.activeUserKey$ = of(fakeNewUserKey); + cryptoService.userKey$.mockReturnValue(of(fakeNewUserKey)); }); it("throws an error when a null user id is passed in", async () => { @@ -631,7 +631,9 @@ describe("deviceTrustService", () => { fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1); // Mock the retrieval of a user key that differs from the new one passed into the method - cryptoService.activeUserKey$ = of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey); + cryptoService.userKey$.mockReturnValue( + of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey), + ); appIdService.getAppId.mockResolvedValue("test_device_identifier"); diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 7d59afb8bd..c2ecec6a09 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -4,16 +4,43 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; -import { OrganizationId, ProviderId, UserId } from "../../types/guid"; -import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key"; +import { OrganizationId, UserId } from "../../types/guid"; +import { + UserKey, + MasterKey, + OrgKey, + ProviderKey, + CipherKey, + UserPrivateKey, + UserPublicKey, +} from "../../types/key"; import { KeySuffixOptions, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -export abstract class CryptoService { - abstract activeUserKey$: Observable; +/** + * An object containing all the users key needed to decrypt a users personal and organization vaults. + */ +export type CipherDecryptionKeys = { + /** + * A users {@link UserKey} that is useful for decrypted ciphers in the users personal vault. + */ + userKey: UserKey; + /** + * A users decrypted organization keys. + */ + orgKeys: Record; +}; + +export abstract class CryptoService { + /** + * Retrieves a stream of the given users {@see UserKey} values. Can emit null if the user does not have a user key, e.g. the user + * is in a locked or logged out state. + * @param userId The user id of the user to get the {@see UserKey} for. + */ + abstract userKey$(userId: UserId): Observable; /** * Returns the an observable key for the given user id. * @@ -46,6 +73,8 @@ export abstract class CryptoService { * Retrieves the user key * @param userId The desired user * @returns The user key + * + * @deprecated Use {@link userKey$} with a required {@link UserId} instead. */ abstract getUserKey(userId?: string): Promise; @@ -174,19 +203,20 @@ export abstract class CryptoService { providerOrgs: ProfileProviderOrganizationResponse[], userId: UserId, ): Promise; + /** + * Retrieves a stream of the active users organization keys, + * will NOT emit any value if there is no active user. + * + * @deprecated Use {@link orgKeys$} with a required {@link UserId} instead. + */ abstract activeUserOrgKeys$: Observable>; /** * Returns the organization's symmetric key - * @deprecated Use the observable activeUserOrgKeys$ and `map` to the desired orgKey instead + * @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead * @param orgId The desired organization * @returns The organization's symmetric key */ abstract getOrgKey(orgId: string): Promise; - /** - * @deprecated Use the observable activeUserOrgKeys$ instead - * @returns A record of the organization Ids to their symmetric keys - */ - abstract getOrgKeys(): Promise>; /** * Uses the org key to derive a new symmetric key for encrypting data * @param orgKey The organization's symmetric key @@ -194,12 +224,6 @@ export abstract class CryptoService { abstract makeDataEncKey( key: T, ): Promise<[SymmetricCryptoKey, EncString]>; - /** - * Stores the encrypted provider keys and clears any decrypted - * provider keys currently in memory - * @param providers The providers to set keys for - */ - abstract activeUserProviderKeys$: Observable>; /** * Stores the provider keys for a given user. @@ -212,16 +236,6 @@ export abstract class CryptoService { * @returns The provider's symmetric key */ abstract getProviderKey(providerId: string): Promise; - /** - * @returns A record of the provider Ids to their symmetric keys - */ - abstract getProviderKeys(): Promise>; - /** - * Returns the public key from memory. If not available, extracts it - * from the private key and stores it in memory - * @returns The user's public key - */ - abstract getPublicKey(): Promise; /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. @@ -239,8 +253,22 @@ export abstract class CryptoService { * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory * @returns The user's private key + * + * @throws An error if there is no user currently active. + * + * @deprecated Use {@link userPrivateKey$} instead. */ abstract getPrivateKey(): Promise; + + /** + * Gets an observable stream of the given users decrypted private key, will emit null if the user + * doesn't have a UserKey to decrypt the encrypted private key or null if the user doesn't have an + * encrypted private key at all. + * + * @param userId The user id of the user to get the data for. + */ + abstract userPrivateKey$(userId: UserId): Observable; + /** * Generates a fingerprint phrase for the user based on their public key * @param fingerprintMaterial Fingerprint material @@ -300,6 +328,8 @@ export abstract class CryptoService { * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! * @returns The user's newly created public key, private key, and encrypted private key + * + * @throws An error if there is no user currently active. */ abstract initAccount(): Promise<{ userKey: UserKey; @@ -345,4 +375,38 @@ export abstract class CryptoService { encBuffer: EncArrayBuffer, key: SymmetricCryptoKey, ): Promise; + + /** + * Retrieves all the keys needed for decrypting Ciphers + * @param userId The user id of the keys to retrieve or null if the user is not Unlocked + * @param legacySupport `true` if you need to support retrieving the legacy version of the users key, `false` if + * you do not need legacy support. Use `true` by necessity only. Defaults to `false`. Legacy support is for users + * that may not have updated to use the new {@link UserKey} yet. + * + * @throws If an invalid user id is passed in. + */ + abstract cipherDecryptionKeys$( + userId: UserId, + legacySupport?: boolean, + ): Observable; + + /** + * Gets an observable of org keys for the given user. + * @param userId The user id of the user of which to get the keys for. + * @return An observable stream of the users organization keys if they are unlocked, or null if the user is not unlocked. + * The observable will stay alive through locks/unlocks. + * + * @throws If an invalid user id is passed in. + */ + abstract orgKeys$(userId: UserId): Observable | null>; + + /** + * Gets an observable stream of the users public key. If the user is does not have + * a {@link UserKey} or {@link UserPrivateKey} that is decryptable, this will emit null. + * + * @param userId The user id of the user of which to get the public key for. + * + * @throws If an invalid user id is passed in. + */ + abstract userPublicKey$(userId: UserId): Observable; } diff --git a/libs/common/src/platform/misc/convert-values.spec.ts b/libs/common/src/platform/misc/convert-values.spec.ts new file mode 100644 index 0000000000..61e98635c6 --- /dev/null +++ b/libs/common/src/platform/misc/convert-values.spec.ts @@ -0,0 +1,74 @@ +import { forkJoin, lastValueFrom, of, switchMap } from "rxjs"; + +import { convertValues } from "./convert-values"; + +describe("convertValues", () => { + it("returns null if given null", async () => { + const output = await lastValueFrom( + of>(null).pipe(convertValues((k, v) => of(v + 1))), + ); + + expect(output).toEqual(null); + }); + + it("returns empty record if given empty record", async () => { + const output = await lastValueFrom( + of>({}).pipe(convertValues((k, v) => of(v + 1))), + ); + + expect(output).toEqual({}); + }); + + const cases: { it: string; input: Record; output: Record }[] = [ + { + it: "converts single entry to observable", + input: { + one: 1, + }, + output: { + one: 2, + }, + }, + { + it: "converts multiple entries to observable", + input: { + one: 1, + two: 2, + three: 3, + }, + output: { + one: 2, + two: 3, + three: 4, + }, + }, + ]; + + it.each(cases)("$it", async ({ input, output: expectedOutput }) => { + const output = await lastValueFrom( + of(input).pipe( + convertValues((key, value) => of(value + 1)), + switchMap((values) => forkJoin(values)), + ), + ); + + expect(output).toEqual(expectedOutput); + }); + + it("converts async functions to observable", async () => { + const output = await lastValueFrom( + of({ + one: 1, + two: 2, + }).pipe( + convertValues(async (key, value) => await Promise.resolve(value + 1)), + switchMap((values) => forkJoin(values)), + ), + ); + + expect(output).toEqual({ + one: 2, + two: 3, + }); + }); +}); diff --git a/libs/common/src/platform/misc/convert-values.ts b/libs/common/src/platform/misc/convert-values.ts new file mode 100644 index 0000000000..7a1087ec36 --- /dev/null +++ b/libs/common/src/platform/misc/convert-values.ts @@ -0,0 +1,23 @@ +import { ObservableInput, OperatorFunction, map } from "rxjs"; + +/** + * Converts a record of keys and values into a record preserving the original key and converting each value into an {@link ObservableInput}. + * @param project A function to project a given key and value pair into an {@link ObservableInput} + */ +export function convertValues( + project: (key: TKey, value: TInput) => ObservableInput, +): OperatorFunction, Record>> { + return map((inputRecord) => { + if (inputRecord == null) { + return null; + } + + // Can't use TKey in here, have to use `PropertyKey` + const result: Record> = {}; + for (const [key, value] of Object.entries(inputRecord) as [TKey, TInput][]) { + result[key] = project(key, value); + } + + return result; + }); +} diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 8bb1289419..1b88922ca5 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -1,15 +1,22 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of, tap } from "rxjs"; +import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { + awaitAsync, + makeEncString, + makeStaticByteArray, + makeSymmetricCryptoKey, +} from "../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; +import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; import { CsprngArray } from "../../types/csprng"; -import { UserId } from "../../types/guid"; +import { OrganizationId, UserId } from "../../types/guid"; import { UserKey, MasterKey } from "../../types/key"; import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; @@ -18,8 +25,9 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; +import { Encrypted } from "../interfaces/encrypted"; import { Utils } from "../misc/utils"; -import { EncString } from "../models/domain/enc-string"; +import { EncString, EncryptedString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { CryptoService } from "../services/crypto.service"; import { UserKeyDefinition } from "../state"; @@ -340,4 +348,326 @@ describe("cryptoService", () => { }); }); }); + + describe("userPrivateKey$", () => { + type SetupKeysParams = { + makeMasterKey: boolean; + makeUserKey: boolean; + }; + + function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] { + const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); + const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey(64) : null; + masterPasswordService.masterKeySubject.next(fakeMasterKey); + userKeyState.stateSubject.next([mockUserId, null]); + const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey(64) : null; + userKeyState.stateSubject.next([mockUserId, fakeUserKey]); + return [fakeUserKey, fakeMasterKey]; + } + + it("will return users decrypted private key when user has a user key and encrypted private key set", async () => { + const [userKey] = setupKeys({ + makeMasterKey: false, + makeUserKey: true, + }); + + const userEncryptedPrivateKeyState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PRIVATE_KEY, + ); + + const fakeEncryptedUserPrivateKey = makeEncString("1"); + + userEncryptedPrivateKeyState.stateSubject.next([ + mockUserId, + fakeEncryptedUserPrivateKey.encryptedString, + ]); + + // Decryption of the user private key + const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1); + encryptService.decryptToBytes.mockResolvedValue(fakeDecryptedUserPrivateKey); + + const fakeUserPublicKey = makeStaticByteArray(10, 2); + cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey); + + const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + + expect(encryptService.decryptToBytes).toHaveBeenCalledWith( + fakeEncryptedUserPrivateKey, + userKey, + ); + + expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey); + }); + + it("returns null user private key when no user key is found", async () => { + setupKeys({ makeMasterKey: false, makeUserKey: false }); + + const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + + expect(encryptService.decryptToBytes).not.toHaveBeenCalled(); + + expect(userPrivateKey).toBeFalsy(); + }); + + it("returns null when user does not have a private key set", async () => { + setupKeys({ makeUserKey: true, makeMasterKey: false }); + + const encryptedUserPrivateKeyState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PRIVATE_KEY, + ); + encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]); + + const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + expect(userPrivateKey).toBeFalsy(); + }); + }); + + describe("cipherDecryptionKeys$", () => { + function fakePrivateKeyDecryption(encryptedPrivateKey: Encrypted, key: SymmetricCryptoKey) { + const output = new Uint8Array(64); + output.set(encryptedPrivateKey.dataBytes); + output.set( + key.key.subarray(0, 64 - encryptedPrivateKey.dataBytes.length), + encryptedPrivateKey.dataBytes.length, + ); + return output; + } + + function fakeOrgKeyDecryption(encryptedString: EncString, userPrivateKey: Uint8Array) { + const output = new Uint8Array(64); + output.set(encryptedString.dataBytes); + output.set( + userPrivateKey.subarray(0, 64 - encryptedString.dataBytes.length), + encryptedString.dataBytes.length, + ); + return output; + } + + const org1Id = "org1" as OrganizationId; + + type UpdateKeysParams = { + userKey: UserKey; + encryptedPrivateKey: EncString; + orgKeys: Record; + providerKeys: Record; + }; + + function updateKeys(keys: Partial = {}) { + if ("userKey" in keys) { + const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); + userKeyState.stateSubject.next([mockUserId, keys.userKey]); + } + + if ("encryptedPrivateKey" in keys) { + const userEncryptedPrivateKey = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PRIVATE_KEY, + ); + userEncryptedPrivateKey.stateSubject.next([ + mockUserId, + keys.encryptedPrivateKey.encryptedString, + ]); + } + + if ("orgKeys" in keys) { + const orgKeysState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_ORGANIZATION_KEYS, + ); + orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]); + } + + if ("providerKeys" in keys) { + const providerKeysState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PROVIDER_KEYS, + ); + providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]); + } + + encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => { + // TOOD: Branch between provider and private key? + return Promise.resolve(fakePrivateKeyDecryption(encryptedPrivateKey, userKey)); + }); + + encryptService.rsaDecrypt.mockImplementation((data, privateKey) => { + return Promise.resolve(fakeOrgKeyDecryption(data, privateKey)); + }); + } + + it("returns decryption keys when there are no org or provider keys set", async () => { + updateKeys({ + userKey: makeSymmetricCryptoKey(64), + encryptedPrivateKey: makeEncString("privateKey"), + }); + + const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + + expect(decryptionKeys).not.toBeNull(); + expect(decryptionKeys.userKey).not.toBeNull(); + expect(decryptionKeys.orgKeys).toEqual({}); + }); + + it("returns decryption keys when there are org keys", async () => { + updateKeys({ + userKey: makeSymmetricCryptoKey(64), + encryptedPrivateKey: makeEncString("privateKey"), + orgKeys: { + [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, + }, + }); + + const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + + expect(decryptionKeys).not.toBeNull(); + expect(decryptionKeys.userKey).not.toBeNull(); + expect(decryptionKeys.orgKeys).not.toBeNull(); + expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1); + expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull(); + const orgKey = decryptionKeys.orgKeys[org1Id]; + expect(orgKey.keyB64).toContain("org1Key"); + }); + + it("returns decryption keys when there is an empty record for provider keys", async () => { + updateKeys({ + userKey: makeSymmetricCryptoKey(64), + encryptedPrivateKey: makeEncString("privateKey"), + orgKeys: { + [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, + }, + providerKeys: {}, + }); + + const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + + expect(decryptionKeys).not.toBeNull(); + expect(decryptionKeys.userKey).not.toBeNull(); + expect(decryptionKeys.orgKeys).not.toBeNull(); + expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1); + expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull(); + const orgKey = decryptionKeys.orgKeys[org1Id]; + expect(orgKey.keyB64).toContain("org1Key"); + }); + + it("returns decryption keys when some of the org keys are providers", async () => { + const org2Id = "org2Id" as OrganizationId; + updateKeys({ + userKey: makeSymmetricCryptoKey(64), + encryptedPrivateKey: makeEncString("privateKey"), + orgKeys: { + [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, + [org2Id]: { + type: "provider", + key: makeEncString("provider1Key").encryptedString, + providerId: "provider1", + }, + }, + providerKeys: { + provider1: makeEncString("provider1Key").encryptedString, + }, + }); + + const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + + expect(decryptionKeys).not.toBeNull(); + expect(decryptionKeys.userKey).not.toBeNull(); + expect(decryptionKeys.orgKeys).not.toBeNull(); + expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(2); + + const orgKey = decryptionKeys.orgKeys[org1Id]; + expect(orgKey).not.toBeNull(); + expect(orgKey.keyB64).toContain("org1Key"); + + const org2Key = decryptionKeys.orgKeys[org2Id]; + expect(org2Key).not.toBeNull(); + expect(org2Key.keyB64).toContain("provider1Key"); + }); + + it("returns a stream that pays attention to updates of all data", async () => { + // Start listening until there have been 6 emissions + const promise = lastValueFrom( + cryptoService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)), + ); + + // User has their UserKey set + const initialUserKey = makeSymmetricCryptoKey(64); + updateKeys({ + userKey: initialUserKey, + }); + + // Because switchMap is a little to good at its job + await awaitAsync(); + + // User has their private key set + const initialPrivateKey = makeEncString("userPrivateKey"); + updateKeys({ + encryptedPrivateKey: initialPrivateKey, + }); + + // Because switchMap is a little to good at its job + await awaitAsync(); + + // Current architecture requires that provider keys are set before org keys + updateKeys({ + providerKeys: {}, + }); + + // Because switchMap is a little to good at its job + await awaitAsync(); + + // User has their org keys set + updateKeys({ + orgKeys: { + [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, + }, + }); + + // Out of band user key update + const updatedUserKey = makeSymmetricCryptoKey(64); + updateKeys({ + userKey: updatedUserKey, + }); + + const emittedValues = await promise; + + // They start with no data + expect(emittedValues[0]).toBeNull(); + + // They get their user key set + expect(emittedValues[1]).toEqual({ + userKey: initialUserKey, + orgKeys: null, + }); + + // Once a private key is set we will attempt org key decryption, even if org keys haven't been set + expect(emittedValues[2]).toEqual({ + userKey: initialUserKey, + orgKeys: {}, + }); + + // Will emit again when providers alone are set, but this won't change the output until orgs are set + expect(emittedValues[3]).toEqual({ + userKey: initialUserKey, + orgKeys: {}, + }); + + // Expect org keys to get emitted + expect(emittedValues[4]).toEqual({ + userKey: initialUserKey, + orgKeys: { + [org1Id]: expect.anything(), + }, + }); + + // Expect out of band user key update + expect(emittedValues[5]).toEqual({ + userKey: updatedUserKey, + orgKeys: { + [org1Id]: expect.anything(), + }, + }); + }); + }); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 7595d5a3e3..bef5922a31 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -1,8 +1,18 @@ import * as bigInt from "big-integer"; -import { Observable, combineLatest, filter, firstValueFrom, map, zip } from "rxjs"; +import { + NEVER, + Observable, + combineLatest, + firstValueFrom, + forkJoin, + map, + of, + switchMap, +} from "rxjs"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; +import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; @@ -25,55 +35,37 @@ import { } from "../../types/key"; import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; +import { + CipherDecryptionKeys, + CryptoService as CryptoServiceAbstraction, +} from "../abstractions/crypto.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; -import { sequentialize } from "../misc/sequentialize"; +import { convertValues } from "../misc/convert-values"; import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString, EncryptedString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { ActiveUserState, DerivedState, StateProvider } from "../state"; +import { ActiveUserState, StateProvider } from "../state"; -import { - USER_ENCRYPTED_ORGANIZATION_KEYS, - USER_ORGANIZATION_KEYS, -} from "./key-state/org-keys.state"; -import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./key-state/provider-keys.state"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state"; import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY, - USER_PRIVATE_KEY, - USER_PUBLIC_KEY, USER_KEY, } from "./key-state/user-key.state"; export class CryptoService implements CryptoServiceAbstraction { - private readonly activeUserKeyState: ActiveUserState; private readonly activeUserEverHadUserKey: ActiveUserState; - private readonly activeUserEncryptedOrgKeysState: ActiveUserState< - Record - >; - private readonly activeUserOrgKeysState: DerivedState>; - private readonly activeUserEncryptedProviderKeysState: ActiveUserState< - Record - >; - private readonly activeUserProviderKeysState: DerivedState>; - private readonly activeUserEncryptedPrivateKeyState: ActiveUserState; - private readonly activeUserPrivateKeyState: DerivedState; - private readonly activeUserPublicKeyState: DerivedState; - readonly activeUserKey$: Observable; + readonly everHadUserKey$: Observable; readonly activeUserOrgKeys$: Observable>; - readonly activeUserProviderKeys$: Observable>; - readonly activeUserPrivateKey$: Observable; - readonly activeUserPublicKey$: Observable; - readonly everHadUserKey$: Observable; constructor( protected pinService: PinServiceAbstraction, @@ -89,60 +81,12 @@ export class CryptoService implements CryptoServiceAbstraction { protected kdfConfigService: KdfConfigService, ) { // User Key - this.activeUserKeyState = stateProvider.getActive(USER_KEY); - this.activeUserKey$ = this.activeUserKeyState.state$; this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY); this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false)); - // User Asymmetric Key Pair - this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY); - this.activeUserPrivateKeyState = stateProvider.getDerived( - zip(this.activeUserEncryptedPrivateKeyState.state$, this.activeUserKey$).pipe( - filter(([, userKey]) => !!userKey), - ), - USER_PRIVATE_KEY, - { - encryptService: this.encryptService, - }, + this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)), ); - this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null - this.activeUserPublicKeyState = stateProvider.getDerived( - this.activeUserPrivateKey$.pipe(filter((key) => key != null)), - USER_PUBLIC_KEY, - { - cryptoFunctionService: this.cryptoFunctionService, - }, - ); - this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null - - // Provider keys - this.activeUserEncryptedProviderKeysState = stateProvider.getActive( - USER_ENCRYPTED_PROVIDER_KEYS, - ); - this.activeUserProviderKeysState = stateProvider.getDerived( - zip( - this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), - this.activeUserPrivateKey$, - ).pipe(filter(([, privateKey]) => !!privateKey)), - USER_PROVIDER_KEYS, - { encryptService: this.encryptService }, - ); - this.activeUserProviderKeys$ = this.activeUserProviderKeysState.state$; // null handled by `derive` function - - // Organization keys - this.activeUserEncryptedOrgKeysState = stateProvider.getActive( - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - this.activeUserOrgKeysState = stateProvider.getDerived( - zip( - this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)), - this.activeUserPrivateKey$, - this.activeUserProviderKeys$, - ).pipe(filter(([, privateKey]) => !!privateKey)), - USER_ORGANIZATION_KEYS, - { encryptService: this.encryptService }, - ); - this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function } async setUserKey(key: UserKey, userId?: UserId): Promise { @@ -157,8 +101,14 @@ export class CryptoService implements CryptoServiceAbstraction { } async refreshAdditionalKeys(): Promise { - const key = await this.getUserKey(); - await this.setUserKey(key); + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + + if (activeUserId == null) { + throw new Error("Can only refresh keys while there is an active user."); + } + + const key = await this.getUserKey(activeUserId); + await this.setUserKey(key, activeUserId); } getInMemoryUserKeyFor$(userId: UserId): Observable { @@ -399,12 +349,12 @@ export class CryptoService implements CryptoServiceAbstraction { } async getOrgKey(orgId: OrganizationId): Promise { - return (await firstValueFrom(this.activeUserOrgKeys$))[orgId]; - } - - @sequentialize(() => "getOrgKeys") - async getOrgKeys(): Promise> { - return await firstValueFrom(this.activeUserOrgKeys$); + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + if (activeUserId == null) { + throw new Error("A user must be active to retrieve an org key"); + } + const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId)); + return orgKeys[orgId]; } async makeDataEncKey( @@ -438,17 +388,16 @@ export class CryptoService implements CryptoServiceAbstraction { }); } + // TODO: Deprecate in favor of observable async getProviderKey(providerId: ProviderId): Promise { if (providerId == null) { return null; } - return (await firstValueFrom(this.activeUserProviderKeys$))[providerId] ?? null; - } + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId)); - @sequentialize(() => "getProviderKeys") - async getProviderKeys(): Promise> { - return await firstValueFrom(this.activeUserProviderKeys$); + return providerKeys[providerId] ?? null; } private async clearProviderKeys(userId: UserId): Promise { @@ -459,13 +408,11 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId); } - async getPublicKey(): Promise { - return await firstValueFrom(this.activeUserPublicKey$); - } - - async makeOrgKey(): Promise<[EncString, T]> { + // TODO: Make userId required + async makeOrgKey(userId?: UserId): Promise<[EncString, T]> { const shareKey = await this.keyGenerationService.createKey(512); - const publicKey = await this.getPublicKey(); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const publicKey = await firstValueFrom(this.userPublicKey$(userId)); const encShareKey = await this.rsaEncrypt(shareKey.key, publicKey); return [encShareKey, shareKey as T]; } @@ -481,13 +428,22 @@ export class CryptoService implements CryptoServiceAbstraction { } async getPrivateKey(): Promise { - return await firstValueFrom(this.activeUserPrivateKey$); + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + + if (activeUserId == null) { + throw new Error("User must be active while attempting to retrieve private key."); + } + + return await firstValueFrom(this.userPrivateKey$(activeUserId)); } + // TODO: Make public key required async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise { if (publicKey == null) { - publicKey = await this.getPublicKey(); + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + publicKey = await firstValueFrom(this.userPublicKey$(activeUserId)); } + if (publicKey === null) { throw new Error("No public key available."); } @@ -671,16 +627,15 @@ export class CryptoService implements CryptoServiceAbstraction { try { const encPrivateKey = await firstValueFrom( - this.stateProvider.getUserState$(USER_ENCRYPTED_PRIVATE_KEY, userId), + this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$, ); + if (encPrivateKey == null) { return false; } // Can decrypt private key - const privateKey = await USER_PRIVATE_KEY.derive([encPrivateKey, key], { - encryptService: this.encryptService, - }); + const privateKey = await this.decryptPrivateKey(encPrivateKey, key); if (privateKey == null) { // failed to decrypt @@ -688,9 +643,7 @@ export class CryptoService implements CryptoServiceAbstraction { } // Can successfully derive public key - const publicKey = await USER_PUBLIC_KEY.derive(privateKey, { - cryptoFunctionService: this.cryptoFunctionService, - }); + const publicKey = await this.derivePublicKey(privateKey); if (publicKey == null) { // failed to decrypt @@ -712,8 +665,15 @@ export class CryptoService implements CryptoServiceAbstraction { publicKey: string; privateKey: EncString; }> { + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + + if (activeUserId == null) { + throw new Error("Cannot initilize an account if one is not active."); + } + // Verify user key doesn't exist - const existingUserKey = await this.getUserKey(); + const existingUserKey = await this.getUserKey(activeUserId); + if (existingUserKey != null) { this.logService.error("Tried to initialize account with existing user key."); throw new Error("Cannot initialize account, keys already exist."); @@ -721,8 +681,10 @@ export class CryptoService implements CryptoServiceAbstraction { const userKey = (await this.keyGenerationService.createKey(512)) as UserKey; const [publicKey, privateKey] = await this.makeKeyPair(userKey); - await this.setUserKey(userKey); - await this.activeUserEncryptedPrivateKeyState.update(() => privateKey.encryptedString); + await this.setUserKey(userKey, activeUserId); + await this.stateProvider + .getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY) + .update(() => privateKey.encryptedString); return { userKey, @@ -925,4 +887,178 @@ export class CryptoService implements CryptoServiceAbstraction { return this.encryptService.decryptToBytes(encBuffer, key); } + + userKey$(userId: UserId) { + return this.stateProvider.getUser(userId, USER_KEY).state$; + } + + private userKeyWithLegacySupport$(userId: UserId) { + return this.userKey$(userId).pipe( + switchMap((userKey) => { + if (userKey != null) { + return of(userKey); + } + + // Legacy path + return this.masterPasswordService.masterKey$(userId).pipe( + switchMap(async (masterKey) => { + if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) { + // We don't have a UserKey or a valid MasterKey + return null; + } + + // The master key is valid meaning, the org keys and such are encrypted with this key + return masterKey as unknown as UserKey; + }), + ); + }), + ); + } + + userPublicKey$(userId: UserId) { + return this.userPrivateKey$(userId).pipe( + switchMap(async (pk) => await this.derivePublicKey(pk)), + ); + } + + private async derivePublicKey(privateKey: UserPrivateKey) { + return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; + } + + userPrivateKey$(userId: UserId): Observable { + return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey)); + } + + private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) { + const userKey$ = legacySupport ? this.userKeyWithLegacySupport$(userId) : this.userKey$(userId); + return userKey$.pipe( + switchMap((userKey) => { + if (userKey == null) { + return of(null); + } + + return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$.pipe( + switchMap( + async (encryptedPrivateKey) => + await this.decryptPrivateKey(encryptedPrivateKey, userKey), + ), + // Combine outerscope info with user private key + map((userPrivateKey) => ({ + userKey, + userPrivateKey, + })), + ); + }), + ); + } + + private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) { + if (encryptedPrivateKey == null) { + return null; + } + + return (await this.encryptService.decryptToBytes( + new EncString(encryptedPrivateKey), + key, + )) as UserPrivateKey; + } + + providerKeys$(userId: UserId) { + return this.userPrivateKey$(userId).pipe( + switchMap((userPrivateKey) => { + if (userPrivateKey == null) { + return of(null); + } + + return this.providerKeysHelper$(userId, userPrivateKey); + }), + ); + } + + /** + * A helper for decrypting provider keys that requires a user id and that users decrypted private key + * this is helpful for when you may have already grabbed the user private key and don't want to redo + * that work to get the provider keys. + */ + private providerKeysHelper$( + userId: UserId, + userPrivateKey: UserPrivateKey, + ): Observable> { + return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe( + // Convert each value in the record to it's own decryption observable + convertValues(async (_, value) => { + const decrypted = await this.encryptService.rsaDecrypt( + new EncString(value), + userPrivateKey, + ); + return new SymmetricCryptoKey(decrypted) as ProviderKey; + }), + // switchMap since there are no side effects + switchMap((encryptedProviderKeys) => { + if (encryptedProviderKeys == null) { + return of(null); + } + + // Can't give an empty record to forkJoin + if (Object.keys(encryptedProviderKeys).length === 0) { + return of({}); + } + + return forkJoin(encryptedProviderKeys); + }), + ); + } + + orgKeys$(userId: UserId) { + return this.cipherDecryptionKeys$(userId).pipe(map((keys) => keys?.orgKeys)); + } + + cipherDecryptionKeys$( + userId: UserId, + legacySupport: boolean = false, + ): Observable { + return this.userPrivateKeyHelper$(userId, legacySupport).pipe( + switchMap((userKeys) => { + if (userKeys == null) { + return of(null); + } + + const userPrivateKey = userKeys.userPrivateKey; + + if (userPrivateKey == null) { + // We can't do any org based decryption + return of({ userKey: userKeys.userKey, orgKeys: null }); + } + + return combineLatest([ + this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$, + this.providerKeysHelper$(userId, userPrivateKey), + ]).pipe( + switchMap(async ([encryptedOrgKeys, providerKeys]) => { + const result: Record = {}; + for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) { + if (result[orgId] != null) { + continue; + } + const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]); + + let decrypted: OrgKey; + + if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) { + decrypted = await encrypted.decrypt(this.encryptService, providerKeys); + } else { + decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey); + } + + result[orgId] = decrypted; + } + + return result; + }), + // Combine them back together + map((orgKeys) => ({ userKey: userKeys.userKey, orgKeys: orgKeys })), + ); + }), + ); + } } diff --git a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts index 98e0139cc4..79f24b61bb 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts @@ -1,11 +1,6 @@ -import { mock } from "jest-mock-extended"; +import { makeEncString } from "../../../../spec"; -import { makeEncString, makeStaticByteArray } from "../../../../spec"; -import { OrgKey, UserPrivateKey } from "../../../types/key"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; - -import { USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ORGANIZATION_KEYS } from "./org-keys.state"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./org-keys.state"; describe("encrypted org keys", () => { const sut = USER_ENCRYPTED_ORGANIZATION_KEYS; @@ -28,85 +23,3 @@ describe("encrypted org keys", () => { expect(result).toEqual(encryptedOrgKeys); }); }); - -describe("derived decrypted org keys", () => { - const encryptService = mock(); - const userPrivateKey = makeStaticByteArray(64, 3) as UserPrivateKey; - const sut = USER_ORGANIZATION_KEYS; - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should deserialize org keys", async () => { - const decryptedOrgKeys = { - "org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey, - "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, - }; - - const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedOrgKeys))); - - expect(result).toEqual(decryptedOrgKeys); - }); - - it("should derive org keys", async () => { - const encryptedOrgKeys = { - "org-id-1": { - type: "organization", - key: makeEncString().encryptedString, - }, - "org-id-2": { - type: "organization", - key: makeEncString().encryptedString, - }, - }; - - const decryptedOrgKeys = { - "org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey, - "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, - }; - - // TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey - encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); - encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); - - const result = await sut.derive([encryptedOrgKeys, userPrivateKey, {}], { encryptService }); - - expect(result).toEqual(decryptedOrgKeys); - }); - - it("should derive org keys from providers", async () => { - const encryptedOrgKeys = { - "org-id-1": { - type: "provider", - key: makeEncString().encryptedString, - providerId: "provider-id-1", - }, - "org-id-2": { - type: "provider", - key: makeEncString().encryptedString, - providerId: "provider-id-2", - }, - }; - - const providerKeys = { - "provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)), - "provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)), - }; - - const decryptedOrgKeys = { - "org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey, - "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, - }; - - // TODO: How to not have to mock these decryptions. They are internal concerns of ProviderEncryptedOrganizationKey - encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); - encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); - - const result = await sut.derive([encryptedOrgKeys, userPrivateKey, providerKeys], { - encryptService, - }); - - expect(result).toEqual(decryptedOrgKeys); - }); -}); diff --git a/libs/common/src/platform/services/key-state/org-keys.state.ts b/libs/common/src/platform/services/key-state/org-keys.state.ts index 8a42e242b1..81cf3411f1 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.ts @@ -1,10 +1,6 @@ import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; -import { BaseEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key"; -import { OrganizationId, ProviderId } from "../../../types/guid"; -import { OrgKey, ProviderKey, UserPrivateKey } from "../../../types/key"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state"; +import { OrganizationId } from "../../../types/guid"; +import { CRYPTO_DISK, UserKeyDefinition } from "../../state"; export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< EncryptedOrganizationKeyData, @@ -13,42 +9,3 @@ export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< deserializer: (obj) => obj, clearOn: ["logout"], }); - -export const USER_ORGANIZATION_KEYS = new DeriveDefinition< - [ - Record, - UserPrivateKey, - Record, - ], - Record, - { encryptService: EncryptService } ->(CRYPTO_MEMORY, "organizationKeys", { - deserializer: (obj) => { - const result: Record = {}; - for (const orgId of Object.keys(obj ?? {}) as OrganizationId[]) { - result[orgId] = SymmetricCryptoKey.fromJSON(obj[orgId]) as OrgKey; - } - return result; - }, - derive: async ([encryptedOrgKeys, privateKey, providerKeys], { encryptService }) => { - const result: Record = {}; - for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) { - if (result[orgId] != null) { - continue; - } - const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]); - - let decrypted: OrgKey; - - if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) { - decrypted = await encrypted.decrypt(encryptService, providerKeys); - } else { - decrypted = await encrypted.decrypt(encryptService, privateKey); - } - - result[orgId] = decrypted; - } - - return result; - }, -}); diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts b/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts index ca84d4a6ea..a8be2893e7 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts @@ -1,13 +1,6 @@ -import { mock } from "jest-mock-extended"; +import { makeEncString } from "../../../../spec"; -import { makeEncString, makeStaticByteArray } from "../../../../spec"; -import { ProviderId } from "../../../types/guid"; -import { ProviderKey, UserPrivateKey } from "../../../types/key"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { EncryptedString } from "../../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; - -import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "./provider-keys.state"; describe("encrypted provider keys", () => { const sut = USER_ENCRYPTED_PROVIDER_KEYS; @@ -23,51 +16,3 @@ describe("encrypted provider keys", () => { expect(result).toEqual(encryptedProviderKeys); }); }); - -describe("derived decrypted provider keys", () => { - const encryptService = mock(); - const userPrivateKey = makeStaticByteArray(64, 0) as UserPrivateKey; - const sut = USER_PROVIDER_KEYS; - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should deserialize provider keys", async () => { - const decryptedProviderKeys = { - "provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as ProviderKey, - "provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as ProviderKey, - }; - - const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedProviderKeys))); - - expect(result).toEqual(decryptedProviderKeys); - }); - - it("should derive provider keys", async () => { - const encryptedProviderKeys = { - "provider-id-1": makeEncString().encryptedString, - "provider-id-2": makeEncString().encryptedString, - }; - - const decryptedProviderKeys = { - "provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as ProviderKey, - "provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as ProviderKey, - }; - - encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-1"].key); - encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-2"].key); - - const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService }); - - expect(result).toEqual(decryptedProviderKeys); - }); - - it("should handle null input values", async () => { - const encryptedProviderKeys: Record = null; - - const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService }); - - expect(result).toEqual({}); - }); -}); diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.ts b/libs/common/src/platform/services/key-state/provider-keys.state.ts index dfda71be21..c0d9e3a1ea 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.ts @@ -1,9 +1,6 @@ import { ProviderId } from "../../../types/guid"; -import { ProviderKey, UserPrivateKey } from "../../../types/key"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { EncString, EncryptedString } from "../../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state"; +import { EncryptedString } from "../../models/domain/enc-string"; +import { CRYPTO_DISK, UserKeyDefinition } from "../../state"; export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record( CRYPTO_DISK, @@ -13,32 +10,3 @@ export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record, UserPrivateKey], - Record, - { encryptService: EncryptService } ->(CRYPTO_MEMORY, "providerKeys", { - deserializer: (obj) => { - const result: Record = {}; - for (const providerId of Object.keys(obj ?? {}) as ProviderId[]) { - result[providerId] = SymmetricCryptoKey.fromJSON(obj[providerId]) as ProviderKey; - } - return result; - }, - derive: async ([encryptedProviderKeys, privateKey], { encryptService }) => { - const result: Record = {}; - for (const providerId of Object.keys(encryptedProviderKeys ?? {}) as ProviderId[]) { - if (result[providerId] != null) { - continue; - } - const encrypted = new EncString(encryptedProviderKeys[providerId]); - const decrypted = await encryptService.rsaDecrypt(encrypted, privateKey); - const providerKey = new SymmetricCryptoKey(decrypted) as ProviderKey; - - result[providerId] = providerKey; - } - - return result; - }, -}); diff --git a/libs/common/src/platform/services/key-state/user-key.state.spec.ts b/libs/common/src/platform/services/key-state/user-key.state.spec.ts index 63273f1c79..6154fba8f4 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.spec.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.spec.ts @@ -1,19 +1,8 @@ -import { mock } from "jest-mock-extended"; - -import { makeStaticByteArray } from "../../../../spec"; -import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; import { EncString } from "../../models/domain/enc-string"; -import { - USER_ENCRYPTED_PRIVATE_KEY, - USER_EVER_HAD_USER_KEY, - USER_PRIVATE_KEY, - USER_PUBLIC_KEY, -} from "./user-key.state"; +import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY } from "./user-key.state"; function makeEncString(data?: string) { data ??= Utils.newGuid(); @@ -43,76 +32,3 @@ describe("Encrypted private key", () => { expect(result).toEqual(encryptedPrivateKey); }); }); - -describe("User public key", () => { - const sut = USER_PUBLIC_KEY; - const userPrivateKey = makeStaticByteArray(64, 1) as UserPrivateKey; - const userPublicKey = makeStaticByteArray(64, 2) as UserPublicKey; - - it("should deserialize user public key", () => { - const userPublicKey = makeStaticByteArray(64, 1); - - const result = sut.deserialize(JSON.parse(JSON.stringify(userPublicKey))); - - expect(result).toEqual(userPublicKey); - }); - - it("should derive user public key", async () => { - const cryptoFunctionService = mock(); - cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(userPublicKey); - - const result = await sut.derive(userPrivateKey, { cryptoFunctionService }); - - expect(result).toEqual(userPublicKey); - }); -}); - -describe("Derived decrypted private key", () => { - const sut = USER_PRIVATE_KEY; - const userKey = mock(); - const encryptedPrivateKey = makeEncString().encryptedString; - const decryptedPrivateKey = makeStaticByteArray(64, 1); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should deserialize decrypted private key", () => { - const decryptedPrivateKey = makeStaticByteArray(64, 1); - - const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedPrivateKey))); - - expect(result).toEqual(decryptedPrivateKey); - }); - - it("should derive decrypted private key", async () => { - const encryptService = mock(); - encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey); - - const result = await sut.derive([encryptedPrivateKey, userKey], { - encryptService, - }); - - expect(result).toEqual(decryptedPrivateKey); - }); - - it("should handle null encryptedPrivateKey", async () => { - const encryptService = mock(); - - const result = await sut.derive([null, userKey], { - encryptService, - }); - - expect(result).toEqual(null); - }); - - it("should handle null userKey", async () => { - const encryptService = mock(); - - const result = await sut.derive([encryptedPrivateKey, null], { - encryptService, - }); - - expect(result).toEqual(null); - }); -}); diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index c2b84d6a24..cd124321f6 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -1,9 +1,7 @@ -import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { EncString, EncryptedString } from "../../models/domain/enc-string"; +import { UserKey } from "../../../types/key"; +import { EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; +import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition( CRYPTO_DISK, @@ -23,41 +21,6 @@ export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition }, ); -export const USER_PRIVATE_KEY = new DeriveDefinition< - [EncryptedString, UserKey], - UserPrivateKey, - { encryptService: EncryptService } ->(CRYPTO_MEMORY, "privateKey", { - deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey, - derive: async ([encPrivateKeyString, userKey], { encryptService }) => { - if (encPrivateKeyString == null || userKey == null) { - return null; - } - - const encPrivateKey = new EncString(encPrivateKeyString); - const privateKey = (await encryptService.decryptToBytes( - encPrivateKey, - userKey, - )) as UserPrivateKey; - return privateKey; - }, -}); - -export const USER_PUBLIC_KEY = DeriveDefinition.from< - UserPrivateKey, - UserPublicKey, - { cryptoFunctionService: CryptoFunctionService } ->([USER_PRIVATE_KEY, "publicKey"], { - deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPublicKey, - derive: async (privateKey, { cryptoFunctionService }) => { - if (privateKey == null) { - return null; - } - - return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; - }, -}); - export const USER_KEY = new UserKeyDefinition(CRYPTO_MEMORY, "userKey", { deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, clearOn: ["logout", "lock"], diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4648a8cc40..9b9841169f 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -135,8 +135,7 @@ export class CipherService implements CipherServiceAbstraction { this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } - async setDecryptedCipherCache(value: CipherView[]) { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); + async setDecryptedCipherCache(value: CipherView[], userId: UserId) { // Sometimes we might prematurely decrypt the vault and that will result in no ciphers // if we cache it then we may accidentally return it when it's not right, we'd rather try decryption again. // We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption. @@ -367,9 +366,15 @@ export class CipherService implements CipherServiceAbstraction { return await this.getDecryptedCiphers(); } - decCiphers = await this.decryptCiphers(await this.getAll()); + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - await this.setDecryptedCipherCache(decCiphers); + if (activeUserId == null) { + return []; + } + + decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId); + + await this.setDecryptedCipherCache(decCiphers, activeUserId); return decCiphers; } @@ -377,10 +382,10 @@ export class CipherService implements CipherServiceAbstraction { return Object.values(await firstValueFrom(this.cipherViews$)); } - private async decryptCiphers(ciphers: Cipher[]) { - const orgKeys = await this.cryptoService.getOrgKeys(); - const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); - if (Object.keys(orgKeys).length === 0 && userKey == null) { + private async decryptCiphers(ciphers: Cipher[], userId: UserId) { + const keys = await firstValueFrom(this.cryptoService.cipherDecryptionKeys$(userId, true)); + + if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { // return early if there are no keys to decrypt with return; } @@ -398,7 +403,10 @@ export class CipherService implements CipherServiceAbstraction { const decCiphers = ( await Promise.all( Object.entries(grouped).map(([orgId, groupedCiphers]) => - this.encryptService.decryptItems(groupedCiphers, orgKeys[orgId] ?? userKey), + this.encryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ), ), ) )