Update & Test CryptoService Changes

This commit is contained in:
Justin Baur 2024-05-24 16:40:11 -04:00
parent cde634fbf4
commit 3c6a885f87
No known key found for this signature in database
3 changed files with 657 additions and 138 deletions

View File

@ -4,16 +4,42 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key";
import { OrganizationId, UserId } from "../../types/guid";
import {
UserKey,
MasterKey,
OrgKey,
ProviderKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../types/key";
import { KeySuffixOptions, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class CryptoService {
abstract activeUserKey$: Observable<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.
* @param userId
*/
abstract userKey$(userId: UserId): Observable<UserKey>;
/**
* Returns the an observable key for the given user id.
*
@ -46,6 +72,8 @@ export abstract class CryptoService {
* Retrieves the user key
* @param userId The desired user
* @returns The user key
*
* @deprecated Use {@link userKey$} with a required {@link UserId} instead.
*/
abstract getUserKey(userId?: string): Promise<UserKey>;
@ -174,19 +202,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 +223,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 +235,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 +252,20 @@ export abstract class CryptoService {
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
* @returns The user's private key
*
* @deprecated Use {@link userPrivateKey$} instead.
*/
abstract getPrivateKey(): Promise<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 a
* 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
@ -345,4 +370,30 @@ 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`.
*/
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 key if they are unlocked, or null if the user is not unlocked.
*/
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.
*/
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey>;
}

View File

@ -1,15 +1,22 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, of, tap } from "rxjs";
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import {
awaitAsync,
makeEncString,
makeStaticByteArray,
makeSymmetricCryptoKey,
} from "../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state";
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";
import { OrganizationId, UserId } from "../../types/guid";
import { UserKey, MasterKey } from "../../types/key";
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
@ -18,8 +25,9 @@ import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { Encrypted } from "../interfaces/encrypted";
import { Utils } from "../misc/utils";
import { EncString } from "../models/domain/enc-string";
import { EncString, EncryptedString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { CryptoService } from "../services/crypto.service";
import { UserKeyDefinition } from "../state";
@ -340,4 +348,327 @@ describe("cryptoService", () => {
});
});
});
describe("userPrivateKey$", () => {
const setupKeys = ({
makeMasterKey,
makeUserKey,
}: {
makeMasterKey: boolean;
makeUserKey: boolean;
}): [UserKey, MasterKey] => {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<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 legacy support set to %legacySupport and user hasUserKey = $hasUserKey and user hasMasterKey = $hasMasterKey", async () => {
const [userKey] = setupKeys({
makeMasterKey: false,
makeUserKey: true,
});
const userEncryptedPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.stateSubject.next([
mockUserId,
fakeEncryptedUserPrivateKey.encryptedString,
]);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
encryptService.decryptToBytes.mockResolvedValue(fakeDecryptedUserPrivateKey);
const fakeUserPublicKey = makeStaticByteArray(10, 2);
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(
fakeEncryptedUserPrivateKey,
userKey,
);
expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey);
});
it("returns null user private key when no user key is found", async () => {
setupKeys({ makeMasterKey: false, makeUserKey: false });
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
expect(userPrivateKey).toBeFalsy();
});
it("returns null when user does not have a private key set", async () => {
setupKeys({ makeUserKey: true, makeMasterKey: false });
const encryptedUserPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(userPrivateKey).toBeFalsy();
});
});
describe("cipherDecryptionKeys$", () => {
const fakePrivateKeyDecryption = (encryptedPrivateKey: Encrypted, key: SymmetricCryptoKey) => {
const output = new Uint8Array(64);
output.set(encryptedPrivateKey.dataBytes);
output.set(
key.key.subarray(0, 64 - encryptedPrivateKey.dataBytes.length),
encryptedPrivateKey.dataBytes.length,
);
return output;
};
const fakeOrgKeyDecryption = (encryptedString: EncString, userPrivateKey: Uint8Array) => {
const output = new Uint8Array(64);
output.set(encryptedString.dataBytes);
output.set(
userPrivateKey.subarray(0, 64 - encryptedString.dataBytes.length),
encryptedString.dataBytes.length,
);
return output;
};
const org1Id = "org1" as OrganizationId;
const updateKeys = (
keys: Partial<{
userKey: UserKey;
encryptedPrivateKey: EncString;
orgKeys: Record<string, EncryptedOrganizationKeyData>;
providerKeys: Record<string, EncryptedString>;
}> = {},
) => {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.stateSubject.next([mockUserId, keys.userKey]);
}
if ("encryptedPrivateKey" in keys) {
const userEncryptedPrivateKey = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.stateSubject.next([
mockUserId,
keys.encryptedPrivateKey.encryptedString,
]);
}
if ("orgKeys" in keys) {
const orgKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]);
}
if ("providerKeys" in keys) {
const providerKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]);
}
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {
// TOOD: Branch between provider and private key?
return Promise.resolve(fakePrivateKeyDecryption(encryptedPrivateKey, userKey));
});
encryptService.rsaDecrypt.mockImplementation((data, privateKey) => {
return Promise.resolve(fakeOrgKeyDecryption(data, privateKey));
});
};
it("returns decryption keys when there are not org or provider keys set", async () => {
updateKeys({
userKey: makeSymmetricCryptoKey<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 5 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(),
},
});
});
});
});

View File

@ -1,8 +1,18 @@
import * as bigInt from "big-integer";
import { Observable, combineLatest, filter, firstValueFrom, map, zip } from "rxjs";
import {
NEVER,
Observable,
combineLatest,
firstValueFrom,
forkJoin,
map,
of,
switchMap,
} from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
@ -25,55 +35,37 @@ import {
} from "../../types/key";
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
import {
CipherDecryptionKeys,
CryptoService as CryptoServiceAbstraction,
} from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums";
import { sequentialize } from "../misc/sequentialize";
import { convertValues } from "../misc/convertValues";
import { EFFLongWordList } from "../misc/wordlist";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString, EncryptedString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { ActiveUserState, DerivedState, StateProvider } from "../state";
import { ActiveUserState, StateProvider } from "../state";
import {
USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ORGANIZATION_KEYS,
} from "./key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./key-state/provider-keys.state";
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_PRIVATE_KEY,
USER_PUBLIC_KEY,
USER_KEY,
} from "./key-state/user-key.state";
export class CryptoService implements CryptoServiceAbstraction {
private readonly activeUserKeyState: ActiveUserState<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,179 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.encryptService.decryptToBytes(encBuffer, key);
}
userKey$(userId: UserId) {
return this.stateProvider.getUser(userId, USER_KEY).state$;
}
private userKeyWithLegacySupport$(userId: UserId) {
return this.userKey$(userId).pipe(
switchMap((userKey) => {
if (userKey != null) {
return of(userKey);
}
// Legacy path
return this.masterPasswordService.masterKey$(userId).pipe(
switchMap(async (masterKey) => {
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) {
// We don't have a UserKey or a valid MasterKey
return null;
}
// The master key is valid meaning, the org keys and such are encrypted with this key
return masterKey as unknown as UserKey;
}),
);
}),
);
}
// Not exposing this until there is a need
private userPublicKey$(userId: UserId) {
return this.userPrivateKey$(userId).pipe(
switchMap(async (pk) => await this.derivePublicKey(pk)),
);
}
private async derivePublicKey(privateKey: UserPrivateKey) {
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}
userPrivateKey$(userId: UserId): Observable<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 })),
);
}),
);
}
}