diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 1d54726727..6de3fd9d8b 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => { } as EmergencyAccessTakeoverResponse); const mockDecryptedGrantorUserKey = new Uint8Array(64); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey); const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; @@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => { kdf: KdfType.PBKDF2_SHA256, kdfIterations: 500, } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); await expect( emergencyAccessService.takeover(mockId, mockEmail, mockName), @@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => { expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); }); + + it("should throw an error if the users private key cannot be retrieved", async () => { + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({ + keyEncrypted: "EncryptedKey", + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 500, + } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(null); + + await expect(emergencyAccessService.takeover(mockId, mockEmail, mockName)).rejects.toThrow( + "user does not have a private key", + ); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); }); describe("getRotatedKeys", () => { diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index dbc1ce820c..819b80c1ad 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -209,7 +209,16 @@ export class EmergencyAccessService { async getViewOnlyCiphers(id: string): Promise { const response = await this.emergencyAccessApiService.postEmergencyAccessView(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot get view only ciphers."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + response.keyEncrypted, + activeUserPrivateKey, + ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; const ciphers = await this.encryptService.decryptItems( @@ -229,7 +238,16 @@ export class EmergencyAccessService { async takeover(id: string, masterPassword: string, email: string) { const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot complete a takeover."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + takeoverResponse.keyEncrypted, + activeUserPrivateKey, + ); if (grantorKeyBuffer == null) { throw new Error("Failed to decrypt grantor key"); } diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 24f6d0e9c7..470fa2317e 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -25,7 +25,13 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey { constructor(private key: string) {} async decrypt(cryptoService: CryptoService) { - const decValue = await cryptoService.rsaDecrypt(this.key); + const activeUserPrivateKey = await cryptoService.getPrivateKey(); + + 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; } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 14baee0d12..0a210e6709 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -311,15 +311,17 @@ export abstract class CryptoService { * @param data The data to encrypt * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used * @returns The encrypted data + * @throws If the given publicKey is a null-ish value. */ - abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; /** * Decrypts a value using RSA. * @param encValue The encrypted value to decrypt - * @param privateKeyValue The private key to use for decryption + * @param privateKey The private key to use for decryption * @returns The decrypted value + * @throws If the given privateKey is a null-ish value. */ - abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise; + abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise; abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index b90fab6b49..6d9574ef38 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -621,19 +621,20 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); } - async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise { + async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise { if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey == null) { - throw new Error("Public key unavailable."); + throw new Error("'publicKey' is a required parameter and must be non-null"); } const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); } - async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise { + async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise { + if (privateKey == null) { + throw new Error("'privateKey' is a required parameter and must be non-null"); + } + const headerPieces = encValue.split("."); let encType: EncryptionType = null; let encPieces: string[]; @@ -665,10 +666,6 @@ export class CryptoService implements CryptoServiceAbstraction { } const data = Utils.fromB64ToArray(encPieces[0]); - const privateKey = privateKeyValue ?? (await this.getPrivateKey()); - if (privateKey == null) { - throw new Error("No private key."); - } let alg: "sha1" | "sha256" = "sha1"; switch (encType) { diff --git a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts index dd1e4a685e..6b547a491a 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts @@ -65,6 +65,10 @@ describe("derived decrypted org keys", () => { "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 cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);