Files
habitica/website/server/libs/encryption.js
2026-03-12 14:53:30 -05:00

47 lines
1.7 KiB
JavaScript

import {
createCipheriv,
createDecipheriv,
randomBytes,
} from 'crypto';
import nconf from 'nconf';
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 key = Buffer.from(SESSION_SECRET_KEY, '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 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 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');
}