@@ -1,6 +1,15 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { MODERN_PFX_BASE64, MODERN_PFX_PASSWORD } from "../fixtures/pkcs12";
|
||||
|
||||
const ORIGIN = "http://127.0.0.1:4173";
|
||||
function nodeBase64Buffer(value: string): never {
|
||||
return (
|
||||
globalThis as unknown as {
|
||||
Buffer: { from(source: string, encoding: "base64"): never };
|
||||
}
|
||||
).Buffer.from(value, "base64");
|
||||
}
|
||||
|
||||
async function localOnly(page: Page) {
|
||||
const external: string[] = [];
|
||||
await page.route("**/*", async (route) => {
|
||||
@@ -39,6 +48,63 @@ test("inspects a JWK and computes its local thumbprint", async ({ page }) => {
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("round-trips authenticated text with an explicit generated AES key", async ({
|
||||
page,
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/crypto/");
|
||||
await page.getByRole("button", { name: "Generate 256-bit AES key" }).click();
|
||||
await expect(page.getByLabel("Operation key")).toHaveValue(
|
||||
/^[A-Za-z0-9_-]{43}$/u,
|
||||
);
|
||||
await page.getByRole("button", { name: "Encrypt locally" }).click();
|
||||
await expect(page.locator(".operation-result > pre")).toContainText(
|
||||
'"algorithm": "AES-256-GCM"',
|
||||
);
|
||||
await page.getByRole("button", { name: "Decrypt" }).click();
|
||||
await page.getByRole("button", { name: "Decrypt locally" }).click();
|
||||
await expect(page.locator(".operation-result > pre")).toHaveText(
|
||||
"Local-only example",
|
||||
);
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("inspects a modern PFX only after explicit password consent", async ({
|
||||
page,
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/crypto/");
|
||||
await page.getByLabel("Open file").setInputFiles({
|
||||
name: "modern-test.p12",
|
||||
mimeType: "application/x-pkcs12",
|
||||
buffer: nodeBase64Buffer(MODERN_PFX_BASE64),
|
||||
});
|
||||
await expect(
|
||||
page.getByText("Present; explicit password required to verify"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Local test identity" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByLabel(/Use this password in memory for this inspection/u)
|
||||
.check();
|
||||
await page
|
||||
.getByLabel(/PBES2 \/ PKCS #12 password/u)
|
||||
.fill(MODERN_PFX_PASSWORD);
|
||||
await page.getByRole("button", { name: "Inspect locally" }).click();
|
||||
await expect(
|
||||
page.getByText("Verified with the explicitly supplied password"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Local PFX Test", { exact: false }).first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("dt", { hasText: "Private-key algorithm" }),
|
||||
).toBeVisible();
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("serves the release identity and hardened headers", async ({
|
||||
request,
|
||||
}) => {
|
||||
@@ -51,7 +117,7 @@ test("serves the release identity and hardened headers", async ({
|
||||
const manifest = await request.get("/deep/nested/crypto/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.crypto-tools",
|
||||
version: "0.1.0",
|
||||
version: "0.2.0",
|
||||
entry: "./",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/crypto/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
AuthorityKeyIdentifierExtension,
|
||||
BasicConstraintsExtension,
|
||||
KeyUsageFlags,
|
||||
KeyUsagesExtension,
|
||||
SubjectKeyIdentifierExtension,
|
||||
X509CertificateGenerator,
|
||||
} from "@peculiar/x509";
|
||||
import {
|
||||
checkCertificateHostname,
|
||||
inspectCryptoInput,
|
||||
@@ -70,4 +78,74 @@ describe("crypto input inspection", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("builds an explicitly untrusted path with signature and CA checks", async () => {
|
||||
const algorithm = {
|
||||
name: "ECDSA",
|
||||
namedCurve: "P-256",
|
||||
hash: "SHA-256",
|
||||
} as const;
|
||||
const rootKeys = await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
const root = await X509CertificateGenerator.createSelfSigned(
|
||||
{
|
||||
serialNumber: "01",
|
||||
name: "CN=Local Test Root",
|
||||
notBefore: new Date("2029-01-01T00:00:00Z"),
|
||||
notAfter: new Date("2035-01-01T00:00:00Z"),
|
||||
signingAlgorithm: algorithm,
|
||||
keys: rootKeys,
|
||||
extensions: [
|
||||
new BasicConstraintsExtension(true, 1, true),
|
||||
new KeyUsagesExtension(
|
||||
KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign,
|
||||
true,
|
||||
),
|
||||
await SubjectKeyIdentifierExtension.create(rootKeys.publicKey),
|
||||
],
|
||||
},
|
||||
crypto,
|
||||
);
|
||||
const leafKeys = await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
const leaf = await X509CertificateGenerator.create(
|
||||
{
|
||||
serialNumber: "02",
|
||||
subject: "CN=Leaf",
|
||||
issuer: root.subject,
|
||||
notBefore: new Date("2029-01-01T00:00:00Z"),
|
||||
notAfter: new Date("2031-01-01T00:00:00Z"),
|
||||
signingAlgorithm: algorithm,
|
||||
publicKey: leafKeys.publicKey,
|
||||
signingKey: rootKeys.privateKey,
|
||||
extensions: [
|
||||
new BasicConstraintsExtension(false, undefined, true),
|
||||
await AuthorityKeyIdentifierExtension.create(rootKeys.publicKey),
|
||||
],
|
||||
},
|
||||
crypto,
|
||||
);
|
||||
const inspection = await inspectCryptoInput(
|
||||
`${leaf.toString("pem")}\n${root.toString("pem")}`,
|
||||
new Date("2030-01-01T00:00:00Z"),
|
||||
);
|
||||
expect(inspection.paths).toHaveLength(1);
|
||||
expect(inspection.paths[0]).toMatchObject({
|
||||
status: "self-signed-anchor-present",
|
||||
trusted: false,
|
||||
certificates: ["CN=Leaf", "CN=Local Test Root"],
|
||||
});
|
||||
expect(inspection.paths[0]?.links[0]).toMatchObject({
|
||||
signatureValid: true,
|
||||
issuerIsCa: true,
|
||||
keyCertSignAllowed: true,
|
||||
authorityKeyIdentifierMatched: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { base64UrlToBytes, bytesToBase64Url } from "@add-ideas/toolbox-helpers";
|
||||
import {
|
||||
decryptText,
|
||||
encryptText,
|
||||
generateAesKeySource,
|
||||
signText,
|
||||
verifyText,
|
||||
} from "../../src/crypto/operations";
|
||||
import { encryptedPkcs8, encryptedPkcs8Pem } from "../fixtures/pbes2";
|
||||
|
||||
describe("concrete WebCrypto operations", () => {
|
||||
it("round-trips authenticated AES-256-GCM and rejects tampering", async () => {
|
||||
const key = generateAesKeySource();
|
||||
expect(key).toMatch(/^[A-Za-z0-9_-]{43}$/u);
|
||||
const encrypted = await encryptText("aes-gcm-256", key, "private text");
|
||||
const decrypted = await decryptText(
|
||||
"aes-gcm-256",
|
||||
key,
|
||||
"",
|
||||
encrypted.output,
|
||||
);
|
||||
expect(decrypted.output).toBe("private text");
|
||||
|
||||
const envelope = JSON.parse(encrypted.output) as { ciphertext: string };
|
||||
const tampered = base64UrlToBytes(envelope.ciphertext);
|
||||
tampered[0] = tampered[0]! ^ 1;
|
||||
envelope.ciphertext = bytesToBase64Url(tampered);
|
||||
await expect(
|
||||
decryptText("aes-gcm-256", key, "", JSON.stringify(envelope)),
|
||||
).rejects.toThrow(/authentication failed/u);
|
||||
});
|
||||
|
||||
it("signs and verifies ECDSA P-256 messages", async () => {
|
||||
const pair = await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
const privateJwk = JSON.stringify(
|
||||
await crypto.subtle.exportKey("jwk", pair.privateKey),
|
||||
);
|
||||
const publicJwk = JSON.stringify(
|
||||
await crypto.subtle.exportKey("jwk", pair.publicKey),
|
||||
);
|
||||
const signed = await signText(
|
||||
"ecdsa-p256-sha256",
|
||||
privateJwk,
|
||||
"",
|
||||
"bounded message",
|
||||
);
|
||||
await expect(
|
||||
verifyText(
|
||||
"ecdsa-p256-sha256",
|
||||
publicJwk,
|
||||
"bounded message",
|
||||
signed.output,
|
||||
),
|
||||
).resolves.toMatchObject({ valid: true });
|
||||
await expect(
|
||||
verifyText(
|
||||
"ecdsa-p256-sha256",
|
||||
publicJwk,
|
||||
"changed message",
|
||||
signed.output,
|
||||
),
|
||||
).resolves.toMatchObject({ valid: false });
|
||||
});
|
||||
|
||||
it("wipes both plaintext PKCS #8 buffers after encrypted key import", async () => {
|
||||
const encrypted = await encryptedPkcs8("operation secret");
|
||||
const fill = vi.spyOn(Uint8Array.prototype, "fill");
|
||||
try {
|
||||
await signText(
|
||||
"ecdsa-p256-sha256",
|
||||
encryptedPkcs8Pem(encrypted),
|
||||
"operation secret",
|
||||
"bounded message",
|
||||
);
|
||||
expect(fill.mock.calls.filter(([value]) => value === 0)).toHaveLength(2);
|
||||
} finally {
|
||||
fill.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("enforces RSA-OAEP message size and round-trips short UTF-8", async () => {
|
||||
const pair = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
modulusLength: 2048,
|
||||
publicExponent: Uint8Array.of(1, 0, 1),
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
const privateJwk = JSON.stringify(
|
||||
await crypto.subtle.exportKey("jwk", pair.privateKey),
|
||||
);
|
||||
const publicJwk = JSON.stringify(
|
||||
await crypto.subtle.exportKey("jwk", pair.publicKey),
|
||||
);
|
||||
const encrypted = await encryptText(
|
||||
"rsa-oaep-sha256",
|
||||
publicJwk,
|
||||
"short secret",
|
||||
);
|
||||
await expect(
|
||||
decryptText("rsa-oaep-sha256", privateJwk, "", encrypted.output),
|
||||
).resolves.toMatchObject({ output: "short secret" });
|
||||
await expect(
|
||||
encryptText("rsa-oaep-sha256", publicJwk, "x".repeat(191)),
|
||||
).rejects.toThrow(/limited to 190 bytes/u);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inspectCryptoInput } from "../../src/crypto/inspection";
|
||||
import { inspectPkcs12 } from "../../src/crypto/pkcs12";
|
||||
import {
|
||||
EMPTY_PASSWORD_PFX_BASE64,
|
||||
LEGACY_PFX_BASE64,
|
||||
MODERN_PFX_PASSWORD,
|
||||
pfxFixture,
|
||||
} from "../fixtures/pkcs12";
|
||||
|
||||
describe("bounded PKCS #12 inspection", () => {
|
||||
it("inventories supported content without silently using a password", async () => {
|
||||
const result = await inspectPkcs12(pfxFixture());
|
||||
expect(result.mac).toMatchObject({
|
||||
present: true,
|
||||
status: "password-required",
|
||||
algorithm: "SHA-256",
|
||||
iterations: 2_048,
|
||||
});
|
||||
expect(result.contents).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "encryptedData",
|
||||
state: "password-required",
|
||||
}),
|
||||
expect.objectContaining({ type: "data", state: "parsed" }),
|
||||
]),
|
||||
);
|
||||
expect(result.bags).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
bagType: "shrouded-private-key",
|
||||
friendlyName: "Local test identity",
|
||||
state: "password-required",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("verifies MacData and decrypts PBES2 SafeContents/key bags", async () => {
|
||||
const result = await inspectPkcs12(pfxFixture(), {
|
||||
password: MODERN_PFX_PASSWORD,
|
||||
});
|
||||
expect(result.mac.status).toBe("verified");
|
||||
expect(result.contents.every((content) => content.state === "parsed")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(result.bags).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
bagType: "certificate",
|
||||
friendlyName: "Local test identity",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
bagType: "shrouded-private-key",
|
||||
state: "inspected",
|
||||
key: expect.objectContaining({ algorithm: "RSA" }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("distinguishes explicit empty passwords from password omission", async () => {
|
||||
const bytes = pfxFixture(EMPTY_PASSWORD_PFX_BASE64);
|
||||
expect((await inspectPkcs12(bytes)).mac.status).toBe("password-required");
|
||||
const inspected = await inspectPkcs12(bytes, { password: "" });
|
||||
expect(inspected.mac.status).toBe("verified");
|
||||
expect(
|
||||
inspected.bags.some(
|
||||
(bag) =>
|
||||
bag.bagType === "shrouded-private-key" &&
|
||||
bag.state === "inspected" &&
|
||||
bag.key?.algorithm === "RSA",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("fails closed for wrong passwords, damaged nesting and legacy PBE", async () => {
|
||||
await expect(
|
||||
inspectPkcs12(pfxFixture(), { password: "wrong-password" }),
|
||||
).rejects.toThrow(/password is incorrect|MacData\/authSafe/u);
|
||||
|
||||
const truncated = pfxFixture().slice(0, -1);
|
||||
await expect(inspectPkcs12(truncated)).rejects.toThrow(
|
||||
/malformed DER|trailing bytes/u,
|
||||
);
|
||||
|
||||
await expect(
|
||||
inspectPkcs12(pfxFixture(LEGACY_PFX_BASE64), {
|
||||
password: MODERN_PFX_PASSWORD,
|
||||
}),
|
||||
).rejects.toThrow(/legacy PKCS #12 PBE/u);
|
||||
|
||||
await expect(
|
||||
inspectPkcs12(pfxFixture(), { password: "x".repeat(4_097) }),
|
||||
).rejects.toThrow(/at most 4,096 UTF-16 units/u);
|
||||
});
|
||||
|
||||
it("integrates bag and certificate inventory without reporting secrets", async () => {
|
||||
const result = await inspectCryptoInput(pfxFixture(), new Date(), {
|
||||
password: MODERN_PFX_PASSWORD,
|
||||
});
|
||||
expect(result.items[0]).toMatchObject({
|
||||
type: "PKCS #12/PFX",
|
||||
facts: expect.objectContaining({
|
||||
MacData: expect.stringMatching(/Verified/u),
|
||||
}),
|
||||
});
|
||||
expect(result.items.some((item) => item.certificate)).toBe(true);
|
||||
expect(
|
||||
result.items.some((item) =>
|
||||
item.facts["Private-key algorithm"]?.includes("RSA"),
|
||||
),
|
||||
).toBe(true);
|
||||
const reportSurface = JSON.stringify(
|
||||
result.items.map(({ id, type, title, facts, findings }) => ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
facts,
|
||||
findings,
|
||||
})),
|
||||
);
|
||||
expect(reportSurface).not.toContain(MODERN_PFX_PASSWORD);
|
||||
expect(reportSurface).not.toContain("PRIVATE KEY-----");
|
||||
});
|
||||
});
|
||||
Vendored
+107
@@ -0,0 +1,107 @@
|
||||
import { Integer, Null, ObjectIdentifier, OctetString, Sequence } from "asn1js";
|
||||
|
||||
function buffer(bytes: Uint8Array): ArrayBuffer {
|
||||
return bytes.buffer.slice(
|
||||
bytes.byteOffset,
|
||||
bytes.byteOffset + bytes.byteLength,
|
||||
) as ArrayBuffer;
|
||||
}
|
||||
|
||||
export function encryptedPkcs8Pem(bytes: Uint8Array): string {
|
||||
const binary = Array.from(bytes, (value) => String.fromCharCode(value)).join(
|
||||
"",
|
||||
);
|
||||
const body =
|
||||
btoa(binary)
|
||||
.match(/.{1,64}/gu)
|
||||
?.join("\n") ?? "";
|
||||
return `-----BEGIN ENCRYPTED PRIVATE KEY-----\n${body}\n-----END ENCRYPTED PRIVATE KEY-----`;
|
||||
}
|
||||
|
||||
export async function encryptedPkcs8(password: string): Promise<Uint8Array> {
|
||||
const pair = await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
const privateKey = new Uint8Array(
|
||||
await crypto.subtle.exportKey("pkcs8", pair.privateKey),
|
||||
);
|
||||
try {
|
||||
return await encryptedPbes2Payload(password, privateKey);
|
||||
} finally {
|
||||
privateKey.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptedPbes2Payload(
|
||||
password: string,
|
||||
plaintext: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const salt = Uint8Array.from({ length: 16 }, (_, index) => index + 1);
|
||||
const iv = Uint8Array.from({ length: 16 }, (_, index) => 32 - index);
|
||||
const material = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveKey"],
|
||||
);
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
salt: buffer(salt),
|
||||
iterations: 12_000,
|
||||
},
|
||||
material,
|
||||
{ name: "AES-CBC", length: 256 },
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
const ciphertext = new Uint8Array(
|
||||
await crypto.subtle.encrypt(
|
||||
{ name: "AES-CBC", iv: buffer(iv) },
|
||||
key,
|
||||
buffer(plaintext),
|
||||
),
|
||||
);
|
||||
const encoded = new Sequence({
|
||||
value: [
|
||||
new Sequence({
|
||||
value: [
|
||||
new ObjectIdentifier({ value: "1.2.840.113549.1.5.13" }),
|
||||
new Sequence({
|
||||
value: [
|
||||
new Sequence({
|
||||
value: [
|
||||
new ObjectIdentifier({ value: "1.2.840.113549.1.5.12" }),
|
||||
new Sequence({
|
||||
value: [
|
||||
new OctetString({ valueHex: buffer(salt) }),
|
||||
new Integer({ value: 12_000 }),
|
||||
new Sequence({
|
||||
value: [
|
||||
new ObjectIdentifier({ value: "1.2.840.113549.2.9" }),
|
||||
new Null(),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Sequence({
|
||||
value: [
|
||||
new ObjectIdentifier({ value: "2.16.840.1.101.3.4.1.42" }),
|
||||
new OctetString({ valueHex: buffer(iv) }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new OctetString({ valueHex: buffer(ciphertext) }),
|
||||
],
|
||||
});
|
||||
return new Uint8Array(encoded.toBER(false));
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
// Synthetic test-only identity generated with OpenSSL 3.5.4. The private key is
|
||||
// protected by the published fixture password and must never be used elsewhere.
|
||||
export const MODERN_PFX_PASSWORD = "local-secret";
|
||||
export const MODERN_PFX_BASE64 =
|
||||
"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";
|
||||
|
||||
export const LEGACY_PFX_BASE64 =
|
||||
"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";
|
||||
|
||||
export const EMPTY_PASSWORD_PFX_BASE64 =
|
||||
"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";
|
||||
|
||||
export function pfxFixture(base64 = MODERN_PFX_BASE64): Uint8Array {
|
||||
return Uint8Array.from(atob(base64), (character) => character.charCodeAt(0));
|
||||
}
|
||||
Reference in New Issue
Block a user