90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|