import { describe, it, expect } from 'vitest' import { generatePassword, generateSalt, deriveKey, encrypt, decrypt, verifyPassword, createTestPayload, uint8ArrayToBase64, base64ToUint8Array, } from '../../../src/lib/crypto/crypto.js' describe('generatePassword', () => { it('should produce different passwords on consecutive calls', () => { const passwords = new Set() for (let i = 0; i < 50; i++) { passwords.add(generatePassword({ length: 16 })) } expect(passwords.size).toBe(50) }) it('should respect the length option', () => { for (let len of [4, 8, 16, 32, 64]) { const pw = generatePassword({ length: len }) expect(pw.length).toBe(len) } }) it('should only use allowed character types', () => { const pw = generatePassword({ uppercase: true, lowercase: false, digits: false, symbols: false }) expect(pw).toMatch(/^[A-Z]+$/) const pw2 = generatePassword({ uppercase: false, lowercase: true, digits: false, symbols: false }) expect(pw2).toMatch(/^[a-z]+$/) const pw3 = generatePassword({ uppercase: false, lowercase: false, digits: true, symbols: false }) expect(pw3).toMatch(/^[0-9]+$/) }) it('should exclude specified characters', () => { const pw = generatePassword({ length: 32, exclude: '0OIl1' }) for (const ch of '0OIl1') { expect(pw).not.toContain(ch) } }) it('should throw when charset is empty', () => { expect(() => generatePassword({ uppercase: false, lowercase: false, digits: false, symbols: false })) .toThrow() }) }) describe('generateSalt', () => { it('should return a 16-byte Uint8Array', () => { const salt = generateSalt() expect(salt).toBeInstanceOf(Uint8Array) expect(salt.length).toBe(16) }) it('should produce different salts on consecutive calls', () => { const salt1 = generateSalt() const salt2 = generateSalt() expect(salt1).not.toEqual(salt2) }) }) describe('deriveKey', () => { it('should derive a key from password and salt', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) expect(key).toBeDefined() expect(key.type).toBe('secret') expect(key.algorithm.name).toBe('AES-GCM') expect(key.algorithm.length).toBe(256) }) it('should derive the same key for same password and salt', async () => { const salt = generateSalt() const key1 = await deriveKey('test-password', salt) const key2 = await deriveKey('test-password', salt) // Encrypt with key1, decrypt with key2 — should work if keys are identical const encrypted = await encrypt('hello', key1) const decrypted = await decrypt(encrypted, key2) expect(decrypted).toBe('hello') }) it('should derive different keys for different passwords', async () => { const salt = generateSalt() const key1 = await deriveKey('password1', salt) const key2 = await deriveKey('password2', salt) const encrypted = await encrypt('hello', key1) // Should fail with wrong key await expect(decrypt(encrypted, key2)).rejects.toThrow() }) }) describe('encrypt / decrypt', () => { it('should encrypt and decrypt a string', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const plaintext = 'my-secret-password' const encrypted = await encrypt(plaintext, key) expect(typeof encrypted).toBe('string') const decrypted = await decrypt(encrypted, key) expect(decrypted).toBe(plaintext) }) it('should produce different ciphertext for same plaintext', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const encrypted1 = await encrypt('hello', key) const encrypted2 = await encrypt('hello', key) expect(encrypted1).not.toBe(encrypted2) }) it('should decrypt to correct value despite different ciphertext', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const encrypted1 = await encrypt('hello', key) const encrypted2 = await encrypt('hello', key) expect(await decrypt(encrypted1, key)).toBe('hello') expect(await decrypt(encrypted2, key)).toBe('hello') }) it('should handle empty string', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const encrypted = await encrypt('', key) const decrypted = await decrypt(encrypted, key) expect(decrypted).toBe('') }) it('should handle unicode characters', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const plaintext = 'Привет мир! 你好世界 🌍' const encrypted = await encrypt(plaintext, key) const decrypted = await decrypt(encrypted, key) expect(decrypted).toBe(plaintext) }) it('should fail with wrong key', async () => { const salt1 = generateSalt() const key1 = await deriveKey('correct', salt1) const salt2 = generateSalt() const key2 = await deriveKey('wrong', salt2) const encrypted = await encrypt('secret', key1) await expect(decrypt(encrypted, key2)).rejects.toThrow() }) it('should return valid JSON', async () => { const salt = generateSalt() const key = await deriveKey('test-password', salt) const encrypted = await encrypt('hello', key) const parsed = JSON.parse(encrypted) expect(parsed).toHaveProperty('iv') expect(parsed).toHaveProperty('ciphertext') }) }) describe('verifyPassword', () => { it('should return true for correct password', async () => { const salt = generateSalt() const key = await deriveKey('correct-password', salt) const testPlaintext = 'test-value' const testEncrypted = await encrypt(testPlaintext, key) const result = await verifyPassword('correct-password', salt, testEncrypted, testPlaintext) expect(result).toBe(true) }) it('should return false for wrong password', async () => { const salt = generateSalt() const key = await deriveKey('correct-password', salt) const testPlaintext = 'test-value' const testEncrypted = await encrypt(testPlaintext, key) const result = await verifyPassword('wrong-password', salt, testEncrypted, testPlaintext) expect(result).toBe(false) }) it('should return false for empty password', async () => { const salt = generateSalt() const key = await deriveKey('correct-password', salt) const testPlaintext = 'test-value' const testEncrypted = await encrypt(testPlaintext, key) const result = await verifyPassword('', salt, testEncrypted, testPlaintext) expect(result).toBe(false) }) }) describe('createTestPayload', () => { it('should return salt, testEncrypted, and testPlaintext', async () => { const payload = await createTestPayload('my-password') expect(payload.salt).toBeInstanceOf(Uint8Array) expect(payload.salt.length).toBe(16) expect(typeof payload.testEncrypted).toBe('string') expect(typeof payload.testPlaintext).toBe('string') expect(payload.testPlaintext).toMatch(/^vault_test_/) }) it('should produce a payload that verifies correctly', async () => { const { salt, testEncrypted, testPlaintext } = await createTestPayload('my-password') const result = await verifyPassword('my-password', salt, testEncrypted, testPlaintext) expect(result).toBe(true) }) it('should produce different payloads on consecutive calls', async () => { const p1 = await createTestPayload('pass') const p2 = await createTestPayload('pass') expect(p1.salt).not.toEqual(p2.salt) expect(p1.testPlaintext).not.toBe(p2.testPlaintext) }) }) describe('uint8ArrayToBase64 / base64ToUint8Array', () => { it('should roundtrip a Uint8Array', () => { const original = new Uint8Array([0, 127, 255, 10, 20, 30, 128, 200]) const encoded = uint8ArrayToBase64(original) const decoded = base64ToUint8Array(encoded) expect(decoded).toEqual(original) }) it('should roundtrip an empty array', () => { const original = new Uint8Array(0) const encoded = uint8ArrayToBase64(original) const decoded = base64ToUint8Array(encoded) expect(decoded).toEqual(original) }) it('should produce valid base64 string', () => { const original = new Uint8Array([65, 66, 67]) const encoded = uint8ArrayToBase64(original) expect(typeof encoded).toBe('string') expect(encoded).toBe('QUJD') }) })