import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto' import { readFileSync } from 'node:fs' const ALGORITHM = 'aes-256-gcm' const IV_BYTES = 12 const TAG_BYTES = 16 /** * The key of the secrets: the SHA-256 of ENCRYPTION_KEY, else of the content of the file ENCRYPTION_KEY_FILE names * without its final newline; null when neither is set. A file that cannot be read throws with the reason. */ export function encryptionKey(): Buffer | null { let secret = process.env.ENCRYPTION_KEY if (!secret && process.env.ENCRYPTION_KEY_FILE) { try { secret = readFileSync(process.env.ENCRYPTION_KEY_FILE, 'utf8').replace(/\r?\n$/, '') } catch (error) { throw new Error(`ENCRYPTION_KEY_FILE ${process.env.ENCRYPTION_KEY_FILE} cannot be read: ${(error as Error).message}`) } } return secret ? createHash('sha256').update(secret, 'utf8').digest() : null } /** The text encrypted with AES-256-GCM: base64 of a random IV, the ciphertext and the authentication tag. */ export function encrypt(text: string, key: Buffer): string { const iv = randomBytes(IV_BYTES) const cipher = createCipheriv(ALGORITHM, key, iv) const data = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]) return Buffer.concat([iv, data, cipher.getAuthTag()]).toString('base64') } /** The text of encrypt's answer; another key, or a changed answer, throws. */ export function decrypt(encrypted: string, key: Buffer): string { const bytes = Buffer.from(encrypted, 'base64') const decipher = createDecipheriv(ALGORITHM, key, bytes.subarray(0, IV_BYTES)) decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES)) return Buffer.concat([decipher.update(bytes.subarray(IV_BYTES, bytes.length - TAG_BYTES)), decipher.final()]).toString('utf8') }