mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[PM-8210] Discourage Active User in CryptoService
(#9364)
* Add Helper For Preparing a Record For Use in `forkJoin` * Update & Test CryptoService Changes * Delete Unused Code * Update DeviceTrustService * Update CipherService * Make `userPublicKey$` Public * Rename convertValues File * Update libs/common/src/platform/abstractions/crypto.service.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Add `convertValues` Tests * Add Doc Comments * Convert to `function`'s Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Fix Test Typos * Add param doc * Update Test Name * Add `@throws` Docs --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
parent
b784fe7593
commit
0e7ed8dd7f
@ -175,7 +175,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
}
|
||||
|
||||
// At this point of rotating their keys, they should still have their old user key in state
|
||||
const oldUserKey = await firstValueFrom(this.cryptoService.activeUserKey$);
|
||||
const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId));
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const secretVerificationRequest = new SecretVerificationRequest();
|
||||
|
@ -595,7 +595,7 @@ describe("deviceTrustService", () => {
|
||||
const fakeNewUserKeyData = new Uint8Array(64);
|
||||
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
|
||||
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
|
||||
cryptoService.activeUserKey$ = of(fakeNewUserKey);
|
||||
cryptoService.userKey$.mockReturnValue(of(fakeNewUserKey));
|
||||
});
|
||||
|
||||
it("throws an error when a null user id is passed in", async () => {
|
||||
@ -631,7 +631,9 @@ describe("deviceTrustService", () => {
|
||||
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
|
||||
|
||||
// Mock the retrieval of a user key that differs from the new one passed into the method
|
||||
cryptoService.activeUserKey$ = of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey);
|
||||
cryptoService.userKey$.mockReturnValue(
|
||||
of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey),
|
||||
);
|
||||
|
||||
appIdService.getAppId.mockResolvedValue("test_device_identifier");
|
||||
|
||||
|
@ -4,16 +4,43 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response
|
||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
||||
import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key";
|
||||
import { OrganizationId, UserId } from "../../types/guid";
|
||||
import {
|
||||
UserKey,
|
||||
MasterKey,
|
||||
OrgKey,
|
||||
ProviderKey,
|
||||
CipherKey,
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
} from "../../types/key";
|
||||
import { KeySuffixOptions, HashPurpose } from "../enums";
|
||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class CryptoService {
|
||||
abstract activeUserKey$: Observable<UserKey>;
|
||||
/**
|
||||
* 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 CryptoService {
|
||||
/**
|
||||
* Retrieves a stream of the given users {@see UserKey} values. Can emit null if the user does not have a user key, e.g. the user
|
||||
* is in a locked or logged out state.
|
||||
* @param userId The user id of the user to get the {@see UserKey} for.
|
||||
*/
|
||||
abstract userKey$(userId: UserId): Observable<UserKey>;
|
||||
/**
|
||||
* Returns the an observable key for the given user id.
|
||||
*
|
||||
@ -46,6 +73,8 @@ export abstract class CryptoService {
|
||||
* Retrieves the user key
|
||||
* @param userId The desired user
|
||||
* @returns The user key
|
||||
*
|
||||
* @deprecated Use {@link userKey$} with a required {@link UserId} instead.
|
||||
*/
|
||||
abstract getUserKey(userId?: string): Promise<UserKey>;
|
||||
|
||||
@ -174,19 +203,20 @@ export abstract class CryptoService {
|
||||
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 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<OrgKey>;
|
||||
/**
|
||||
* @deprecated Use the observable activeUserOrgKeys$ instead
|
||||
* @returns A record of the organization Ids to their symmetric keys
|
||||
*/
|
||||
abstract getOrgKeys(): Promise<Record<string, SymmetricCryptoKey>>;
|
||||
/**
|
||||
* Uses the org key to derive a new symmetric key for encrypting data
|
||||
* @param orgKey The organization's symmetric key
|
||||
@ -194,12 +224,6 @@ export abstract class CryptoService {
|
||||
abstract makeDataEncKey<T extends UserKey | OrgKey>(
|
||||
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<Record<ProviderId, ProviderKey>>;
|
||||
|
||||
/**
|
||||
* Stores the provider keys for a given user.
|
||||
@ -212,16 +236,6 @@ export abstract class CryptoService {
|
||||
* @returns The provider's symmetric key
|
||||
*/
|
||||
abstract getProviderKey(providerId: string): Promise<ProviderKey>;
|
||||
/**
|
||||
* @returns A record of the provider Ids to their symmetric keys
|
||||
*/
|
||||
abstract getProviderKeys(): Promise<Record<ProviderId, ProviderKey>>;
|
||||
/**
|
||||
* 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<Uint8Array>;
|
||||
/**
|
||||
* Creates a new organization key and encrypts it with the user's public key.
|
||||
* This method can also return Provider keys for creating new Provider users.
|
||||
@ -239,8 +253,22 @@ export abstract class CryptoService {
|
||||
* Returns the private key from memory. If not available, decrypts it
|
||||
* from storage and stores it in memory
|
||||
* @returns The user's private key
|
||||
*
|
||||
* @throws An error if there is no user currently active.
|
||||
*
|
||||
* @deprecated Use {@link userPrivateKey$} instead.
|
||||
*/
|
||||
abstract getPrivateKey(): Promise<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>;
|
||||
|
||||
/**
|
||||
* Generates a fingerprint phrase for the user based on their public key
|
||||
* @param fingerprintMaterial Fingerprint material
|
||||
@ -300,6 +328,8 @@ export abstract class CryptoService {
|
||||
* Initialize all necessary crypto keys needed for a new account.
|
||||
* Warning! This completely replaces any existing keys!
|
||||
* @returns The user's newly created public key, private key, and encrypted private key
|
||||
*
|
||||
* @throws An error if there is no user currently active.
|
||||
*/
|
||||
abstract initAccount(): Promise<{
|
||||
userKey: UserKey;
|
||||
@ -345,4 +375,38 @@ export abstract class CryptoService {
|
||||
encBuffer: EncArrayBuffer,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
* 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 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>;
|
||||
}
|
||||
|
74
libs/common/src/platform/misc/convert-values.spec.ts
Normal file
74
libs/common/src/platform/misc/convert-values.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { forkJoin, lastValueFrom, of, switchMap } from "rxjs";
|
||||
|
||||
import { convertValues } from "./convert-values";
|
||||
|
||||
describe("convertValues", () => {
|
||||
it("returns null if given null", async () => {
|
||||
const output = await lastValueFrom(
|
||||
of<Record<string, number>>(null).pipe(convertValues((k, v) => of(v + 1))),
|
||||
);
|
||||
|
||||
expect(output).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns empty record if given empty record", async () => {
|
||||
const output = await lastValueFrom(
|
||||
of<Record<string, number>>({}).pipe(convertValues((k, v) => of(v + 1))),
|
||||
);
|
||||
|
||||
expect(output).toEqual({});
|
||||
});
|
||||
|
||||
const cases: { it: string; input: Record<string, number>; output: Record<string, number> }[] = [
|
||||
{
|
||||
it: "converts single entry to observable",
|
||||
input: {
|
||||
one: 1,
|
||||
},
|
||||
output: {
|
||||
one: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
it: "converts multiple entries to observable",
|
||||
input: {
|
||||
one: 1,
|
||||
two: 2,
|
||||
three: 3,
|
||||
},
|
||||
output: {
|
||||
one: 2,
|
||||
two: 3,
|
||||
three: 4,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cases)("$it", async ({ input, output: expectedOutput }) => {
|
||||
const output = await lastValueFrom(
|
||||
of(input).pipe(
|
||||
convertValues((key, value) => of(value + 1)),
|
||||
switchMap((values) => forkJoin(values)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("converts async functions to observable", async () => {
|
||||
const output = await lastValueFrom(
|
||||
of({
|
||||
one: 1,
|
||||
two: 2,
|
||||
}).pipe(
|
||||
convertValues(async (key, value) => await Promise.resolve(value + 1)),
|
||||
switchMap((values) => forkJoin(values)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
one: 2,
|
||||
two: 3,
|
||||
});
|
||||
});
|
||||
});
|
23
libs/common/src/platform/misc/convert-values.ts
Normal file
23
libs/common/src/platform/misc/convert-values.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ObservableInput, OperatorFunction, map } from "rxjs";
|
||||
|
||||
/**
|
||||
* Converts a record of keys and values into a record preserving the original key and converting each value into an {@link ObservableInput}.
|
||||
* @param project A function to project a given key and value pair into an {@link ObservableInput}
|
||||
*/
|
||||
export function convertValues<TKey extends PropertyKey, TInput, TOutput>(
|
||||
project: (key: TKey, value: TInput) => ObservableInput<TOutput>,
|
||||
): OperatorFunction<Record<TKey, TInput>, Record<TKey, ObservableInput<TOutput>>> {
|
||||
return map((inputRecord) => {
|
||||
if (inputRecord == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Can't use TKey in here, have to use `PropertyKey`
|
||||
const result: Record<PropertyKey, ObservableInput<TOutput>> = {};
|
||||
for (const [key, value] of Object.entries(inputRecord) as [TKey, TInput][]) {
|
||||
result[key] = project(key, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
@ -1,15 +1,22 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of, tap } from "rxjs";
|
||||
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import {
|
||||
awaitAsync,
|
||||
makeEncString,
|
||||
makeStaticByteArray,
|
||||
makeSymmetricCryptoKey,
|
||||
} from "../../../spec";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
|
||||
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
||||
import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { OrganizationId, UserId } from "../../types/guid";
|
||||
import { UserKey, MasterKey } from "../../types/key";
|
||||
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
@ -18,8 +25,9 @@ import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { Encrypted } from "../interfaces/encrypted";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { EncString, EncryptedString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../services/crypto.service";
|
||||
import { UserKeyDefinition } from "../state";
|
||||
@ -340,4 +348,326 @@ describe("cryptoService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("userPrivateKey$", () => {
|
||||
type SetupKeysParams = {
|
||||
makeMasterKey: boolean;
|
||||
makeUserKey: boolean;
|
||||
};
|
||||
|
||||
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] {
|
||||
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
|
||||
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
|
||||
masterPasswordService.masterKeySubject.next(fakeMasterKey);
|
||||
userKeyState.stateSubject.next([mockUserId, null]);
|
||||
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
|
||||
userKeyState.stateSubject.next([mockUserId, fakeUserKey]);
|
||||
return [fakeUserKey, fakeMasterKey];
|
||||
}
|
||||
|
||||
it("will return users decrypted private key when user has a user key and encrypted private key set", async () => {
|
||||
const [userKey] = setupKeys({
|
||||
makeMasterKey: false,
|
||||
makeUserKey: true,
|
||||
});
|
||||
|
||||
const userEncryptedPrivateKeyState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
|
||||
const fakeEncryptedUserPrivateKey = makeEncString("1");
|
||||
|
||||
userEncryptedPrivateKeyState.stateSubject.next([
|
||||
mockUserId,
|
||||
fakeEncryptedUserPrivateKey.encryptedString,
|
||||
]);
|
||||
|
||||
// Decryption of the user private key
|
||||
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
|
||||
encryptService.decryptToBytes.mockResolvedValue(fakeDecryptedUserPrivateKey);
|
||||
|
||||
const fakeUserPublicKey = makeStaticByteArray(10, 2);
|
||||
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey);
|
||||
|
||||
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
|
||||
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(
|
||||
fakeEncryptedUserPrivateKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey);
|
||||
});
|
||||
|
||||
it("returns null user private key when no user key is found", async () => {
|
||||
setupKeys({ makeMasterKey: false, makeUserKey: false });
|
||||
|
||||
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
|
||||
|
||||
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
|
||||
|
||||
expect(userPrivateKey).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns null when user does not have a private key set", async () => {
|
||||
setupKeys({ makeUserKey: true, makeMasterKey: false });
|
||||
|
||||
const encryptedUserPrivateKeyState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]);
|
||||
|
||||
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
|
||||
expect(userPrivateKey).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cipherDecryptionKeys$", () => {
|
||||
function fakePrivateKeyDecryption(encryptedPrivateKey: Encrypted, key: SymmetricCryptoKey) {
|
||||
const output = new Uint8Array(64);
|
||||
output.set(encryptedPrivateKey.dataBytes);
|
||||
output.set(
|
||||
key.key.subarray(0, 64 - encryptedPrivateKey.dataBytes.length),
|
||||
encryptedPrivateKey.dataBytes.length,
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
function fakeOrgKeyDecryption(encryptedString: EncString, userPrivateKey: Uint8Array) {
|
||||
const output = new Uint8Array(64);
|
||||
output.set(encryptedString.dataBytes);
|
||||
output.set(
|
||||
userPrivateKey.subarray(0, 64 - encryptedString.dataBytes.length),
|
||||
encryptedString.dataBytes.length,
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
const org1Id = "org1" as OrganizationId;
|
||||
|
||||
type UpdateKeysParams = {
|
||||
userKey: UserKey;
|
||||
encryptedPrivateKey: EncString;
|
||||
orgKeys: Record<string, EncryptedOrganizationKeyData>;
|
||||
providerKeys: Record<string, EncryptedString>;
|
||||
};
|
||||
|
||||
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
|
||||
if ("userKey" in keys) {
|
||||
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
|
||||
userKeyState.stateSubject.next([mockUserId, keys.userKey]);
|
||||
}
|
||||
|
||||
if ("encryptedPrivateKey" in keys) {
|
||||
const userEncryptedPrivateKey = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
userEncryptedPrivateKey.stateSubject.next([
|
||||
mockUserId,
|
||||
keys.encryptedPrivateKey.encryptedString,
|
||||
]);
|
||||
}
|
||||
|
||||
if ("orgKeys" in keys) {
|
||||
const orgKeysState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]);
|
||||
}
|
||||
|
||||
if ("providerKeys" in keys) {
|
||||
const providerKeysState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]);
|
||||
}
|
||||
|
||||
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {
|
||||
// TOOD: Branch between provider and private key?
|
||||
return Promise.resolve(fakePrivateKeyDecryption(encryptedPrivateKey, userKey));
|
||||
});
|
||||
|
||||
encryptService.rsaDecrypt.mockImplementation((data, privateKey) => {
|
||||
return Promise.resolve(fakeOrgKeyDecryption(data, privateKey));
|
||||
});
|
||||
}
|
||||
|
||||
it("returns decryption keys when there are no org or provider keys set", async () => {
|
||||
updateKeys({
|
||||
userKey: makeSymmetricCryptoKey<UserKey>(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<UserKey>(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<UserKey>(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<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(cryptoService.cipherDecryptionKeys$(mockUserId));
|
||||
|
||||
expect(decryptionKeys).not.toBeNull();
|
||||
expect(decryptionKeys.userKey).not.toBeNull();
|
||||
expect(decryptionKeys.orgKeys).not.toBeNull();
|
||||
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(2);
|
||||
|
||||
const orgKey = decryptionKeys.orgKeys[org1Id];
|
||||
expect(orgKey).not.toBeNull();
|
||||
expect(orgKey.keyB64).toContain("org1Key");
|
||||
|
||||
const org2Key = decryptionKeys.orgKeys[org2Id];
|
||||
expect(org2Key).not.toBeNull();
|
||||
expect(org2Key.keyB64).toContain("provider1Key");
|
||||
});
|
||||
|
||||
it("returns a stream that pays attention to updates of all data", async () => {
|
||||
// Start listening until there have been 6 emissions
|
||||
const promise = lastValueFrom(
|
||||
cryptoService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)),
|
||||
);
|
||||
|
||||
// User has their UserKey set
|
||||
const initialUserKey = makeSymmetricCryptoKey<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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,18 @@
|
||||
import * as bigInt from "big-integer";
|
||||
import { Observable, combineLatest, filter, firstValueFrom, map, zip } from "rxjs";
|
||||
import {
|
||||
NEVER,
|
||||
Observable,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
forkJoin,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key";
|
||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||
@ -25,55 +35,37 @@ import {
|
||||
} from "../../types/key";
|
||||
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import {
|
||||
CipherDecryptionKeys,
|
||||
CryptoService as CryptoServiceAbstraction,
|
||||
} from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { convertValues } from "../misc/convert-values";
|
||||
import { EFFLongWordList } from "../misc/wordlist";
|
||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString, EncryptedString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, DerivedState, StateProvider } from "../state";
|
||||
import { ActiveUserState, StateProvider } from "../state";
|
||||
|
||||
import {
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
USER_ORGANIZATION_KEYS,
|
||||
} from "./key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./key-state/provider-keys.state";
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state";
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_PRIVATE_KEY,
|
||||
USER_PUBLIC_KEY,
|
||||
USER_KEY,
|
||||
} from "./key-state/user-key.state";
|
||||
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
private readonly activeUserKeyState: ActiveUserState<UserKey>;
|
||||
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
|
||||
private readonly activeUserEncryptedOrgKeysState: ActiveUserState<
|
||||
Record<OrganizationId, EncryptedOrganizationKeyData>
|
||||
>;
|
||||
private readonly activeUserOrgKeysState: DerivedState<Record<OrganizationId, OrgKey>>;
|
||||
private readonly activeUserEncryptedProviderKeysState: ActiveUserState<
|
||||
Record<ProviderId, EncryptedString>
|
||||
>;
|
||||
private readonly activeUserProviderKeysState: DerivedState<Record<ProviderId, ProviderKey>>;
|
||||
private readonly activeUserEncryptedPrivateKeyState: ActiveUserState<EncryptedString>;
|
||||
private readonly activeUserPrivateKeyState: DerivedState<UserPrivateKey>;
|
||||
private readonly activeUserPublicKeyState: DerivedState<UserPublicKey>;
|
||||
|
||||
readonly activeUserKey$: Observable<UserKey>;
|
||||
readonly everHadUserKey$: Observable<boolean>;
|
||||
|
||||
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
|
||||
readonly activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
|
||||
readonly activeUserPrivateKey$: Observable<UserPrivateKey>;
|
||||
readonly activeUserPublicKey$: Observable<UserPublicKey>;
|
||||
readonly everHadUserKey$: Observable<boolean>;
|
||||
|
||||
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<void> {
|
||||
@ -157,8 +101,14 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async refreshAdditionalKeys(): Promise<void> {
|
||||
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<UserKey> {
|
||||
@ -399,12 +349,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> {
|
||||
return (await firstValueFrom(this.activeUserOrgKeys$))[orgId];
|
||||
}
|
||||
|
||||
@sequentialize(() => "getOrgKeys")
|
||||
async getOrgKeys(): Promise<Record<string, OrgKey>> {
|
||||
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<T extends OrgKey | UserKey>(
|
||||
@ -438,17 +388,16 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Deprecate in favor of observable
|
||||
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> {
|
||||
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<Record<ProviderId, ProviderKey>> {
|
||||
return await firstValueFrom(this.activeUserProviderKeys$);
|
||||
return providerKeys[providerId] ?? null;
|
||||
}
|
||||
|
||||
private async clearProviderKeys(userId: UserId): Promise<void> {
|
||||
@ -459,13 +408,11 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<Uint8Array> {
|
||||
return await firstValueFrom(this.activeUserPublicKey$);
|
||||
}
|
||||
|
||||
async makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]> {
|
||||
// TODO: Make userId required
|
||||
async makeOrgKey<T extends OrgKey | ProviderKey>(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<Uint8Array> {
|
||||
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<string[]> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId));
|
||||
}
|
||||
|
||||
if (publicKey === null) {
|
||||
throw new Error("No public key available.");
|
||||
}
|
||||
@ -671,16 +627,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
try {
|
||||
const encPrivateKey = await firstValueFrom(
|
||||
this.stateProvider.getUserState$(USER_ENCRYPTED_PRIVATE_KEY, userId),
|
||||
this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$,
|
||||
);
|
||||
|
||||
if (encPrivateKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can decrypt private key
|
||||
const privateKey = await USER_PRIVATE_KEY.derive([encPrivateKey, key], {
|
||||
encryptService: this.encryptService,
|
||||
});
|
||||
const privateKey = await this.decryptPrivateKey(encPrivateKey, key);
|
||||
|
||||
if (privateKey == null) {
|
||||
// failed to decrypt
|
||||
@ -688,9 +643,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
// Can successfully derive public key
|
||||
const publicKey = await USER_PUBLIC_KEY.derive(privateKey, {
|
||||
cryptoFunctionService: this.cryptoFunctionService,
|
||||
});
|
||||
const publicKey = await this.derivePublicKey(privateKey);
|
||||
|
||||
if (publicKey == null) {
|
||||
// failed to decrypt
|
||||
@ -712,8 +665,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
publicKey: string;
|
||||
privateKey: EncString;
|
||||
}> {
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
if (activeUserId == null) {
|
||||
throw new Error("Cannot initilize an account if one is not active.");
|
||||
}
|
||||
|
||||
// Verify user key doesn't exist
|
||||
const existingUserKey = await this.getUserKey();
|
||||
const existingUserKey = await this.getUserKey(activeUserId);
|
||||
|
||||
if (existingUserKey != null) {
|
||||
this.logService.error("Tried to initialize account with existing user key.");
|
||||
throw new Error("Cannot initialize account, keys already exist.");
|
||||
@ -721,8 +681,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||
await this.setUserKey(userKey);
|
||||
await this.activeUserEncryptedPrivateKeyState.update(() => privateKey.encryptedString);
|
||||
await this.setUserKey(userKey, activeUserId);
|
||||
await this.stateProvider
|
||||
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.update(() => privateKey.encryptedString);
|
||||
|
||||
return {
|
||||
userKey,
|
||||
@ -925,4 +887,178 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
return this.encryptService.decryptToBytes(encBuffer, key);
|
||||
}
|
||||
|
||||
userKey$(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, USER_KEY).state$;
|
||||
}
|
||||
|
||||
private userKeyWithLegacySupport$(userId: UserId) {
|
||||
return this.userKey$(userId).pipe(
|
||||
switchMap((userKey) => {
|
||||
if (userKey != null) {
|
||||
return of(userKey);
|
||||
}
|
||||
|
||||
// Legacy path
|
||||
return this.masterPasswordService.masterKey$(userId).pipe(
|
||||
switchMap(async (masterKey) => {
|
||||
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) {
|
||||
// We don't have a UserKey or a valid MasterKey
|
||||
return null;
|
||||
}
|
||||
|
||||
// The master key is valid meaning, the org keys and such are encrypted with this key
|
||||
return masterKey as unknown as UserKey;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
userPublicKey$(userId: UserId) {
|
||||
return this.userPrivateKey$(userId).pipe(
|
||||
switchMap(async (pk) => await this.derivePublicKey(pk)),
|
||||
);
|
||||
}
|
||||
|
||||
private async derivePublicKey(privateKey: UserPrivateKey) {
|
||||
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||
}
|
||||
|
||||
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> {
|
||||
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<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) {
|
||||
return this.cipherDecryptionKeys$(userId).pipe(map((keys) => keys?.orgKeys));
|
||||
}
|
||||
|
||||
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 })),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { makeEncString } from "../../../../spec";
|
||||
|
||||
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||
import { OrgKey, UserPrivateKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ORGANIZATION_KEYS } from "./org-keys.state";
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./org-keys.state";
|
||||
|
||||
describe("encrypted org keys", () => {
|
||||
const sut = USER_ENCRYPTED_ORGANIZATION_KEYS;
|
||||
@ -28,85 +23,3 @@ describe("encrypted org keys", () => {
|
||||
expect(result).toEqual(encryptedOrgKeys);
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived decrypted org keys", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const userPrivateKey = makeStaticByteArray(64, 3) as UserPrivateKey;
|
||||
const sut = USER_ORGANIZATION_KEYS;
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should deserialize org keys", async () => {
|
||||
const decryptedOrgKeys = {
|
||||
"org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey,
|
||||
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
|
||||
};
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedOrgKeys)));
|
||||
|
||||
expect(result).toEqual(decryptedOrgKeys);
|
||||
});
|
||||
|
||||
it("should derive org keys", async () => {
|
||||
const encryptedOrgKeys = {
|
||||
"org-id-1": {
|
||||
type: "organization",
|
||||
key: makeEncString().encryptedString,
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "organization",
|
||||
key: makeEncString().encryptedString,
|
||||
},
|
||||
};
|
||||
|
||||
const decryptedOrgKeys = {
|
||||
"org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey,
|
||||
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
|
||||
};
|
||||
|
||||
// TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
|
||||
|
||||
const result = await sut.derive([encryptedOrgKeys, userPrivateKey, {}], { encryptService });
|
||||
|
||||
expect(result).toEqual(decryptedOrgKeys);
|
||||
});
|
||||
|
||||
it("should derive org keys from providers", async () => {
|
||||
const encryptedOrgKeys = {
|
||||
"org-id-1": {
|
||||
type: "provider",
|
||||
key: makeEncString().encryptedString,
|
||||
providerId: "provider-id-1",
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "provider",
|
||||
key: makeEncString().encryptedString,
|
||||
providerId: "provider-id-2",
|
||||
},
|
||||
};
|
||||
|
||||
const providerKeys = {
|
||||
"provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)),
|
||||
"provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)),
|
||||
};
|
||||
|
||||
const decryptedOrgKeys = {
|
||||
"org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey,
|
||||
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
|
||||
};
|
||||
|
||||
// TODO: How to not have to mock these decryptions. They are internal concerns of ProviderEncryptedOrganizationKey
|
||||
encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
|
||||
encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
|
||||
|
||||
const result = await sut.derive([encryptedOrgKeys, userPrivateKey, providerKeys], {
|
||||
encryptService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(decryptedOrgKeys);
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { BaseEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key";
|
||||
import { OrganizationId, ProviderId } from "../../../types/guid";
|
||||
import { OrgKey, ProviderKey, UserPrivateKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state";
|
||||
import { OrganizationId } from "../../../types/guid";
|
||||
import { CRYPTO_DISK, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record<
|
||||
EncryptedOrganizationKeyData,
|
||||
@ -13,42 +9,3 @@ export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record<
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
export const USER_ORGANIZATION_KEYS = new DeriveDefinition<
|
||||
[
|
||||
Record<OrganizationId, EncryptedOrganizationKeyData>,
|
||||
UserPrivateKey,
|
||||
Record<ProviderId, ProviderKey>,
|
||||
],
|
||||
Record<OrganizationId, OrgKey>,
|
||||
{ encryptService: EncryptService }
|
||||
>(CRYPTO_MEMORY, "organizationKeys", {
|
||||
deserializer: (obj) => {
|
||||
const result: Record<OrganizationId, OrgKey> = {};
|
||||
for (const orgId of Object.keys(obj ?? {}) as OrganizationId[]) {
|
||||
result[orgId] = SymmetricCryptoKey.fromJSON(obj[orgId]) as OrgKey;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
derive: async ([encryptedOrgKeys, privateKey, providerKeys], { encryptService }) => {
|
||||
const result: Record<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(encryptService, providerKeys);
|
||||
} else {
|
||||
decrypted = await encrypted.decrypt(encryptService, privateKey);
|
||||
}
|
||||
|
||||
result[orgId] = decrypted;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
@ -1,13 +1,6 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { makeEncString } from "../../../../spec";
|
||||
|
||||
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||
import { ProviderId } from "../../../types/guid";
|
||||
import { ProviderKey, UserPrivateKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./provider-keys.state";
|
||||
|
||||
describe("encrypted provider keys", () => {
|
||||
const sut = USER_ENCRYPTED_PROVIDER_KEYS;
|
||||
@ -23,51 +16,3 @@ describe("encrypted provider keys", () => {
|
||||
expect(result).toEqual(encryptedProviderKeys);
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived decrypted provider keys", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const userPrivateKey = makeStaticByteArray(64, 0) as UserPrivateKey;
|
||||
const sut = USER_PROVIDER_KEYS;
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should deserialize provider keys", async () => {
|
||||
const decryptedProviderKeys = {
|
||||
"provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as ProviderKey,
|
||||
"provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as ProviderKey,
|
||||
};
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedProviderKeys)));
|
||||
|
||||
expect(result).toEqual(decryptedProviderKeys);
|
||||
});
|
||||
|
||||
it("should derive provider keys", async () => {
|
||||
const encryptedProviderKeys = {
|
||||
"provider-id-1": makeEncString().encryptedString,
|
||||
"provider-id-2": makeEncString().encryptedString,
|
||||
};
|
||||
|
||||
const decryptedProviderKeys = {
|
||||
"provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as ProviderKey,
|
||||
"provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as ProviderKey,
|
||||
};
|
||||
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-1"].key);
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-2"].key);
|
||||
|
||||
const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService });
|
||||
|
||||
expect(result).toEqual(decryptedProviderKeys);
|
||||
});
|
||||
|
||||
it("should handle null input values", async () => {
|
||||
const encryptedProviderKeys: Record<ProviderId, EncryptedString> = null;
|
||||
|
||||
const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService });
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { ProviderId } from "../../../types/guid";
|
||||
import { ProviderKey, UserPrivateKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
import { CRYPTO_DISK, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedString, ProviderId>(
|
||||
CRYPTO_DISK,
|
||||
@ -13,32 +10,3 @@ export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedSt
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_PROVIDER_KEYS = new DeriveDefinition<
|
||||
[Record<ProviderId, EncryptedString>, UserPrivateKey],
|
||||
Record<ProviderId, ProviderKey>,
|
||||
{ encryptService: EncryptService }
|
||||
>(CRYPTO_MEMORY, "providerKeys", {
|
||||
deserializer: (obj) => {
|
||||
const result: Record<ProviderId, ProviderKey> = {};
|
||||
for (const providerId of Object.keys(obj ?? {}) as ProviderId[]) {
|
||||
result[providerId] = SymmetricCryptoKey.fromJSON(obj[providerId]) as ProviderKey;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
derive: async ([encryptedProviderKeys, privateKey], { encryptService }) => {
|
||||
const result: Record<ProviderId, ProviderKey> = {};
|
||||
for (const providerId of Object.keys(encryptedProviderKeys ?? {}) as ProviderId[]) {
|
||||
if (result[providerId] != null) {
|
||||
continue;
|
||||
}
|
||||
const encrypted = new EncString(encryptedProviderKeys[providerId]);
|
||||
const decrypted = await encryptService.rsaDecrypt(encrypted, privateKey);
|
||||
const providerKey = new SymmetricCryptoKey(decrypted) as ProviderKey;
|
||||
|
||||
result[providerId] = providerKey;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
@ -1,19 +1,8 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { EncString } from "../../models/domain/enc-string";
|
||||
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_PRIVATE_KEY,
|
||||
USER_PUBLIC_KEY,
|
||||
} from "./user-key.state";
|
||||
import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY } from "./user-key.state";
|
||||
|
||||
function makeEncString(data?: string) {
|
||||
data ??= Utils.newGuid();
|
||||
@ -43,76 +32,3 @@ describe("Encrypted private key", () => {
|
||||
expect(result).toEqual(encryptedPrivateKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User public key", () => {
|
||||
const sut = USER_PUBLIC_KEY;
|
||||
const userPrivateKey = makeStaticByteArray(64, 1) as UserPrivateKey;
|
||||
const userPublicKey = makeStaticByteArray(64, 2) as UserPublicKey;
|
||||
|
||||
it("should deserialize user public key", () => {
|
||||
const userPublicKey = makeStaticByteArray(64, 1);
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(userPublicKey)));
|
||||
|
||||
expect(result).toEqual(userPublicKey);
|
||||
});
|
||||
|
||||
it("should derive user public key", async () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(userPublicKey);
|
||||
|
||||
const result = await sut.derive(userPrivateKey, { cryptoFunctionService });
|
||||
|
||||
expect(result).toEqual(userPublicKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Derived decrypted private key", () => {
|
||||
const sut = USER_PRIVATE_KEY;
|
||||
const userKey = mock<UserKey>();
|
||||
const encryptedPrivateKey = makeEncString().encryptedString;
|
||||
const decryptedPrivateKey = makeStaticByteArray(64, 1);
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should deserialize decrypted private key", () => {
|
||||
const decryptedPrivateKey = makeStaticByteArray(64, 1);
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedPrivateKey)));
|
||||
|
||||
expect(result).toEqual(decryptedPrivateKey);
|
||||
});
|
||||
|
||||
it("should derive decrypted private key", async () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey);
|
||||
|
||||
const result = await sut.derive([encryptedPrivateKey, userKey], {
|
||||
encryptService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(decryptedPrivateKey);
|
||||
});
|
||||
|
||||
it("should handle null encryptedPrivateKey", async () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
const result = await sut.derive([null, userKey], {
|
||||
encryptService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it("should handle null userKey", async () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
const result = await sut.derive([encryptedPrivateKey, null], {
|
||||
encryptService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state";
|
||||
import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition<boolean>(
|
||||
CRYPTO_DISK,
|
||||
@ -23,41 +21,6 @@ export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_PRIVATE_KEY = new DeriveDefinition<
|
||||
[EncryptedString, UserKey],
|
||||
UserPrivateKey,
|
||||
{ encryptService: EncryptService }
|
||||
>(CRYPTO_MEMORY, "privateKey", {
|
||||
deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey,
|
||||
derive: async ([encPrivateKeyString, userKey], { encryptService }) => {
|
||||
if (encPrivateKeyString == null || userKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encPrivateKey = new EncString(encPrivateKeyString);
|
||||
const privateKey = (await encryptService.decryptToBytes(
|
||||
encPrivateKey,
|
||||
userKey,
|
||||
)) as UserPrivateKey;
|
||||
return privateKey;
|
||||
},
|
||||
});
|
||||
|
||||
export const USER_PUBLIC_KEY = DeriveDefinition.from<
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
{ cryptoFunctionService: CryptoFunctionService }
|
||||
>([USER_PRIVATE_KEY, "publicKey"], {
|
||||
deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPublicKey,
|
||||
derive: async (privateKey, { cryptoFunctionService }) => {
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||
},
|
||||
});
|
||||
|
||||
export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
|
||||
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
|
||||
clearOn: ["logout", "lock"],
|
||||
|
@ -135,8 +135,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
|
||||
}
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[]) {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
async setDecryptedCipherCache(value: CipherView[], userId: UserId) {
|
||||
// Sometimes we might prematurely decrypt the vault and that will result in no ciphers
|
||||
// if we cache it then we may accidentally return it when it's not right, we'd rather try decryption again.
|
||||
// We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption.
|
||||
@ -367,9 +366,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return await this.getDecryptedCiphers();
|
||||
}
|
||||
|
||||
decCiphers = await this.decryptCiphers(await this.getAll());
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
if (activeUserId == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId);
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers, activeUserId);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
@ -377,10 +382,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return Object.values(await firstValueFrom(this.cipherViews$));
|
||||
}
|
||||
|
||||
private async decryptCiphers(ciphers: Cipher[]) {
|
||||
const orgKeys = await this.cryptoService.getOrgKeys();
|
||||
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
|
||||
if (Object.keys(orgKeys).length === 0 && userKey == null) {
|
||||
private async decryptCiphers(ciphers: Cipher[], userId: UserId) {
|
||||
const keys = await firstValueFrom(this.cryptoService.cipherDecryptionKeys$(userId, true));
|
||||
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
// return early if there are no keys to decrypt with
|
||||
return;
|
||||
}
|
||||
@ -398,7 +403,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const decCiphers = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(([orgId, groupedCiphers]) =>
|
||||
this.encryptService.decryptItems(groupedCiphers, orgKeys[orgId] ?? userKey),
|
||||
this.encryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user