1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +00:00

[Pm-13097] Rename cryptoservice to keyservice and move it to km ownership (#11358)

* Rename cryptoservice to keyservice

* Rename cryptoservice to keyservice

* Move key service to key management ownership

* Remove accidentally added file

* Fix cli build

* Fix browser build

* Run prettier

* Fix builds

* Fix cli build

* Fix tests

* Fix incorrect renames

* Rename webauthn-login-crypto-service

* Fix build errors due to merge conflicts

* Fix linting
This commit is contained in:
Bernd Schoolmann
2024-10-24 19:41:30 +02:00
committed by GitHub
parent 554171b688
commit b486fcc689
229 changed files with 1385 additions and 1446 deletions

View File

@@ -0,0 +1,425 @@
import { Observable } from "rxjs";
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../../common/src/auth/models/domain/kdf-config";
import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums";
import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key";
import { OrganizationId, UserId } from "../../../common/src/types/guid";
import {
UserKey,
MasterKey,
OrgKey,
ProviderKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../../common/src/types/key";
export class UserPrivateKeyDecryptionFailedError extends Error {
constructor() {
super("Failed to decrypt the user's private key.");
}
}
/**
* 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<OrganizationId, OrgKey>;
};
export abstract class KeyService {
/**
* 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<UserKey>;
/**
* Returns the an observable key for the given user id.
*
* @note this observable represents only user keys stored in memory. A null value does not indicate that we cannot load a user key from storage.
* @param userId The desired user
*/
abstract getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey>;
/**
* Sets the provided user key and stores
* any other necessary versions (such as auto, biometrics,
* or pin)
*
* @throws when key is null. Lock the account to clear a key
* @param key The user key to set
* @param userId The desired user
*/
abstract setUserKey(key: UserKey, userId?: string): Promise<void>;
/**
* Sets the provided user keys and stores any other necessary versions
* (such as auto, biometrics, or pin).
* Also sets the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
*
* @throws Error when userKey, encPrivateKey or userId is null
* @throws UserPrivateKeyDecryptionFailedError when the userKey cannot decrypt encPrivateKey
* @param userKey The user key to set
* @param encPrivateKey An encrypted private key
* @param userId The desired user
*/
abstract setUserKeys(userKey: UserKey, encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys
* (such as auto, biometrics, or pin)
*/
abstract refreshAdditionalKeys(): Promise<void>;
/**
* Observable value that returns whether or not the currently active user has ever had auser key,
* i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states.
*/
abstract everHadUserKey$: Observable<boolean>;
/**
* 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<UserKey>;
/**
* Checks if the user is using an old encryption scheme that used the master key
* for encryption of data instead of the user key.
*/
abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
*
* @deprecated Please provide the userId of the user you want the user key for.
*/
abstract getUserKeyWithLegacySupport(): Promise<UserKey>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
* @param userId The desired user
*/
abstract getUserKeyWithLegacySupport(userId: UserId): Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
* @param userId The desired user
* @returns The user key
*/
abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey>;
/**
* Determines whether the user key is available for the given user.
* @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
* @returns True if the user key is available
*/
abstract hasUserKey(userId?: UserId): Promise<boolean>;
/**
* Determines whether the user key is available for the given user in memory.
* @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
* @returns True if the user key is available
*/
abstract hasUserKeyInMemory(userId?: string): Promise<boolean>;
/**
* @param keySuffix The desired version of the user's key to check
* @param userId The desired user
* @returns True if the provided version of the user key is stored
*/
abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
* @returns A new user key and the master key protected version of it
*/
abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>;
/**
* Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear
* @param userId The desired user
*/
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/**
* Stores the master key encrypted user key
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>;
/**
* @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user
*/
abstract getOrDeriveMasterKey(password: string, userId?: string): Promise<MasterKey>;
/**
* Generates a master key from the provided password
* @param password The user's master password
* @param email The user's email
* @param KdfConfig The user's key derivation function configuration
* @returns A master key derived from the provided password
*/
abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey>;
/**
* Encrypts the existing (or provided) user key with the
* provided master key
* @param masterKey The user's master key
* @param userKey The user key
* @returns The user key and the master key protected version of it
*/
abstract encryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: UserKey,
): Promise<[UserKey, EncString]>;
/**
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
* on the hashPurpose provided.
* @param password The user's master password
* @param key The user's master key
* @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash
*/
abstract hashMasterKey(
password: string,
key: MasterKey,
hashPurpose?: HashPurpose,
): Promise<string>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.
* @param masterPassword The user's master password
* @param key The user's master key
* @returns True if the provided master password matches either the stored
* key hash or the server key hash
*/
abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean>;
/**
* Stores the encrypted organization keys and clears any decrypted
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
* @param userId The user id of the user to set the org keys for
*/
abstract setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void>;
/**
* 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<Record<OrganizationId, OrgKey>>;
/**
* Returns the organization's symmetric key
* @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<OrgKey>;
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key
*/
abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T,
): Promise<[SymmetricCryptoKey, EncString]>;
/**
* Stores the provider keys for a given user.
* @param orgs The provider orgs for which to save the keys from.
* @param userId The user id of the user for which to store the keys for.
*/
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
*/
abstract getProviderKey(providerId: string): Promise<ProviderKey>;
/**
* 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.
* @returns The new encrypted org key and the decrypted key itself
*/
abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>;
/**
* Sets the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* 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<Uint8Array>;
/**
* 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<UserPrivateKey>;
/**
* Gets an observable stream of the given users encrypted private key, will emit 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.
*
* @deprecated Temporary function to allow the SDK to be initialized after the login process, it
* will be removed when auth has been migrated to the SDK.
*/
abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString>;
/**
* Gets an observable stream of the given users decrypted private key with legacy support,
* 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 userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey>;
/**
* Generates a fingerprint phrase for the user based on their public key
* @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key
* @returns The user's fingerprint phrase
*/
abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]>;
/**
* Generates a new keypair
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
* @throws If the provided key is a null-ish value.
*/
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
* disable pin protection for the user
* @param userId The desired user
*/
abstract clearPinKeys(userId?: string): Promise<void>;
/**
* @param keyMaterial The key material to derive the send key from
* @returns A new send key
*/
abstract makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey>;
/**
* Clears all of the user's keys from storage
* @param userId The user's Id
*/
abstract clearKeys(userId?: string): Promise<any>;
abstract randomNumber(min: number, max: number): Promise<number>;
/**
* Generates a new cipher key
* @returns A new cipher key
*/
abstract makeCipherKey(): Promise<CipherKey>;
/**
* 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;
publicKey: string;
privateKey: EncString;
}>;
/**
* Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state
* of the older keys on logout or post migration.
* @param keySuffix The desired type of key to clear
* @param userId The desired user
*/
abstract clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/**
* 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<CipherDecryptionKeys | null>;
/**
* 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<Record<OrganizationId, OrgKey> | null>;
/**
* Gets an observable stream of the given users encrypted organisation keys.
*
* @param userId The user id of the user to get the data for.
*
* @deprecated Temporary function to allow the SDK to be initialized after the login process, it
* will be removed when auth has been migrated to the SDK.
*/
abstract encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>>;
/**
* 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<UserPublicKey>;
/**
* Validates that a userkey is correct for a given user
* @param key The key to validate
* @param userId The user id for the key
*/
abstract validateUserKey(key: UserKey, userId: UserId): Promise<boolean>;
}

View File

@@ -4,3 +4,6 @@ export {
} from "./biometrics/biometric-state.service";
export { BiometricsService } from "./biometrics/biometric.service";
export * from "./biometrics/biometric.state";
export { KeyService } from "./abstractions/key.service";
export { DefaultKeyService } from "./key.service";

View File

@@ -0,0 +1,736 @@
import { mock } from "jest-mock-extended";
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
import { PinServiceAbstraction } from "../../auth/src/common/abstractions";
import {
awaitAsync,
makeEncString,
makeStaticByteArray,
makeSymmetricCryptoKey,
} from "../../common/spec";
import { FakeAccountService, mockAccountServiceWith } from "../../common/spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../common/spec/fake-state";
import { FakeStateProvider } from "../../common/spec/fake-state-provider";
import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data";
import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../common/src/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service";
import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service";
import { LogService } from "../../common/src/platform/abstractions/log.service";
import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service";
import { StateService } from "../../common/src/platform/abstractions/state.service";
import { Encrypted } from "../../common/src/platform/interfaces/encrypted";
import { Utils } from "../../common/src/platform/misc/utils";
import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key";
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
} from "../../common/src/platform/services/key-state/user-key.state";
import { UserKeyDefinition } from "../../common/src/platform/state";
import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state";
import { CsprngArray } from "../../common/src/types/csprng";
import { OrganizationId, UserId } from "../../common/src/types/guid";
import { UserKey, MasterKey } from "../../common/src/types/key";
import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type";
import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service";
import { DefaultKeyService } from "./key.service";
describe("keyService", () => {
let keyService: DefaultKeyService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<StateService>();
const kdfConfigService = mock<KdfConfigService>();
let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
stateProvider = new FakeStateProvider(accountService);
keyService = new DefaultKeyService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
encryptService,
platformUtilService,
logService,
stateService,
accountService,
stateProvider,
kdfConfigService,
);
});
afterEach(() => {
jest.resetAllMocks();
});
it("instantiates", () => {
expect(keyService).not.toBeFalsy();
});
describe("getUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
it("retrieves the key state of the requested user", async () => {
await keyService.getUserKey(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(USER_KEY, mockUserId);
});
it("returns the User Key if available", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
const userKey = await keyService.getUserKey(mockUserId);
expect(userKey).toEqual(mockUserKey);
});
it("returns nullish if the user key is not set", async () => {
const userKey = await keyService.getUserKey(mockUserId);
expect(userKey).toBeFalsy();
});
});
describe.each(["hasUserKey", "hasUserKeyInMemory"])(
`%s`,
(method: "hasUserKey" | "hasUserKeyInMemory") => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
it.each([true, false])("returns %s if the user key is set", async (hasKey) => {
stateProvider.singleUser
.getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null);
expect(await keyService[method](mockUserId)).toBe(hasKey);
});
it("returns false when no active userId is set", async () => {
accountService.activeAccountSubject.next(null);
expect(await keyService[method]()).toBe(false);
});
it.each([true, false])(
"resolves %s for active user id when none is provided",
async (hasKey) => {
stateProvider.activeUserId$ = of(mockUserId);
stateProvider.singleUser
.getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null);
expect(await keyService[method]()).toBe(hasKey);
},
);
},
);
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey;
let mockMasterKey: MasterKey;
let getMasterKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
getMasterKey = jest.spyOn(masterPasswordService, "masterKey$");
});
it("returns the User Key if available", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
const getKeySpy = jest.spyOn(keyService, "getUserKey");
const userKey = await keyService.getUserKeyWithLegacySupport(mockUserId);
expect(getKeySpy).toHaveBeenCalledWith(mockUserId);
expect(getMasterKey).not.toHaveBeenCalled();
expect(userKey).toEqual(mockUserKey);
});
it("returns the user's master key when User Key is not available", async () => {
masterPasswordService.masterKeySubject.next(mockMasterKey);
const userKey = await keyService.getUserKeyWithLegacySupport(mockUserId);
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
expect(userKey).toEqual(mockMasterKey);
});
});
describe("everHadUserKey$", () => {
let everHadUserKeyState: FakeActiveUserState<boolean>;
beforeEach(() => {
everHadUserKeyState = stateProvider.activeUser.getFake(USER_EVER_HAD_USER_KEY);
});
it("should return true when stored value is true", async () => {
everHadUserKeyState.nextState(true);
expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(true);
});
it("should return false when stored value is false", async () => {
everHadUserKeyState.nextState(false);
expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(false);
});
it("should return false when stored value is null", async () => {
everHadUserKeyState.nextState(null);
expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(false);
});
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
let everHadUserKeyState: FakeSingleUserState<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
// Initialize storage
everHadUserKeyState.nextState(null);
});
it("should set everHadUserKey if key is not null to true", async () => {
await keyService.setUserKey(mockUserKey, mockUserId);
expect(await firstValueFrom(everHadUserKeyState.state$)).toBe(true);
});
describe("Auto Key refresh", () => {
it("sets an Auto key if vault timeout is set to 'never'", async () => {
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
await keyService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, {
userId: mockUserId,
});
});
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
await keyService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Auto key whenever a User Key is set", async () => {
await keyService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
it("throws if key is null", async () => {
await expect(keyService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
});
it("throws if userId is null", async () => {
await expect(keyService.setUserKey(mockUserKey, null)).rejects.toThrow("No userId provided.");
});
describe("Pin Key refresh", () => {
const mockPinKeyEncryptedUserKey = new EncString(
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
const mockUserKeyEncryptedPin = new EncString(
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
mockPinKeyEncryptedUserKey,
);
await keyService.setUserKey(mockUserKey, mockUserId);
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
false,
mockUserId,
);
});
it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
await keyService.setUserKey(mockUserKey, mockUserId);
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
true,
mockUserId,
);
});
it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => {
pinService.getUserKeyEncryptedPin.mockResolvedValue(null);
await keyService.setUserKey(mockUserKey, mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
});
});
});
describe("setUserKeys", () => {
let mockUserKey: UserKey;
let mockEncPrivateKey: EncryptedString;
let everHadUserKeyState: FakeSingleUserState<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockEncPrivateKey = new SymmetricCryptoKey(mockRandomBytes).toString() as EncryptedString;
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
// Initialize storage
everHadUserKeyState.nextState(null);
// Mock private key decryption
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
});
it("throws if userKey is null", async () => {
await expect(keyService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow(
"No userKey provided.",
);
});
it("throws if encPrivateKey is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow(
"No encPrivateKey provided.",
);
});
it("throws if userId is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow(
"No userId provided.",
);
});
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
encryptService.decryptToBytes.mockResolvedValue(null);
await expect(
keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId),
).rejects.toThrow(UserPrivateKeyDecryptionFailedError);
});
// We already have tests for setUserKey, so we just need to test that the correct methods are called
it("calls setUserKey with the userKey and userId", async () => {
const setUserKeySpy = jest.spyOn(keyService, "setUserKey");
await keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setUserKeySpy).toHaveBeenCalledWith(mockUserKey, mockUserId);
});
// We already have tests for setPrivateKey, so we just need to test that the correct methods are called
// TODO: Move those tests into here since `setPrivateKey` will be converted to a private method
it("calls setPrivateKey with the encPrivateKey and userId", async () => {
const setEncryptedPrivateKeySpy = jest.spyOn(keyService, "setPrivateKey");
await keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setEncryptedPrivateKeySpy).toHaveBeenCalledWith(mockEncPrivateKey, mockUserId);
});
});
describe("clearKeys", () => {
it("resolves active user id when called with no user id", async () => {
let callCount = 0;
stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++));
await keyService.clearKeys(null);
expect(callCount).toBe(1);
// revert to the original state
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
});
describe.each([
USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ENCRYPTED_PROVIDER_KEYS,
USER_ENCRYPTED_PRIVATE_KEY,
USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for active user when unspecified`, async () => {
await keyService.clearKeys(null);
const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
});
it(`clears ${key.key} for the specified user when specified`, async () => {
const userId = "someOtherUser" as UserId;
await keyService.clearKeys(userId);
const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
});
});
});
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<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey);
userKeyState.nextState(null);
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
userKeyState.nextState(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.nextState(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(keyService.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(keyService.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.nextState(null);
const userPrivateKey = await firstValueFrom(keyService.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<string, EncryptedOrganizationKeyData>;
providerKeys: Record<string, EncryptedString>;
};
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.nextState(keys.userKey);
}
if ("encryptedPrivateKey" in keys) {
const userEncryptedPrivateKey = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString);
}
if ("orgKeys" in keys) {
const orgKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.nextState(keys.orgKeys);
}
if ("providerKeys" in keys) {
const providerKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.nextState(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<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
});
const decryptionKeys = await firstValueFrom(keyService.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<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
},
});
const decryptionKeys = await firstValueFrom(keyService.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<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
},
providerKeys: {},
});
const decryptionKeys = await firstValueFrom(keyService.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<UserKey>(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(keyService.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(
keyService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)),
);
// User has their UserKey set
const initialUserKey = makeSymmetricCryptoKey<UserKey>(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<UserKey>(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(),
},
});
});
});
});

View File

@@ -0,0 +1,990 @@
import * as bigInt from "big-integer";
import {
NEVER,
Observable,
combineLatest,
firstValueFrom,
forkJoin,
map,
of,
switchMap,
} from "rxjs";
import { PinServiceAbstraction } from "../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data";
import { BaseEncryptedOrganizationKey } from "../../common/src/admin-console/models/domain/encrypted-organization-key";
import { ProfileOrganizationResponse } from "../../common/src/admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../common/src/admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../common/src/admin-console/models/response/profile-provider.response";
import { AccountService } from "../../common/src/auth/abstractions/account.service";
import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "../../common/src/auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "../../common/src/auth/models/domain/kdf-config";
import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service";
import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service";
import { LogService } from "../../common/src/platform/abstractions/log.service";
import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service";
import { StateService } from "../../common/src/platform/abstractions/state.service";
import { KeySuffixOptions, HashPurpose } from "../../common/src/platform/enums";
import { convertValues } from "../../common/src/platform/misc/convert-values";
import { Utils } from "../../common/src/platform/misc/utils";
import { EFFLongWordList } from "../../common/src/platform/misc/wordlist";
import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key";
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
} from "../../common/src/platform/services/key-state/user-key.state";
import { ActiveUserState, StateProvider } from "../../common/src/platform/state";
import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state";
import { CsprngArray } from "../../common/src/types/csprng";
import { OrganizationId, ProviderId, UserId } from "../../common/src/types/guid";
import {
OrgKey,
UserKey,
MasterKey,
ProviderKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../common/src/types/key";
import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type";
import {
CipherDecryptionKeys,
KeyService as KeyServiceAbstraction,
UserPrivateKeyDecryptionFailedError,
} from "./abstractions/key.service";
export class DefaultKeyService implements KeyServiceAbstraction {
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
readonly everHadUserKey$: Observable<boolean>;
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
constructor(
protected pinService: PinServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
protected encryptService: EncryptService,
protected platformUtilService: PlatformUtilsService,
protected logService: LogService,
protected stateService: StateService,
protected accountService: AccountService,
protected stateProvider: StateProvider,
protected kdfConfigService: KdfConfigService,
) {
// User Key
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
);
}
async setUserKey(key: UserKey, userId: UserId): Promise<void> {
if (key == null) {
throw new Error("No key provided. Lock the user to clear the key");
}
if (userId == null) {
throw new Error("No userId provided.");
}
// Set userId to ensure we have one for the account status update
await this.stateProvider.setUserState(USER_KEY, key, userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
await this.storeAdditionalKeys(key, userId);
}
async setUserKeys(
userKey: UserKey,
encPrivateKey: EncryptedString,
userId: UserId,
): Promise<void> {
if (userKey == null) {
throw new Error("No userKey provided. Lock the user to clear the key");
}
if (encPrivateKey == null) {
throw new Error("No encPrivateKey provided.");
}
if (userId == null) {
throw new Error("No userId provided.");
}
const decryptedPrivateKey = await this.decryptPrivateKey(encPrivateKey, userKey);
if (decryptedPrivateKey == null) {
throw new UserPrivateKeyDecryptionFailedError();
}
await this.setUserKey(userKey, userId);
await this.setPrivateKey(encPrivateKey, userId);
}
async refreshAdditionalKeys(): Promise<void> {
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<UserKey> {
return this.stateProvider.getUserState$(USER_KEY, userId);
}
async getUserKey(userId?: UserId): Promise<UserKey> {
const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
return userKey;
}
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return await this.validateUserKey(masterKey as unknown as UserKey, userId);
}
// TODO: legacy support for user key is no longer needed since we require users to migrate on login
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const userKey = await this.getUserKey(userId);
if (userKey) {
return userKey;
}
// Legacy support: encryption used to be done with the master key (derived from master password).
// Users who have not migrated will have a null user key and must use the master key instead.
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return masterKey as unknown as UserKey;
}
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey) {
if (!(await this.validateUserKey(userKey, userId))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
}
}
async hasUserKey(userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
return false;
}
return await this.hasUserKeyInMemory(userId);
}
async hasUserKeyInMemory(userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
return false;
}
return (await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId))) != null;
}
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
}
async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> {
if (!masterKey) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
if (masterKey == null) {
throw new Error("No Master Key found.");
}
const newUserKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(masterKey, newUserKey.key);
}
/**
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
* @param userId The desired user
*/
private async clearUserKey(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
// Set userId to ensure we have one for the account status update
await this.stateProvider.setUserState(USER_KEY, null, userId);
await this.clearAllStoredUserKeys(userId);
}
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
if (keySuffix === KeySuffixOptions.Auto) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
}
if (keySuffix === KeySuffixOptions.Pin) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
}
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey),
userId,
);
}
// TODO: Move to MasterPasswordService
async getOrDeriveMasterKey(password: string, userId?: UserId) {
const [resolvedUserId, email] = await firstValueFrom(
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
map(([activeAccount, accounts]) => {
userId ??= activeAccount?.id;
return [userId, accounts[userId]?.email];
}),
),
);
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
return (masterKey ||= await this.makeMasterKey(
password,
email,
await this.kdfConfigService.getKdfConfig(),
));
}
/**
* Derive a master key from a password and email.
*
* @remarks
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
* TODO: Move to MasterPasswordService
*/
async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey> {
return (await this.keyGenerationService.deriveKeyFromPassword(
password,
email,
KdfConfig,
)) as MasterKey;
}
async encryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: UserKey,
): Promise<[UserKey, EncString]> {
userKey ||= await this.getUserKey();
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
}
// TODO: move to MasterPasswordService
async hashMasterKey(
password: string,
key: MasterKey,
hashPurpose?: HashPurpose,
): Promise<string> {
if (!key) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
if (password == null || key == null) {
throw new Error("Invalid parameters.");
}
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations);
return Utils.fromBufferToB64(hash);
}
// TODO: move to MasterPasswordService
async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
const storedPasswordHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId),
);
if (masterPassword != null && storedPasswordHash != null) {
const localKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
if (localKeyHash != null && storedPasswordHash === localKeyHash) {
return true;
}
// TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated
const serverKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.ServerAuthorization,
);
if (serverKeyHash != null && storedPasswordHash === serverKeyHash) {
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
return true;
}
}
return false;
}
async setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => {
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
encOrgKeyData[org.id] = {
type: "organization",
key: org.key,
};
});
providerOrgs.forEach((org) => {
encOrgKeyData[org.id] = {
type: "provider",
providerId: org.providerId,
key: org.key,
};
});
return encOrgKeyData;
});
}
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> {
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<T extends OrgKey | UserKey>(
key: T,
): Promise<[SymmetricCryptoKey, EncString]> {
if (key == null) {
throw new Error("No key provided");
}
const newSymKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey.key);
}
private async clearOrgKeys(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId);
}
async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => {
const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {};
providers.forEach((provider) => {
encProviderKeys[provider.id as ProviderId] = provider.key as EncryptedString;
});
return encProviderKeys;
});
}
// TODO: Deprecate in favor of observable
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> {
if (providerId == null) {
return null;
}
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
return providerKeys[providerId] ?? null;
}
private async clearProviderKeys(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId);
}
// TODO: Make userId required
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
const shareKey = await this.keyGenerationService.createKey(512);
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const publicKey = await firstValueFrom(this.userPublicKey$(userId));
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
return [encShareKey, shareKey as T];
}
async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise<void> {
if (encPrivateKey == null) {
return;
}
await this.stateProvider
.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
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<string[]> {
if (publicKey == null) {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId));
}
if (publicKey === null) {
throw new Error("No public key available.");
}
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
keyFingerprint,
fingerprintMaterial,
32,
"sha256",
);
return this.hashPhrase(userFingerprint);
}
async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> {
if (key == null) {
throw new Error("'key' is a required parameter and must be non-null.");
}
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
const privateEnc = await this.encryptService.encrypt(keyPair[1], key);
return [publicB64, privateEnc];
}
/**
* Clears the user's key pair
* @param userId The desired user
*/
private async clearKeyPair(userId: UserId): Promise<void[]> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
async clearPinKeys(userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot clear PIN keys, no user Id resolved.");
}
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
await this.pinService.clearUserKeyEncryptedPin(userId);
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial,
"bitwarden-send",
"send",
);
}
async makeCipherKey(): Promise<CipherKey> {
return (await this.keyGenerationService.createKey(512)) as CipherKey;
}
async clearKeys(userId?: UserId): Promise<any> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot clear keys, no user Id resolved.");
}
await this.masterPasswordService.clearMasterKeyHash(userId);
await this.clearUserKey(userId);
await this.clearOrgKeys(userId);
await this.clearProviderKeys(userId);
await this.clearKeyPair(userId);
await this.clearPinKeys(userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
}
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
async randomNumber(min: number, max: number): Promise<number> {
let rval = 0;
const range = max - min + 1;
const bitsNeeded = Math.ceil(Math.log2(range));
if (bitsNeeded > 53) {
throw new Error("We cannot generate numbers larger than 53 bits.");
}
const bytesNeeded = Math.ceil(bitsNeeded / 8);
const mask = Math.pow(2, bitsNeeded) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Fill a byte array with N random numbers
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
let p = (bytesNeeded - 1) * 8;
for (let i = 0; i < bytesNeeded; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return this.randomNumber(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
// ---HELPERS---
async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
if (!key) {
return false;
}
try {
const encPrivateKey = await firstValueFrom(
this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$,
);
if (encPrivateKey == null) {
return false;
}
// Can decrypt private key
const privateKey = await this.decryptPrivateKey(encPrivateKey, key);
if (privateKey == null) {
// failed to decrypt
return false;
}
// Can successfully derive public key
const publicKey = await this.derivePublicKey(privateKey);
if (publicKey == null) {
// failed to decrypt
return false;
}
} catch (e) {
return false;
}
return true;
}
/**
* Initialize all necessary crypto keys needed for a new account.
* Warning! This completely replaces any existing keys!
*/
async initAccount(): Promise<{
userKey: UserKey;
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(activeUserId);
if (existingUserKey != null) {
this.logService.error("Tried to initialize account with existing user key.");
throw new Error("Cannot initialize account, keys already exist.");
}
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
await this.setUserKey(userKey, activeUserId);
await this.stateProvider
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => privateKey.encryptedString);
return {
userKey,
publicKey,
privateKey,
};
}
/**
* Generates any additional keys if needed. Additional keys are
* keys such as biometrics, auto, and pin keys.
* Useful to make sure other keys stay in sync when the user key
* has been rotated.
* @param key The user key
* @param userId The desired user
*/
protected async storeAdditionalKeys(key: UserKey, userId: UserId) {
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
} else {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
}
await this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) {
// Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId),
key,
);
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
key,
userId,
);
const noPreExistingPersistentKey =
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
noPreExistingPersistentKey,
userId,
);
// We can't always clear deprecated keys because the pin is only
// migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} else {
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) {
let shouldStoreKey = false;
switch (keySuffix) {
case KeySuffixOptions.Auto: {
// TODO: Sharing the UserKeyDefinition is temporary to get around a circ dep issue between
// the VaultTimeoutSettingsSvc and this service.
// This should be fixed as part of the PM-7082 - Auto Key Service work.
const vaultTimeout = await firstValueFrom(
this.stateProvider.getUserState$(VAULT_TIMEOUT, userId),
);
shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never;
break;
}
case KeySuffixOptions.Pin: {
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
shouldStoreKey = !!userKeyEncryptedPin;
break;
}
}
return shouldStoreKey;
}
protected async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Auto) {
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
if (userKey) {
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey;
}
}
return null;
}
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
const entropyPerWord = Math.log(EFFLongWordList.length) / Math.log(2);
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
const hashArr = Array.from(new Uint8Array(hash));
const entropyAvailable = hashArr.length * 4;
if (numWords * entropyPerWord > entropyAvailable) {
throw new Error("Output entropy of hash function is too small");
}
const phrase: string[] = [];
let hashNumber = bigInt.fromArray(hashArr, 256);
while (numWords--) {
const remainder = hashNumber.mod(EFFLongWordList.length);
hashNumber = hashNumber.divide(EFFLongWordList.length);
phrase.push(EFFLongWordList[remainder as any]);
}
return phrase;
}
private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>(
encryptionKey: SymmetricCryptoKey,
newSymKey: Uint8Array,
): Promise<[T, EncString]> {
let protectedSymKey: EncString = null;
if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
} else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
} else {
throw new Error("Invalid key size.");
}
return [new SymmetricCryptoKey(newSymKey) as T, protectedSymKey];
}
// --LEGACY METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) {
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
}
}
userKey$(userId: UserId): Observable<UserKey> {
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) {
if (privateKey == null) {
return null;
}
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey));
}
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
}
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, true).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<Record<ProviderId, ProviderKey>> {
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): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys));
}
encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$;
}
cipherDecryptionKeys$(
userId: UserId,
legacySupport: boolean = false,
): Observable<CipherDecryptionKeys | null> {
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<OrganizationId, OrgKey> = {};
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 })),
);
}),
);
}
}