@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user