UNPKG

better-cipher

Version:

A secure encryption library with browser and Node.js support using AES-GCM

261 lines (221 loc) 9.32 kB
import { describe, it, expect } from "vitest"; import { Cipher } from "./index.node"; import type { Encrypted } from "./types"; describe("Node Cipher", () => { const validKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const validKey2 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; const validKey3 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; describe("Constructor", () => { it("should create instance with valid key", () => { expect(() => new Cipher(validKey)).not.toThrow(); }); it("should throw on invalid hex string", () => { expect(() => new Cipher("not-a-hex-string")).toThrow(); }); it("should throw on empty string", () => { expect(() => new Cipher("")).toThrow(); }); it("should throw on non-string input", () => { // @ts-expect-error testing invalid input expect(() => new Cipher(123)).toThrow(); // @ts-expect-error testing invalid input expect(() => new Cipher(null)).toThrow(); // @ts-expect-error testing invalid input expect(() => new Cipher(undefined)).toThrow(); }); }); describe("Encryption", () => { const cipher = new Cipher(validKey); it("should encrypt string to valid format", async () => { const encrypted = await cipher.encrypt("test"); expect(encrypted).toEqual({ iv: expect.stringMatching(/^[0-9a-f]{24}$/), // 12 bytes in hex content: expect.stringMatching(/^[0-9a-f]+$/), // hex string authTag: expect.stringMatching(/^[0-9a-f]{32}$/), // 16 bytes in hex }); }); it("should handle empty string", async () => { const encrypted = await cipher.encrypt(""); expect(encrypted.content).not.toBe(""); const decrypted = await cipher.decrypt(encrypted); expect(decrypted).toBe(""); }); it("should handle unicode characters", async () => { const testCases = ["Hello, 世界!", "🌍🌎🌏", "Γεια σας", "مرحبا"]; for (const test of testCases) { const encrypted = await cipher.encrypt(test); const decrypted = await cipher.decrypt(encrypted); expect(decrypted).toBe(test); } }); it("should handle long strings", async () => { const longString = "a".repeat(1000000); // 1MB string const encrypted = await cipher.encrypt(longString); const decrypted = await cipher.decrypt(encrypted); expect(decrypted).toBe(longString); }); it("should produce different ciphertexts for same input", async () => { const input = "test"; const encrypted1 = await cipher.encrypt(input); const encrypted2 = await cipher.encrypt(input); // IVs should be different expect(encrypted1.iv).not.toBe(encrypted2.iv); // Content should be different due to different IVs expect(encrypted1.content).not.toBe(encrypted2.content); // Auth tags should be different expect(encrypted1.authTag).not.toBe(encrypted2.authTag); // Both should decrypt to the same value const decrypted1 = await cipher.decrypt(encrypted1); const decrypted2 = await cipher.decrypt(encrypted2); expect(decrypted1).toBe(input); expect(decrypted2).toBe(input); }); }); describe("Decryption", () => { const cipher = new Cipher(validKey); it("should decrypt previously encrypted data", async () => { const input = "test"; const encrypted = await cipher.encrypt(input); const decrypted = await cipher.decrypt(encrypted); expect(decrypted).toBe(input); }); it("should fail with wrong key", async () => { const cipher1 = new Cipher(validKey); const cipher2 = new Cipher(validKey2); const encrypted = await cipher1.encrypt("test"); await expect(cipher2.decrypt(encrypted)).rejects.toThrow(); }); it("should fail with tampered IV", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, iv: "a".repeat(24), // same length, different value }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with tampered content", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, content: encrypted.content + "00", // append extra bytes }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with tampered auth tag", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, authTag: "a".repeat(32), // same length, different value }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with truncated content", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, content: encrypted.content.slice(0, -2), // remove last byte }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with invalid hex in IV", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, iv: "x".repeat(24), // invalid hex }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with invalid hex in content", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, content: "x".repeat(encrypted.content.length), // invalid hex }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); it("should fail with invalid hex in auth tag", async () => { const encrypted = await cipher.encrypt("test"); const tampered: Encrypted = { ...encrypted, authTag: "x".repeat(32), // invalid hex }; await expect(cipher.decrypt(tampered)).rejects.toThrow(); }); }); describe("Key Rotation", () => { const cipher1 = new Cipher(validKey); const cipher2 = new Cipher(validKey2); it("should create working rotator", async () => { const rotator = Cipher.createRotator(validKey, validKey2); const encrypted = await cipher1.encrypt("test"); const rotated = await rotator(encrypted); // Should not be able to decrypt rotated data with old key await expect(cipher1.decrypt(rotated)).rejects.toThrow(); // Should be able to decrypt with new key const decrypted = await cipher2.decrypt(rotated); expect(decrypted).toBe("test"); }); it("should handle multiple rotations", async () => { const cipher3 = new Cipher(validKey3); const rotator1 = Cipher.createRotator(validKey, validKey2); const rotator2 = Cipher.createRotator(validKey2, validKey3); const encrypted = await cipher1.encrypt("test"); const rotated1 = await rotator1(encrypted); const rotated2 = await rotator2(rotated1); // Should not be able to decrypt with old keys await expect(cipher1.decrypt(rotated2)).rejects.toThrow(); await expect(cipher2.decrypt(rotated2)).rejects.toThrow(); // Should be able to decrypt with final key const decrypted = await cipher3.decrypt(rotated2); expect(decrypted).toBe("test"); }); it("should handle rotation of empty string", async () => { const rotator = Cipher.createRotator(validKey, validKey2); const encrypted = await cipher1.encrypt(""); const rotated = await rotator(encrypted); const decrypted = await cipher2.decrypt(rotated); expect(decrypted).toBe(""); }); it("should handle rotation of long strings", async () => { const longString = "a".repeat(1000000); // 1MB string const rotator = Cipher.createRotator(validKey, validKey2); const encrypted = await cipher1.encrypt(longString); const rotated = await rotator(encrypted); const decrypted = await cipher2.decrypt(rotated); expect(decrypted).toBe(longString); }); it("should fail rotation with invalid encrypted data", async () => { const rotator = Cipher.createRotator(validKey, validKey2); const encrypted = await cipher1.encrypt("test"); const tampered: Encrypted = { ...encrypted, authTag: "a".repeat(32), }; await expect(rotator(tampered)).rejects.toThrow(); }); }); describe("Performance", () => { const cipher = new Cipher(validKey); it("should handle concurrent operations", async () => { const inputs = Array.from({ length: 100 }, (_, i) => `test${i}`); const encrypted = await Promise.all( inputs.map((input) => cipher.encrypt(input)) ); const decrypted = await Promise.all( encrypted.map((e) => cipher.decrypt(e)) ); expect(decrypted).toEqual(inputs); }); it("should handle rapid sequential operations", async () => { const input = "test"; for (let i = 0; i < 1000; i++) { const encrypted = await cipher.encrypt(input); const decrypted = await cipher.decrypt(encrypted); expect(decrypted).toBe(input); } }); }); });