A framework-agnostic encryption library for Node.js. Built with simplicity and security in mind, @boringnode/encryption provides a unified API for encrypting and signing data with support for multiple encryption algorithms and key rotation.
npm install @boringnode/encryption
- Multiple Algorithms: ChaCha20-Poly1305, AES-256-GCM, AES-256-CBC, AES-SIV
- Key Rotation: Encrypt with new keys, decrypt with old ones
- Deterministic Encryption: AES-SIV driver for equality queries
- Purpose-Bound Encryption: Ensure encrypted values are used for their intended purpose
- Expiration Support: Set time-to-live on encrypted values
- Blind Indexes: Deterministic indexes for equality queries
- Message Verification: Sign data without encrypting (HMAC-based)
- Type-Safe: Full TypeScript support with typed payloads
import { Encryption } from '@boringnode/encryption' import { chacha20poly1305 } from '@boringnode/encryption/drivers/chacha20_poly1305' const encryption = new Encryption( chacha20poly1305({ id: 'app', keys: [process.env.APP_KEY], }) )
id must be a non-empty string and cannot contain ..
// Encrypt any value const encrypted = encryption.encrypt({ userId: 1, role: 'admin' }) // => "app.base64EncodedCipherText.base64EncodedIv.base64EncodedTag" // Decrypt the value const decrypted = encryption.decrypt(encrypted) // => { userId: 1, role: 'admin' }
The library supports encrypting a wide range of data types:
- Strings
- Numbers
- Booleans
- Arrays
- Objects
- Dates
Modern, fast, and secure. Recommended for most use cases.
import { chacha20poly1305 } from '@boringnode/encryption/drivers/chacha20_poly1305' const config = chacha20poly1305({ id: 'app', keys: ['your-32-character-secret-key-here'], })
Industry-standard authenticated encryption.
import { aes256gcm } from '@boringnode/encryption/drivers/aes_256_gcm' const config = aes256gcm({ id: 'app', keys: ['your-32-character-secret-key-here'], })
Legacy support with HMAC authentication.
import { aes256cbc } from '@boringnode/encryption/drivers/aes_256_cbc' const config = aes256cbc({ id: 'app', keys: ['your-32-character-secret-key-here'], })
Deterministic encryption for direct equality lookups on encrypted columns.
import { aessiv } from '@boringnode/encryption/drivers/aes_siv' const config = aessiv({ id: 'app', key: 'your-32-character-secret-key-here', })
Notes:
expiresInis not supported with deterministic encryption.- Key rotation is not automatic for deterministic ciphertexts. Use an explicit migration/backfill strategy.
The library supports multiple keys for seamless key rotation. The first key is used for encryption, while all keys are tried during decryption.
const encryption = new Encryption( chacha20poly1305({ id: 'app', keys: [ process.env.NEW_APP_KEY, // Used for encryption process.env.OLD_APP_KEY, // Still valid for decryption ], }) ) // New encryptions use NEW_APP_KEY const encrypted = encryption.encrypt('secret') // Decryption works with both keys encryption.decrypt(encryptedWithOldKey) // Works encryption.decrypt(encryptedWithNewKey) // Works
Ensure encrypted values are only used for their intended purpose:
// Encrypt with a purpose const token = encryption.encrypt({ userId: 1 }, undefined, 'password-reset') // Must provide same purpose to decrypt encryption.decrypt(token, 'password-reset') // => { userId: 1 } encryption.decrypt(token, 'email-verify') // => null encryption.decrypt(token) // => null
Set a time-to-live on encrypted values:
// Expires in 1 hour const token = encryption.encrypt({ userId: 1 }, '1h') // Expires in 30 minutes const token = encryption.encrypt({ userId: 1 }, '30m') // Expires in 7 days const token = encryption.encrypt({ userId: 1 }, '7d') // After expiration, decrypt returns null encryption.decrypt(expiredToken) // => null
Blind indexes are deterministic hashes used for equality queries:
const index = encryption.blindIndex('[email protected]', 'users.email')
When rotating keys, query using all blind indexes:
const indexes = encryption.blindIndexes('[email protected]', 'users.email') // Use SQL: WHERE email_index IN (...)
Rules:
purposeis required and should identify the field/context (users.email,users.ssn, ...).- Matching is exact-bytes (no implicit normalization).
- Prefer normalized primitive values for blind indexes (
string/number/boolean/ISO date). - For structured objects, normalize/canonicalize before indexing (for example, map object -> stable string yourself).
const emailIndex = encryption.blindIndex(email.trim().toLowerCase(), 'users.email')
When you need to ensure data integrity without hiding the content, use the MessageVerifier. The payload is base64-encoded (not encrypted) and signed with HMAC.
import { MessageVerifier } from '@boringnode/encryption/message_verifier' const verifier = new MessageVerifier(['your-32-character-secret-key-here']) // Sign a value const signed = verifier.sign({ userId: 1 }) // Verify and retrieve the value const payload = verifier.unsign(signed) // => { userId: 1 } // Tampered values return null verifier.unsign('tampered.value') // => null
The verifier also supports purpose and expiration:
// With expiration const signed = verifier.sign({ userId: 1 }, '1h') // With purpose const signed = verifier.sign({ userId: 1 }, undefined, 'api-token') const payload = verifier.unsign(signed, 'api-token')
URL-safe base64 encoding/decoding utilities are available:
import { base64UrlEncode, base64UrlDecode } from '@boringnode/encryption/base64' const encoded = base64UrlEncode('Hello World') const decoded = base64UrlDecode(encoded, 'utf8')
Generate and verify HMAC signatures:
import { Hmac } from '@boringnode/encryption' const hmac = new Hmac(secretKey) // Generate HMAC const hash = hmac.generate('data to sign') // Verify HMAC (timing-safe comparison) const isValid = hmac.compare('data to sign', hash)
The library is designed to return null on decryption failures rather than throwing exceptions. This prevents timing attacks and simplifies error handling:
const result = encryption.decrypt(maybeInvalidValue) if (result === null) { // Invalid, expired, wrong purpose, or tampered }