116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
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);
|
|
});
|
|
});
|