diff --git a/config.json.example b/config.json.example index 8a8a092ef0..d20fd328f1 100644 --- a/config.json.example +++ b/config.json.example @@ -75,7 +75,6 @@ "S3_ACCESS_KEY_ID": "accessKeyId", "S3_BUCKET": "bucket", "S3_SECRET_ACCESS_KEY": "secretAccessKey", - "SESSION_SECRET_IV": "12345678912345678912345678912345", "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", "SESSION_SECRET": "YOUR SECRET HERE", "SITE_HTTP_AUTH_ENABLED": "false", diff --git a/website/server/libs/encryption.js b/website/server/libs/encryption.js index a9779044c9..91ec6349bb 100644 --- a/website/server/libs/encryption.js +++ b/website/server/libs/encryption.js @@ -1,26 +1,46 @@ import { createCipheriv, createDecipheriv, + randomBytes, } from 'crypto'; import nconf from 'nconf'; -const algorithm = 'aes-256-ctr'; +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH_BYTES = 12; // 96-bit nonce per NIST guidance for GCM +const AUTH_TAG_LENGTH_BYTES = 16; // 128-bit authentication tag const SESSION_SECRET_KEY = nconf.get('SESSION_SECRET_KEY'); -const SESSION_SECRET_IV = nconf.get('SESSION_SECRET_IV'); const key = Buffer.from(SESSION_SECRET_KEY, 'hex'); -const iv = Buffer.from(SESSION_SECRET_IV, 'hex'); +/** + * Encrypt a UTF-8 string using AES-256-GCM and return iv|ciphertext|tag as hex. + * A fresh nonce is generated for every message to avoid keystream reuse, and + * the auth tag ensures forged payloads are rejected at the trust boundary. + */ export function encrypt (text) { - const cipher = createCipheriv(algorithm, key, iv); - let crypted = cipher.update(text, 'utf8', 'hex'); - crypted += cipher.final('hex'); - return crypted; + const iv = randomBytes(IV_LENGTH_BYTES); + const cipher = createCipheriv(ALGORITHM, key, iv); + const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return Buffer.concat([iv, ciphertext, authTag]).toString('hex'); } +/** + * Decrypt an AES-256-GCM payload previously produced by encrypt(). + * The layout is iv (12B) || ciphertext || authTag (16B), all hex encoded. + */ export function decrypt (text) { - const decipher = createDecipheriv(algorithm, key, iv); - let dec = decipher.update(text, 'hex', 'utf8'); - dec += decipher.final('utf8'); - return dec; + const payload = Buffer.from(text, 'hex'); + if (payload.length <= IV_LENGTH_BYTES + AUTH_TAG_LENGTH_BYTES) { + throw new Error('Encrypted payload is malformed'); + } + + const iv = payload.subarray(0, IV_LENGTH_BYTES); + const authTag = payload.subarray(payload.length - AUTH_TAG_LENGTH_BYTES); + const ciphertext = payload.subarray(IV_LENGTH_BYTES, payload.length - AUTH_TAG_LENGTH_BYTES); + + const decipher = createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + return decrypted.toString('utf8'); }