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); }); });