253 lines
8.3 KiB
JavaScript
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')
|
|
})
|
|
})
|