/**
* Tests for encryption utilities
*/
import { describe, it, expect } from 'vitest';
import {
encrypt,
decrypt,
encryptWithKey,
decryptWithKey,
generateKey,
deriveKey,
generateSecureToken,
secureCompare,
hashForLookup,
SecretsManager,
} from './crypto.js';
describe('encrypt/decrypt with password', () => {
it('should encrypt and decrypt a string', () => {
const plaintext = 'Hello, World!';
const password = 'my-secure-password';
const encrypted = encrypt(plaintext, password);
const decrypted = decrypt(encrypted, password);
expect(decrypted).toBe(plaintext);
});
it('should produce different ciphertext for same input', () => {
const plaintext = 'Same message';
const password = 'password';
const encrypted1 = encrypt(plaintext, password);
const encrypted2 = encrypt(plaintext, password);
expect(encrypted1.ciphertext).not.toBe(encrypted2.ciphertext);
expect(encrypted1.iv).not.toBe(encrypted2.iv);
});
it('should fail with wrong password', () => {
const encrypted = encrypt('secret', 'correct-password');
expect(() => decrypt(encrypted, 'wrong-password')).toThrow();
});
it('should handle unicode strings', () => {
const plaintext = '你好世界 🌍';
const password = 'password';
const encrypted = encrypt(plaintext, password);
const decrypted = decrypt(encrypted, password);
expect(decrypted).toBe(plaintext);
});
});
describe('encryptWithKey/decryptWithKey', () => {
it('should encrypt and decrypt with raw key', () => {
const key = generateKey();
const plaintext = 'Secret data';
const encrypted = encryptWithKey(plaintext, key);
const decrypted = decryptWithKey(encrypted, key);
expect(decrypted).toBe(plaintext);
});
it('should reject invalid key length', () => {
const shortKey = Buffer.alloc(16);
expect(() => encryptWithKey('test', shortKey)).toThrow('Invalid key length');
});
});
describe('deriveKey', () => {
it('should derive consistent key from password and salt', () => {
const password = 'my-password';
const { key: key1, salt } = deriveKey(password);
const { key: key2 } = deriveKey(password, salt);
expect(key1.equals(key2)).toBe(true);
});
it('should generate different keys with different salts', () => {
const password = 'same-password';
const { key: key1 } = deriveKey(password);
const { key: key2 } = deriveKey(password);
expect(key1.equals(key2)).toBe(false);
});
});
describe('generateSecureToken', () => {
it('should generate tokens of specified length', () => {
const token = generateSecureToken(32);
// Base64url encoding: 32 bytes = ~43 characters
expect(token.length).toBeGreaterThan(40);
});
it('should generate unique tokens', () => {
const tokens = Array.from({ length: 100 }, () => generateSecureToken());
const unique = new Set(tokens);
expect(unique.size).toBe(100);
});
});
describe('secureCompare', () => {
it('should return true for equal strings', () => {
expect(secureCompare('abc', 'abc')).toBe(true);
});
it('should return false for different strings', () => {
expect(secureCompare('abc', 'abd')).toBe(false);
});
it('should return false for different lengths', () => {
expect(secureCompare('abc', 'abcd')).toBe(false);
});
});
describe('hashForLookup', () => {
it('should produce consistent hashes', () => {
const hash1 = hashForLookup('value', 'salt');
const hash2 = hashForLookup('value', 'salt');
expect(hash1).toBe(hash2);
});
it('should produce different hashes for different inputs', () => {
const hash1 = hashForLookup('value1');
const hash2 = hashForLookup('value2');
expect(hash1).not.toBe(hash2);
});
});
describe('SecretsManager', () => {
it('should encrypt and decrypt strings', () => {
const manager = new SecretsManager('master-password');
const secret = 'my-api-key';
const encrypted = manager.encrypt(secret);
const decrypted = manager.decrypt(encrypted);
expect(decrypted).toBe(secret);
});
it('should encrypt and decrypt objects', () => {
const manager = new SecretsManager('master-password');
const obj = { apiKey: 'secret', token: 'token123' };
const encrypted = manager.encryptObject(obj);
const decrypted = manager.decryptObject<typeof obj>(encrypted);
expect(decrypted).toEqual(obj);
});
});