import test from 'brittle'
import { MockRegistry } from '../helpers/fixtures.js'
import { NotebookCrypto } from '../../lib/notebook-crypto.js'
// ==================== FIELD ENCRYPTION ====================
test('encryptField / decryptField — round-trip', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
// Enable encryption to get a notebook key
const key = await crypto.enableEncryption('test-notebook', 'password123')
const plaintext = 'Hello, this is a secret note!'
const encrypted = crypto.encryptField(plaintext, key)
// Encrypted is hex nonce:ciphertext
t.ok(encrypted.includes(':'), 'encrypted contains nonce:ciphertext separator')
t.not(encrypted, plaintext, 'encrypted differs from plaintext')
const decrypted = crypto.decryptField(encrypted, key)
t.is(decrypted, plaintext, 'decrypted matches original')
})
test('encryptField — different nonces per call', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('nonce-test', 'password')
const enc1 = crypto.encryptField('same content', key)
const enc2 = crypto.encryptField('same content', key)
// Same plaintext, different nonces → different ciphertexts
t.not(enc1, enc2, 'two encryptions produce different ciphertexts')
// Both decrypt to same plaintext
t.is(crypto.decryptField(enc1, key), 'same content')
t.is(crypto.decryptField(enc2, key), 'same content')
})
test('encryptField — handles empty string', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('empty-test', 'password')
const encrypted = crypto.encryptField('', key)
const decrypted = crypto.decryptField(encrypted, key)
t.is(decrypted, '', 'empty string round-trips')
})
test('encryptField — handles unicode', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('unicode-test', 'password')
const plaintext = 'Hello World! Emoji: \u{1F680}\u{1F30D} CJK: \u4F60\u597D Diacritics: caf\u00E9'
const encrypted = crypto.encryptField(plaintext, key)
const decrypted = crypto.decryptField(encrypted, key)
t.is(decrypted, plaintext, 'unicode round-trips correctly')
})
// ==================== KEY MANAGEMENT ====================
test('enableEncryption — stores key in registry', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('my-notebook', 'secretpass')
t.ok(key, 'returns notebook key')
t.is(key.length, 32, 'key is 32 bytes')
// Registry has the entry
const entry = await registry.get('notebook-key:my-notebook')
t.ok(entry, 'key record stored in registry')
t.ok(entry.value.salt, 'has salt')
t.ok(entry.value.nonce, 'has nonce')
t.ok(entry.value.ciphertext, 'has ciphertext')
t.ok(entry.value.opslimit, 'has opslimit')
t.ok(entry.value.memlimit, 'has memlimit')
})
test('enableEncryption — rejects duplicate', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
await crypto.enableEncryption('dup-test', 'password')
try {
await crypto.enableEncryption('dup-test', 'password')
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('already has encryption'), 'throws on duplicate')
}
})
test('getNotebookKey — correct password returns key', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const originalKey = await crypto.enableEncryption('key-test', 'correct-password')
const retrievedKey = await crypto.getNotebookKey('key-test', 'correct-password')
t.alike(retrievedKey, originalKey, 'retrieved key matches original')
})
test('getNotebookKey — wrong password throws', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
await crypto.enableEncryption('wrong-pw', 'correct')
try {
await crypto.getNotebookKey('wrong-pw', 'incorrect')
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('wrong password'), 'throws on wrong password')
}
})
test('getNotebookKey — nonexistent notebook throws', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
try {
await crypto.getNotebookKey('nonexistent', 'password')
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('No encryption key'), 'throws for nonexistent')
}
})
test('isEncrypted — returns true/false', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
t.is(await crypto.isEncrypted('not-encrypted'), false)
await crypto.enableEncryption('is-encrypted', 'password')
t.is(await crypto.isEncrypted('is-encrypted'), true)
})
test('disableEncryption — removes key, requires correct password', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('disable-test', 'mypassword')
// Wrong password fails
try {
await crypto.disableEncryption('disable-test', 'wrongpassword')
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('wrong password'))
}
// Key still exists
t.is(await crypto.isEncrypted('disable-test'), true)
// Correct password removes it
const returnedKey = await crypto.disableEncryption('disable-test', 'mypassword')
t.alike(returnedKey, key, 'returns the notebook key')
t.is(await crypto.isEncrypted('disable-test'), false)
})
test('enableEncryption — accepts string password', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('str-pw', 'string-password')
const retrieved = await crypto.getNotebookKey('str-pw', 'string-password')
t.alike(retrieved, key)
})
test('enableEncryption — accepts Buffer password', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const pwd = Buffer.from('buffer-password')
const key = await crypto.enableEncryption('buf-pw', pwd)
const retrieved = await crypto.getNotebookKey('buf-pw', pwd)
t.alike(retrieved, key)
})
// ==================== INTEGRATION: ENCRYPT + DECRYPT FIELD ====================
test('full flow — enable, encrypt fields, retrieve key, decrypt fields', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const password = 'my-secure-password'
const key = await crypto.enableEncryption('full-flow', password)
// Encrypt some fields
const encTitle = crypto.encryptField('Secret Title', key)
const encContent = crypto.encryptField('This is the secret content of my note.', key)
// Later — retrieve key with password
const key2 = await crypto.getNotebookKey('full-flow', password)
// Decrypt
t.is(crypto.decryptField(encTitle, key2), 'Secret Title')
t.is(crypto.decryptField(encContent, key2), 'This is the secret content of my note.')
})
test('decryptField — wrong key fails', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key1 = await crypto.enableEncryption('key1', 'pass1')
const key2 = await crypto.enableEncryption('key2', 'pass2')
const encrypted = crypto.encryptField('secret', key1)
try {
crypto.decryptField(encrypted, key2)
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('decryption failed') || err.message.includes('wrong key'), 'wrong key throws')
}
})
test('decryptField — invalid format throws', async (t) => {
const registry = new MockRegistry()
const crypto = new NotebookCrypto(registry)
const key = await crypto.enableEncryption('fmt-test', 'password')
try {
crypto.decryptField('not-valid-encrypted-data', key)
t.fail('should have thrown')
} catch (err) {
t.ok(err.message, 'throws on invalid format')
}
})