import test from 'brittle'
import { CryptoManager } from '../lib/crypto.js'
test('CryptoManager - salt generation', async (t) => {
t.plan(3)
const salt1 = CryptoManager.generateSalt()
const salt2 = CryptoManager.generateSalt()
t.ok(Buffer.isBuffer(salt1), 'Should return a Buffer')
t.is(salt1.length, 32, 'Should return 32 bytes')
t.not(salt1.equals(salt2), 'Should generate different salts each time')
})
test('CryptoManager - vault key derivation', async (t) => {
t.plan(4)
const passphrase = 'test-passphrase'
const salt = Buffer.alloc(32, 1)
const key1 = CryptoManager.deriveVaultKey(passphrase, salt)
const key2 = CryptoManager.deriveVaultKey(passphrase, salt)
t.ok(Buffer.isBuffer(key1), 'Should return a Buffer')
t.is(key1.length, 32, 'Should return a 32-byte secretbox key')
t.ok(key1.equals(key2), 'Same passphrase and salt should produce same key')
const differentSalt = Buffer.alloc(32, 2)
t.not(key1.equals(CryptoManager.deriveVaultKey(passphrase, differentSalt)), 'Different salt should produce different key')
})
test('CryptoManager - vault key derivation validation', async (t) => {
t.plan(2)
try {
CryptoManager.deriveVaultKey(123, Buffer.alloc(32))
t.fail('Should throw for non-string passphrase')
} catch (error) {
t.ok(/Passphrase must be/.test(error.message), 'Should validate passphrase type')
}
try {
// Shorter than crypto_pwhash_SALTBYTES.
CryptoManager.deriveVaultKey('test', Buffer.alloc(8))
t.fail('Should throw for an undersized salt')
} catch (error) {
t.ok(/Salt must be/.test(error.message), 'Should validate salt size')
}
})
// The previous suite asserted `encrypt(encrypt(d, k), k) === d` — the self-inverse property of a
// repeating-key XOR. That was a test OF the weakness (#47): a cipher with that property is
// unauthenticated and its keystream is recoverable from known plaintext. It is deliberately not
// replaced. The properties that matter now — round-trip, wrong-passphrase rejection, tamper
// detection, nonce freshness — are asserted in test/vault-crypto.test.js.
test('CryptoManager - vault encryption is authenticated and non-malleable', async (t) => {
t.plan(3)
const data = Buffer.from('Hello, World! This is a test message.')
const salt = CryptoManager.generateSalt()
const box = CryptoManager.encryptSecret(data, 'pw', salt)
t.ok(box.length > data.length, 'Ciphertext carries a nonce and a MAC, so it is LONGER than the plaintext')
t.ok(CryptoManager.decryptSecret(box, 'pw', salt).equals(data), 'Round-trips')
const twice = CryptoManager.encryptSecret(box, 'pw', salt)
t.not(CryptoManager.decryptSecret(twice, 'pw', salt).equals(data), 'Encrypting twice does NOT return the original — no self-inverse')
})
test('CryptoManager - vault encryption validation', async (t) => {
t.plan(2)
const salt = CryptoManager.generateSalt()
try {
CryptoManager.encryptSecret('not a buffer', 'pw', salt)
t.fail('Should throw error for non-Buffer data')
} catch (error) {
t.is(error.message, 'Data must be a Buffer', 'Should validate data type')
}
try {
// Too short to contain a nonce and a MAC, so it cannot be a vault box.
CryptoManager.decryptSecret(Buffer.alloc(8), 'pw', salt)
t.fail('Should throw error for a truncated box')
} catch (error) {
t.ok(/too short/.test(error.message), 'Should reject a truncated ciphertext')
}
})
test('CryptoManager - hashing', async (t) => {
t.plan(4)
const data1 = Buffer.from('test data')
const data2 = Buffer.from('test data')
const data3 = Buffer.from('different data')
const hash1 = CryptoManager.hash(data1)
const hash2 = CryptoManager.hash(data2)
const hash3 = CryptoManager.hash(data3)
t.ok(Buffer.isBuffer(hash1), 'Should return a Buffer')
t.is(hash1.length, 32, 'Should return 32-byte hash')
t.ok(hash1.equals(hash2), 'Same data should produce same hash')
t.not(hash1.equals(hash3), 'Different data should produce different hash')
})
test('CryptoManager - hash validation', async (t) => {
t.plan(1)
try {
CryptoManager.hash('not a buffer')
t.fail('Should throw error for non-Buffer data')
} catch (error) {
t.is(error.message, 'Data must be a Buffer', 'Should validate data type')
}
})
test('CryptoManager - random bytes', async (t) => {
t.plan(4)
const bytes1 = CryptoManager.randomBytes(16)
const bytes2 = CryptoManager.randomBytes(16)
const bytes3 = CryptoManager.randomBytes(32)
t.ok(Buffer.isBuffer(bytes1), 'Should return a Buffer')
t.is(bytes1.length, 16, 'Should return requested number of bytes')
t.not(bytes1.equals(bytes2), 'Should generate different bytes each time')
t.is(bytes3.length, 32, 'Should handle different lengths')
})
test('CryptoManager - random bytes validation', async (t) => {
t.plan(2)
try {
CryptoManager.randomBytes('not a number')
t.fail('Should throw error for non-number length')
} catch (error) {
t.is(error.message, 'Length must be a non-negative number', 'Should validate length type')
}
try {
CryptoManager.randomBytes(-1)
t.fail('Should throw error for negative length')
} catch (error) {
t.is(error.message, 'Length must be a non-negative number', 'Should validate length value')
}
})