|
1 | 1 | import { |
2 | 2 | createCipheriv, |
3 | 3 | createDecipheriv, |
| 4 | + randomBytes, |
4 | 5 | } from 'crypto'; |
5 | 6 | import nconf from 'nconf'; |
6 | 7 |
|
7 | | -const algorithm = 'aes-256-ctr'; |
| 8 | +const ALGORITHM = 'aes-256-gcm'; |
| 9 | +const IV_LENGTH_BYTES = 12; // 96-bit nonce per NIST guidance for GCM |
| 10 | +const AUTH_TAG_LENGTH_BYTES = 16; // 128-bit authentication tag |
8 | 11 | const SESSION_SECRET_KEY = nconf.get('SESSION_SECRET_KEY'); |
9 | | -const SESSION_SECRET_IV = nconf.get('SESSION_SECRET_IV'); |
10 | 12 |
|
11 | 13 | const key = Buffer.from(SESSION_SECRET_KEY, 'hex'); |
12 | | -const iv = Buffer.from(SESSION_SECRET_IV, 'hex'); |
13 | 14 |
|
| 15 | +/** |
| 16 | + * Encrypt a UTF-8 string using AES-256-GCM and return iv|ciphertext|tag as hex. |
| 17 | + * A fresh nonce is generated for every message to avoid keystream reuse, and |
| 18 | + * the auth tag ensures forged payloads are rejected at the trust boundary. |
| 19 | + */ |
14 | 20 | export function encrypt (text) { |
15 | | - const cipher = createCipheriv(algorithm, key, iv); |
16 | | - let crypted = cipher.update(text, 'utf8', 'hex'); |
17 | | - crypted += cipher.final('hex'); |
18 | | - return crypted; |
| 21 | + const iv = randomBytes(IV_LENGTH_BYTES); |
| 22 | + const cipher = createCipheriv(ALGORITHM, key, iv); |
| 23 | + const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); |
| 24 | + const authTag = cipher.getAuthTag(); |
| 25 | + return Buffer.concat([iv, ciphertext, authTag]).toString('hex'); |
19 | 26 | } |
20 | 27 |
|
| 28 | +/** |
| 29 | + * Decrypt an AES-256-GCM payload previously produced by encrypt(). |
| 30 | + * The layout is iv (12B) || ciphertext || authTag (16B), all hex encoded. |
| 31 | + */ |
21 | 32 | export function decrypt (text) { |
22 | | - const decipher = createDecipheriv(algorithm, key, iv); |
23 | | - let dec = decipher.update(text, 'hex', 'utf8'); |
24 | | - dec += decipher.final('utf8'); |
25 | | - return dec; |
| 33 | + const payload = Buffer.from(text, 'hex'); |
| 34 | + if (payload.length <= IV_LENGTH_BYTES + AUTH_TAG_LENGTH_BYTES) { |
| 35 | + throw new Error('Encrypted payload is malformed'); |
| 36 | + } |
| 37 | + |
| 38 | + const iv = payload.subarray(0, IV_LENGTH_BYTES); |
| 39 | + const authTag = payload.subarray(payload.length - AUTH_TAG_LENGTH_BYTES); |
| 40 | + const ciphertext = payload.subarray(IV_LENGTH_BYTES, payload.length - AUTH_TAG_LENGTH_BYTES); |
| 41 | + |
| 42 | + const decipher = createDecipheriv(ALGORITHM, key, iv); |
| 43 | + decipher.setAuthTag(authTag); |
| 44 | + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); |
| 45 | + return decrypted.toString('utf8'); |
26 | 46 | } |
0 commit comments