diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 7d59afb8bd7..99b14b637d0 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -4,16 +4,42 @@ 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. + * @param userId + */ + abstract userKey$(userId: UserId): Observable; /** * Returns the an observable key for the given user id. * @@ -46,6 +72,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 +202,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 +223,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 +235,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 +252,20 @@ 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 + * + * @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 a + * 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 @@ -345,4 +370,30 @@ 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`. + */ + 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 key if they are unlocked, or null if the user is not unlocked. + */ + 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. + */ + abstract userPublicKey$(userId: UserId): Observable; } diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 8bb12894196..4ef2e7e7365 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,327 @@ describe("cryptoService", () => { }); }); }); + + describe("userPrivateKey$", () => { + const setupKeys = ({ + makeMasterKey, + makeUserKey, + }: { + makeMasterKey: boolean; + makeUserKey: boolean; + }): [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 legacy support set to %legacySupport and user hasUserKey = $hasUserKey and user hasMasterKey = $hasMasterKey", 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$", () => { + const 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; + }; + + const 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; + + const updateKeys = ( + keys: Partial<{ + userKey: UserKey; + encryptedPrivateKey: EncString; + orgKeys: Record; + providerKeys: Record; + }> = {}, + ) => { + 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 not 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 5 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 7595d5a3e32..60081460e9a 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/convertValues"; 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,179 @@ 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; + }), + ); + }), + ); + } + + // Not exposing this until there is a need + private 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 })), + ); + }), + ); + } }