Release Crypto Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 10:40:23 +02:00
parent 4b75f819e2
commit 82bb01b13f
37 changed files with 3673 additions and 140 deletions
+67 -1
View File
@@ -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: "./",
});
});
+18
View File
@@ -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);
});
+78
View File
@@ -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,
});
});
});
+115
View File
@@ -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);
});
});
+89
View File
@@ -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();
}
});
});
+127
View File
@@ -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-----");
});
});
+107
View File
@@ -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));
}
+15
View File
@@ -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));
}