39 lines
1.8 KiB
TypeScript
39 lines
1.8 KiB
TypeScript
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')
|
|
}
|