import { describe, expect, it, vi } from "vitest"; import { MAX_TOTAL_PBKDF2_ITERATIONS, decryptEncryptedPkcs8, inspectEncryptedPkcs8, reservePbkdf2Work, } from "../../src/crypto/pbes2"; import { inspectCryptoInput } from "../../src/crypto/inspection"; import { encryptedPbes2Payload, encryptedPkcs8, encryptedPkcs8Pem as pem, } from "../fixtures/pbes2"; describe("PBES2 encrypted PKCS #8", () => { it("enforces one aggregate PBKDF2 work budget", () => { const budget = { iterations: 0 }; reservePbkdf2Work(budget, 10_000_000, "first object"); reservePbkdf2Work(budget, 10_000_000, "second object"); expect(budget.iterations).toBe(MAX_TOTAL_PBKDF2_ITERATIONS); expect(() => reservePbkdf2Work(budget, 1, "third object")).toThrow( /aggregate PBKDF2 safety budget/u, ); }); it("inspects supported parameters and decrypts only with the password", async () => { const bytes = await encryptedPkcs8("correct horse"); expect(inspectEncryptedPkcs8(bytes)).toMatchObject({ scheme: "PBES2", kdf: "PBKDF2", prf: "SHA-256", iterations: 12_000, cipher: "AES-CBC", keyLength: 256, }); await expect(decryptEncryptedPkcs8(bytes, "wrong battery")).rejects.toThrow( /decryption failed/u, ); const decrypted = await decryptEncryptedPkcs8(bytes, "correct horse"); expect(decrypted.key).toMatchObject({ algorithm: "EC", curve: "P-256" }); decrypted.bytes.fill(0); }); it("integrates password-gated decryption into PEM inspection", async () => { const bytes = await encryptedPkcs8("local secret"); const withoutPassword = await inspectCryptoInput(pem(bytes)); expect(withoutPassword.items[0]?.facts["Decryption status"]).toBe( "Not attempted", ); const withPassword = await inspectCryptoInput(pem(bytes), new Date(), { password: "local secret", }); expect(withPassword.items[0]?.facts["Private-key algorithm"]).toContain( "EC", ); }); it("wipes decrypted private-key bytes after PEM and DER inspection", async () => { const bytes = await encryptedPkcs8("ephemeral secret"); const fill = vi.spyOn(Uint8Array.prototype, "fill"); try { await inspectCryptoInput(pem(bytes), new Date(), { password: "ephemeral secret", }); await inspectCryptoInput(bytes, new Date(), { password: "ephemeral secret", }); expect(fill.mock.calls.filter(([value]) => value === 0)).toHaveLength(2); } finally { fill.mockRestore(); } }); it("wipes decrypted bytes when the plaintext is not valid PKCS #8", async () => { const bytes = await encryptedPbes2Payload( "wrong structure", new TextEncoder().encode("valid AES-CBC padding; invalid PKCS #8"), ); const fill = vi.spyOn(Uint8Array.prototype, "fill"); try { await expect( decryptEncryptedPkcs8(bytes, "wrong structure"), ).rejects.toThrow(/PKCS #8|DER|SEQUENCE/u); expect(fill.mock.calls.filter(([value]) => value === 0)).toHaveLength(1); } finally { fill.mockRestore(); } }); });