@@ -0,0 +1,89 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user