|
| 1 | +# @boringnode/encryption |
| 2 | + |
| 3 | +<div align="center"> |
| 4 | + |
| 5 | +[![typescript-image]][typescript-url] |
| 6 | +[![gh-workflow-image]][gh-workflow-url] |
| 7 | +[![npm-image]][npm-url] |
| 8 | +[![npm-download-image]][npm-download-url] |
| 9 | +[![license-image]][license-url] |
| 10 | + |
| 11 | +</div> |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +```bash |
| 18 | +npm install @boringnode/encryption |
| 19 | +``` |
| 20 | + |
| 21 | +## Features |
| 22 | + |
| 23 | +- **Multiple Algorithms**: ChaCha20-Poly1305, AES-256-GCM, AES-256-CBC |
| 24 | +- **Key Rotation**: Encrypt with new keys, decrypt with old ones |
| 25 | +- **Purpose-Bound Encryption**: Ensure encrypted values are used for their intended purpose |
| 26 | +- **Expiration Support**: Set time-to-live on encrypted values |
| 27 | +- **Message Verification**: Sign data without encrypting (HMAC-based) |
| 28 | +- **Type-Safe**: Full TypeScript support with typed payloads |
| 29 | + |
| 30 | +## Quick Start |
| 31 | + |
| 32 | +### 1. Configure Encryption |
| 33 | + |
| 34 | +```typescript |
| 35 | +import { Encryption } from '@boringnode/encryption' |
| 36 | +import { chacha20poly1305 } from '@boringnode/encryption/drivers/chacha20_poly1305' |
| 37 | + |
| 38 | +const encryption = new Encryption( |
| 39 | + chacha20poly1305({ |
| 40 | + id: 'app', |
| 41 | + keys: [process.env.APP_KEY], |
| 42 | + }) |
| 43 | +) |
| 44 | +``` |
| 45 | + |
| 46 | +### 2. Encrypt & Decrypt |
| 47 | + |
| 48 | +```typescript |
| 49 | +// Encrypt any value |
| 50 | +const encrypted = encryption.encrypt({ userId: 1, role: 'admin' }) |
| 51 | +// => "app.base64EncodedCipherText.base64EncodedIv.base64EncodedTag" |
| 52 | + |
| 53 | +// Decrypt the value |
| 54 | +const decrypted = encryption.decrypt(encrypted) |
| 55 | +// => { userId: 1, role: 'admin' } |
| 56 | +``` |
| 57 | + |
| 58 | +## Supported Data Types |
| 59 | + |
| 60 | +The library supports encrypting a wide range of data types: |
| 61 | + |
| 62 | +- Strings |
| 63 | +- Numbers |
| 64 | +- Booleans |
| 65 | +- Arrays |
| 66 | +- Objects |
| 67 | +- Dates |
| 68 | + |
| 69 | +## Encryption Drivers |
| 70 | + |
| 71 | +### ChaCha20-Poly1305 (recommended) |
| 72 | + |
| 73 | +Modern, fast, and secure. Recommended for most use cases. |
| 74 | + |
| 75 | +```typescript |
| 76 | +import { chacha20poly1305 } from '@boringnode/encryption/drivers/chacha20_poly1305' |
| 77 | + |
| 78 | +const config = chacha20poly1305({ |
| 79 | + id: 'app', |
| 80 | + keys: ['your-32-character-secret-key-here'], |
| 81 | +}) |
| 82 | +``` |
| 83 | + |
| 84 | +### AES-256-GCM |
| 85 | + |
| 86 | +Industry-standard authenticated encryption. |
| 87 | + |
| 88 | +```typescript |
| 89 | +import { aes256gcm } from '@boringnode/encryption/drivers/aes_256_gcm' |
| 90 | + |
| 91 | +const config = aes256gcm({ |
| 92 | + id: 'app', |
| 93 | + keys: ['your-32-character-secret-key-here'], |
| 94 | +}) |
| 95 | +``` |
| 96 | + |
| 97 | +### AES-256-CBC |
| 98 | + |
| 99 | +Legacy support with HMAC authentication. |
| 100 | + |
| 101 | +```typescript |
| 102 | +import { aes256cbc } from '@boringnode/encryption/drivers/aes_256_cbc' |
| 103 | + |
| 104 | +const config = aes256cbc({ |
| 105 | + id: 'app', |
| 106 | + keys: ['your-32-character-secret-key-here'], |
| 107 | +}) |
| 108 | +``` |
| 109 | + |
| 110 | +## Key Rotation |
| 111 | + |
| 112 | +The library supports multiple keys for seamless key rotation. The first key is used for encryption, while all keys are tried during decryption. |
| 113 | + |
| 114 | +```typescript |
| 115 | +const encryption = new Encryption( |
| 116 | + chacha20poly1305({ |
| 117 | + id: 'app', |
| 118 | + keys: [ |
| 119 | + process.env.NEW_APP_KEY, // Used for encryption |
| 120 | + process.env.OLD_APP_KEY, // Still valid for decryption |
| 121 | + ], |
| 122 | + }) |
| 123 | +) |
| 124 | + |
| 125 | +// New encryptions use NEW_APP_KEY |
| 126 | +const encrypted = encryption.encrypt('secret') |
| 127 | + |
| 128 | +// Decryption works with both keys |
| 129 | +encryption.decrypt(encryptedWithOldKey) // Works |
| 130 | +encryption.decrypt(encryptedWithNewKey) // Works |
| 131 | +``` |
| 132 | + |
| 133 | +## Purpose-Bound Encryption |
| 134 | + |
| 135 | +Ensure encrypted values are only used for their intended purpose: |
| 136 | + |
| 137 | +```typescript |
| 138 | +// Encrypt with a purpose |
| 139 | +const token = encryption.encrypt({ userId: 1 }, undefined, 'password-reset') |
| 140 | + |
| 141 | +// Must provide same purpose to decrypt |
| 142 | +encryption.decrypt(token, 'password-reset') // => { userId: 1 } |
| 143 | +encryption.decrypt(token, 'email-verify') // => null |
| 144 | +encryption.decrypt(token) // => null |
| 145 | +``` |
| 146 | + |
| 147 | +## Expiration Support |
| 148 | + |
| 149 | +Set a time-to-live on encrypted values: |
| 150 | + |
| 151 | +```typescript |
| 152 | +// Expires in 1 hour |
| 153 | +const token = encryption.encrypt({ userId: 1 }, '1h') |
| 154 | + |
| 155 | +// Expires in 30 minutes |
| 156 | +const token = encryption.encrypt({ userId: 1 }, '30m') |
| 157 | + |
| 158 | +// Expires in 7 days |
| 159 | +const token = encryption.encrypt({ userId: 1 }, '7d') |
| 160 | + |
| 161 | +// After expiration, decrypt returns null |
| 162 | +encryption.decrypt(expiredToken) // => null |
| 163 | +``` |
| 164 | + |
| 165 | +## Message Verifier |
| 166 | + |
| 167 | +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. |
| 168 | + |
| 169 | +```typescript |
| 170 | +import { MessageVerifier } from '@boringnode/encryption/message_verifier' |
| 171 | + |
| 172 | +const verifier = new MessageVerifier(['your-secret-key']) |
| 173 | + |
| 174 | +// Sign a value |
| 175 | +const signed = verifier.sign({ userId: 1 }) |
| 176 | + |
| 177 | +// Verify and retrieve the value |
| 178 | +const payload = verifier.unsign(signed) |
| 179 | +// => { userId: 1 } |
| 180 | + |
| 181 | +// Tampered values return null |
| 182 | +verifier.unsign('tampered.value') // => null |
| 183 | +``` |
| 184 | + |
| 185 | +The verifier also supports purpose and expiration: |
| 186 | + |
| 187 | +```typescript |
| 188 | +// With expiration |
| 189 | +const signed = verifier.sign({ userId: 1 }, '1h') |
| 190 | + |
| 191 | +// With purpose |
| 192 | +const signed = verifier.sign({ userId: 1 }, undefined, 'api-token') |
| 193 | +const payload = verifier.unsign(signed, 'api-token') |
| 194 | +``` |
| 195 | + |
| 196 | +## Base64 Utilities |
| 197 | + |
| 198 | +URL-safe base64 encoding/decoding utilities are available: |
| 199 | + |
| 200 | +```typescript |
| 201 | +import { base64UrlEncode, base64UrlDecode } from '@boringnode/encryption/base64' |
| 202 | + |
| 203 | +const encoded = base64UrlEncode('Hello World') |
| 204 | +const decoded = base64UrlDecode(encoded, 'utf8') |
| 205 | +``` |
| 206 | + |
| 207 | +## HMAC |
| 208 | + |
| 209 | +Generate and verify HMAC signatures: |
| 210 | + |
| 211 | +```typescript |
| 212 | +import { Hmac } from '@boringnode/encryption' |
| 213 | + |
| 214 | +const hmac = new Hmac(secretKey) |
| 215 | + |
| 216 | +// Generate HMAC |
| 217 | +const hash = hmac.generate('data to sign') |
| 218 | + |
| 219 | +// Verify HMAC (timing-safe comparison) |
| 220 | +const isValid = hmac.compare('data to sign', hash) |
| 221 | +``` |
| 222 | + |
| 223 | +## Error Handling |
| 224 | + |
| 225 | +The library is designed to return `null` on decryption failures rather than throwing exceptions. This prevents timing attacks and simplifies error handling: |
| 226 | + |
| 227 | +```typescript |
| 228 | +const result = encryption.decrypt(maybeInvalidValue) |
| 229 | + |
| 230 | +if (result === null) { |
| 231 | + // Invalid, expired, wrong purpose, or tampered |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/boringnode/encryption/checks.yml?branch=main&style=for-the-badge |
| 236 | +[gh-workflow-url]: https://github.com/boringnode/encryption/actions/workflows/checks.yml |
| 237 | +[npm-image]: https://img.shields.io/npm/v/@boringnode/encryption.svg?style=for-the-badge&logo=npm |
| 238 | +[npm-url]: https://www.npmjs.com/package/@boringnode/encryption |
| 239 | +[npm-download-image]: https://img.shields.io/npm/dm/@boringnode/encryption?style=for-the-badge |
| 240 | +[npm-download-url]: https://www.npmjs.com/package/@boringnode/encryption |
| 241 | +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript |
| 242 | +[typescript-url]: https://www.typescriptlang.org |
| 243 | +[license-image]: https://img.shields.io/npm/l/@boringnode/encryption?color=blueviolet&style=for-the-badge |
| 244 | +[license-url]: LICENSE.md |
0 commit comments