better-cipher
Version:
A secure encryption library with browser and Node.js support using AES-GCM
261 lines (221 loc) • 9.32 kB
text/typescript
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);
}
});
});
});