password_manager/tests/lib/crypto/crypto.test.js

253 lines
8.3 KiB
JavaScript

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')
})
})