1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-11 13:30:39 +01:00

[PM-8155] Keep crypto derive dependencies in lockstep (#9191)

* Keep derive dependencies in lockstep

This reduces emissions in general due to updates of multiple inputs and removes decryption errors due to partially updated dependencies

* Fix provider encrypted org keys

* Fix provider state test types

* Type fixes
This commit is contained in:
Matt Gibson 2024-05-15 17:40:16 -04:00 committed by GitHub
parent c19a640557
commit 4ccf920da8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 112 additions and 103 deletions

View File

@ -1,11 +1,11 @@
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { OrgKey } from "../../../types/key"; import { OrgKey, UserPrivateKey } from "../../../types/key";
import { EncryptedOrganizationKeyData } from "../data/encrypted-organization-key.data"; import { EncryptedOrganizationKeyData } from "../data/encrypted-organization-key.data";
export abstract class BaseEncryptedOrganizationKey { export abstract class BaseEncryptedOrganizationKey {
decrypt: (cryptoService: CryptoService) => Promise<SymmetricCryptoKey>; abstract get encryptedOrganizationKey(): EncString;
static fromData(data: EncryptedOrganizationKeyData) { static fromData(data: EncryptedOrganizationKeyData) {
switch (data.type) { switch (data.type) {
@ -19,22 +19,26 @@ export abstract class BaseEncryptedOrganizationKey {
return null; return null;
} }
} }
static isProviderEncrypted(
key: EncryptedOrganizationKey | ProviderEncryptedOrganizationKey,
): key is ProviderEncryptedOrganizationKey {
return key.toData().type === "provider";
}
} }
export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey { export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
constructor(private key: string) {} constructor(private key: string) {}
async decrypt(cryptoService: CryptoService) { async decrypt(encryptService: EncryptService, privateKey: UserPrivateKey) {
const activeUserPrivateKey = await cryptoService.getPrivateKey(); const decValue = await encryptService.rsaDecrypt(this.encryptedOrganizationKey, privateKey);
if (activeUserPrivateKey == null) {
throw new Error("Active user does not have a private key, cannot decrypt organization key.");
}
const decValue = await cryptoService.rsaDecrypt(this.key, activeUserPrivateKey);
return new SymmetricCryptoKey(decValue) as OrgKey; return new SymmetricCryptoKey(decValue) as OrgKey;
} }
get encryptedOrganizationKey() {
return new EncString(this.key);
}
toData(): EncryptedOrganizationKeyData { toData(): EncryptedOrganizationKeyData {
return { return {
type: "organization", type: "organization",
@ -49,12 +53,18 @@ export class ProviderEncryptedOrganizationKey implements BaseEncryptedOrganizati
private providerId: string, private providerId: string,
) {} ) {}
async decrypt(cryptoService: CryptoService) { async decrypt(encryptService: EncryptService, providerKeys: Record<string, SymmetricCryptoKey>) {
const providerKey = await cryptoService.getProviderKey(this.providerId); const decValue = await encryptService.decryptToBytes(
const decValue = await cryptoService.decryptToBytes(new EncString(this.key), providerKey); new EncString(this.key),
providerKeys[this.providerId],
);
return new SymmetricCryptoKey(decValue) as OrgKey; return new SymmetricCryptoKey(decValue) as OrgKey;
} }
get encryptedOrganizationKey() {
return new EncString(this.key);
}
toData(): EncryptedOrganizationKeyData { toData(): EncryptedOrganizationKeyData {
return { return {
type: "provider", type: "provider",

View File

@ -1,5 +1,5 @@
import * as bigInt from "big-integer"; import * as bigInt from "big-integer";
import { Observable, filter, firstValueFrom, map } from "rxjs"; import { Observable, filter, firstValueFrom, map, zip } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
@ -97,13 +97,12 @@ export class CryptoService implements CryptoServiceAbstraction {
// User Asymmetric Key Pair // User Asymmetric Key Pair
this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY); this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY);
this.activeUserPrivateKeyState = stateProvider.getDerived( this.activeUserPrivateKeyState = stateProvider.getDerived(
this.activeUserEncryptedPrivateKeyState.combinedState$.pipe( zip(this.activeUserEncryptedPrivateKeyState.state$, this.activeUserKey$).pipe(
filter(([_userId, key]) => key != null), filter(([, userKey]) => !!userKey),
), ),
USER_PRIVATE_KEY, USER_PRIVATE_KEY,
{ {
encryptService: this.encryptService, encryptService: this.encryptService,
getUserKey: (userId) => this.getUserKey(userId),
}, },
); );
this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null
@ -116,27 +115,34 @@ export class CryptoService implements CryptoServiceAbstraction {
); );
this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null
// Organization keys
this.activeUserEncryptedOrgKeysState = stateProvider.getActive(
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
this.activeUserOrgKeysState = stateProvider.getDerived(
this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)),
USER_ORGANIZATION_KEYS,
{ cryptoService: this },
);
this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function
// Provider keys // Provider keys
this.activeUserEncryptedProviderKeysState = stateProvider.getActive( this.activeUserEncryptedProviderKeysState = stateProvider.getActive(
USER_ENCRYPTED_PROVIDER_KEYS, USER_ENCRYPTED_PROVIDER_KEYS,
); );
this.activeUserProviderKeysState = stateProvider.getDerived( this.activeUserProviderKeysState = stateProvider.getDerived(
this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), zip(
this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)),
this.activeUserPrivateKey$,
).pipe(filter(([, privateKey]) => !!privateKey)),
USER_PROVIDER_KEYS, USER_PROVIDER_KEYS,
{ encryptService: this.encryptService, cryptoService: this }, { encryptService: this.encryptService },
); );
this.activeUserProviderKeys$ = this.activeUserProviderKeysState.state$; // null handled by `derive` function 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> { async setUserKey(key: UserKey, userId?: UserId): Promise<void> {
@ -656,17 +662,14 @@ export class CryptoService implements CryptoServiceAbstraction {
} }
try { try {
const [userId, encPrivateKey] = await firstValueFrom( const encPrivateKey = await firstValueFrom(this.activeUserEncryptedPrivateKeyState.state$);
this.activeUserEncryptedPrivateKeyState.combinedState$,
);
if (encPrivateKey == null) { if (encPrivateKey == null) {
return false; return false;
} }
// Can decrypt private key // Can decrypt private key
const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], { const privateKey = await USER_PRIVATE_KEY.derive([encPrivateKey, key], {
encryptService: this.encryptService, encryptService: this.encryptService,
getUserKey: () => Promise.resolve(key),
}); });
if (privateKey == null) { if (privateKey == null) {

View File

@ -1,8 +1,8 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { makeEncString, makeStaticByteArray } from "../../../../spec";
import { OrgKey } from "../../../types/key"; import { OrgKey, UserPrivateKey } from "../../../types/key";
import { CryptoService } from "../../abstractions/crypto.service"; import { EncryptService } from "../../abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; 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, USER_ORGANIZATION_KEYS } from "./org-keys.state";
@ -30,7 +30,8 @@ describe("encrypted org keys", () => {
}); });
describe("derived decrypted org keys", () => { describe("derived decrypted org keys", () => {
const cryptoService = mock<CryptoService>(); const encryptService = mock<EncryptService>();
const userPrivateKey = makeStaticByteArray(64, 3) as UserPrivateKey;
const sut = USER_ORGANIZATION_KEYS; const sut = USER_ORGANIZATION_KEYS;
afterEach(() => { afterEach(() => {
@ -65,15 +66,11 @@ describe("derived decrypted org keys", () => {
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
}; };
const userPrivateKey = makeStaticByteArray(64, 3);
cryptoService.getPrivateKey.mockResolvedValue(userPrivateKey);
// TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey // TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
const result = await sut.derive(encryptedOrgKeys, { cryptoService }); const result = await sut.derive([encryptedOrgKeys, userPrivateKey, {}], { encryptService });
expect(result).toEqual(decryptedOrgKeys); expect(result).toEqual(decryptedOrgKeys);
}); });
@ -92,16 +89,23 @@ describe("derived decrypted org keys", () => {
}, },
}; };
const providerKeys = {
"provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)),
"provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)),
};
const decryptedOrgKeys = { const decryptedOrgKeys = {
"org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey, "org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey,
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) 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 // TODO: How to not have to mock these decryptions. They are internal concerns of ProviderEncryptedOrganizationKey
cryptoService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
cryptoService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
const result = await sut.derive(encryptedOrgKeys, { cryptoService }); const result = await sut.derive([encryptedOrgKeys, userPrivateKey, providerKeys], {
encryptService,
});
expect(result).toEqual(decryptedOrgKeys); expect(result).toEqual(decryptedOrgKeys);
}); });

View File

@ -1,10 +1,10 @@
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
import { BaseEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key"; import { BaseEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key";
import { OrganizationId } from "../../../types/guid"; import { OrganizationId, ProviderId } from "../../../types/guid";
import { OrgKey } from "../../../types/key"; import { OrgKey, ProviderKey, UserPrivateKey } from "../../../types/key";
import { CryptoService } from "../../abstractions/crypto.service"; import { EncryptService } from "../../abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state";
export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record<
EncryptedOrganizationKeyData, EncryptedOrganizationKeyData,
@ -14,11 +14,15 @@ export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record<
clearOn: ["logout"], clearOn: ["logout"],
}); });
export const USER_ORGANIZATION_KEYS = DeriveDefinition.from< export const USER_ORGANIZATION_KEYS = new DeriveDefinition<
Record<OrganizationId, EncryptedOrganizationKeyData>, [
Record<OrganizationId, EncryptedOrganizationKeyData>,
UserPrivateKey,
Record<ProviderId, ProviderKey>,
],
Record<OrganizationId, OrgKey>, Record<OrganizationId, OrgKey>,
{ cryptoService: CryptoService } { encryptService: EncryptService }
>(USER_ENCRYPTED_ORGANIZATION_KEYS, { >(CRYPTO_MEMORY, "organizationKeys", {
deserializer: (obj) => { deserializer: (obj) => {
const result: Record<OrganizationId, OrgKey> = {}; const result: Record<OrganizationId, OrgKey> = {};
for (const orgId of Object.keys(obj ?? {}) as OrganizationId[]) { for (const orgId of Object.keys(obj ?? {}) as OrganizationId[]) {
@ -26,14 +30,21 @@ export const USER_ORGANIZATION_KEYS = DeriveDefinition.from<
} }
return result; return result;
}, },
derive: async (from, { cryptoService }) => { derive: async ([encryptedOrgKeys, privateKey, providerKeys], { encryptService }) => {
const result: Record<OrganizationId, OrgKey> = {}; const result: Record<OrganizationId, OrgKey> = {};
for (const orgId of Object.keys(from ?? {}) as OrganizationId[]) { for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) {
if (result[orgId] != null) { if (result[orgId] != null) {
continue; continue;
} }
const encrypted = BaseEncryptedOrganizationKey.fromData(from[orgId]); const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]);
const decrypted = await encrypted.decrypt(cryptoService);
let decrypted: OrgKey;
if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) {
decrypted = await encrypted.decrypt(encryptService, providerKeys);
} else {
decrypted = await encrypted.decrypt(encryptService, privateKey);
}
result[orgId] = decrypted; result[orgId] = decrypted;
} }

View File

@ -6,7 +6,6 @@ import { ProviderKey, UserPrivateKey } from "../../../types/key";
import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptService } from "../../abstractions/encrypt.service";
import { EncryptedString } from "../../models/domain/enc-string"; import { EncryptedString } from "../../models/domain/enc-string";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { CryptoService } from "../crypto.service";
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state"; import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state";
@ -27,7 +26,6 @@ describe("encrypted provider keys", () => {
describe("derived decrypted provider keys", () => { describe("derived decrypted provider keys", () => {
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const cryptoService = mock<CryptoService>();
const userPrivateKey = makeStaticByteArray(64, 0) as UserPrivateKey; const userPrivateKey = makeStaticByteArray(64, 0) as UserPrivateKey;
const sut = USER_PROVIDER_KEYS; const sut = USER_PROVIDER_KEYS;
@ -59,9 +57,8 @@ describe("derived decrypted provider keys", () => {
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-1"].key); encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-1"].key);
encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-2"].key); encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-2"].key);
cryptoService.getPrivateKey.mockResolvedValueOnce(userPrivateKey);
const result = await sut.derive(encryptedProviderKeys, { encryptService, cryptoService }); const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService });
expect(result).toEqual(decryptedProviderKeys); expect(result).toEqual(decryptedProviderKeys);
}); });
@ -69,7 +66,7 @@ describe("derived decrypted provider keys", () => {
it("should handle null input values", async () => { it("should handle null input values", async () => {
const encryptedProviderKeys: Record<ProviderId, EncryptedString> = null; const encryptedProviderKeys: Record<ProviderId, EncryptedString> = null;
const result = await sut.derive(encryptedProviderKeys, { encryptService, cryptoService }); const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService });
expect(result).toEqual({}); expect(result).toEqual({});
}); });

View File

@ -1,10 +1,9 @@
import { ProviderId } from "../../../types/guid"; import { ProviderId } from "../../../types/guid";
import { ProviderKey } from "../../../types/key"; import { ProviderKey, UserPrivateKey } from "../../../types/key";
import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptService } from "../../abstractions/encrypt.service";
import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { EncString, EncryptedString } from "../../models/domain/enc-string";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state";
import { CryptoService } from "../crypto.service";
export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedString, ProviderId>( export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedString, ProviderId>(
CRYPTO_DISK, CRYPTO_DISK,
@ -15,11 +14,11 @@ export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedSt
}, },
); );
export const USER_PROVIDER_KEYS = DeriveDefinition.from< export const USER_PROVIDER_KEYS = new DeriveDefinition<
Record<ProviderId, EncryptedString>, [Record<ProviderId, EncryptedString>, UserPrivateKey],
Record<ProviderId, ProviderKey>, Record<ProviderId, ProviderKey>,
{ encryptService: EncryptService; cryptoService: CryptoService } // TODO: This should depend on an active user private key observable directly { encryptService: EncryptService }
>(USER_ENCRYPTED_PROVIDER_KEYS, { >(CRYPTO_MEMORY, "providerKeys", {
deserializer: (obj) => { deserializer: (obj) => {
const result: Record<ProviderId, ProviderKey> = {}; const result: Record<ProviderId, ProviderKey> = {};
for (const providerId of Object.keys(obj ?? {}) as ProviderId[]) { for (const providerId of Object.keys(obj ?? {}) as ProviderId[]) {
@ -27,14 +26,13 @@ export const USER_PROVIDER_KEYS = DeriveDefinition.from<
} }
return result; return result;
}, },
derive: async (from, { encryptService, cryptoService }) => { derive: async ([encryptedProviderKeys, privateKey], { encryptService }) => {
const result: Record<ProviderId, ProviderKey> = {}; const result: Record<ProviderId, ProviderKey> = {};
for (const providerId of Object.keys(from ?? {}) as ProviderId[]) { for (const providerId of Object.keys(encryptedProviderKeys ?? {}) as ProviderId[]) {
if (result[providerId] != null) { if (result[providerId] != null) {
continue; continue;
} }
const encrypted = new EncString(from[providerId]); const encrypted = new EncString(encryptedProviderKeys[providerId]);
const privateKey = await cryptoService.getPrivateKey();
const decrypted = await encryptService.rsaDecrypt(encrypted, privateKey); const decrypted = await encryptService.rsaDecrypt(encrypted, privateKey);
const providerKey = new SymmetricCryptoKey(decrypted) as ProviderKey; const providerKey = new SymmetricCryptoKey(decrypted) as ProviderKey;

View File

@ -1,7 +1,6 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { makeStaticByteArray } from "../../../../spec"; import { makeStaticByteArray } from "../../../../spec";
import { UserId } from "../../../types/guid";
import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key"; import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key";
import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptService } from "../../abstractions/encrypt.service";
@ -70,7 +69,6 @@ describe("User public key", () => {
describe("Derived decrypted private key", () => { describe("Derived decrypted private key", () => {
const sut = USER_PRIVATE_KEY; const sut = USER_PRIVATE_KEY;
const userId = "userId" as UserId;
const userKey = mock<UserKey>(); const userKey = mock<UserKey>();
const encryptedPrivateKey = makeEncString().encryptedString; const encryptedPrivateKey = makeEncString().encryptedString;
const decryptedPrivateKey = makeStaticByteArray(64, 1); const decryptedPrivateKey = makeStaticByteArray(64, 1);
@ -88,37 +86,31 @@ describe("Derived decrypted private key", () => {
}); });
it("should derive decrypted private key", async () => { it("should derive decrypted private key", async () => {
const getUserKey = jest.fn(async () => userKey);
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey); encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey);
const result = await sut.derive([userId, encryptedPrivateKey], { const result = await sut.derive([encryptedPrivateKey, userKey], {
encryptService, encryptService,
getUserKey,
}); });
expect(result).toEqual(decryptedPrivateKey); expect(result).toEqual(decryptedPrivateKey);
}); });
it("should handle null input values", async () => { it("should handle null encryptedPrivateKey", async () => {
const getUserKey = jest.fn(async () => userKey);
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const result = await sut.derive([userId, null], { const result = await sut.derive([null, userKey], {
encryptService, encryptService,
getUserKey,
}); });
expect(result).toEqual(null); expect(result).toEqual(null);
}); });
it("should handle null user key", async () => { it("should handle null userKey", async () => {
const getUserKey = jest.fn(async () => null);
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const result = await sut.derive([userId, encryptedPrivateKey], { const result = await sut.derive([encryptedPrivateKey, null], {
encryptService, encryptService,
getUserKey,
}); });
expect(result).toEqual(null); expect(result).toEqual(null);

View File

@ -1,4 +1,3 @@
import { UserId } from "../../../types/guid";
import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key"; import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key";
import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptService } from "../../abstractions/encrypt.service";
@ -24,20 +23,14 @@ export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>
}, },
); );
export const USER_PRIVATE_KEY = DeriveDefinition.fromWithUserId< export const USER_PRIVATE_KEY = new DeriveDefinition<
EncryptedString, [EncryptedString, UserKey],
UserPrivateKey, UserPrivateKey,
// TODO: update cryptoService to user key directly { encryptService: EncryptService }
{ encryptService: EncryptService; getUserKey: (userId: UserId) => Promise<UserKey> } >(CRYPTO_MEMORY, "privateKey", {
>(USER_ENCRYPTED_PRIVATE_KEY, {
deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey, deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey,
derive: async ([userId, encPrivateKeyString], { encryptService, getUserKey }) => { derive: async ([encPrivateKeyString, userKey], { encryptService }) => {
if (encPrivateKeyString == null) { if (encPrivateKeyString == null || userKey == null) {
return null;
}
const userKey = await getUserKey(userId);
if (userKey == null) {
return null; return null;
} }
@ -64,6 +57,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from<
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}, },
}); });
export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", { export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
clearOn: ["logout", "lock"], clearOn: ["logout", "lock"],