You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/src/crypto.ts

34 lines
1.5 KiB
TypeScript

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
/**
* AES-256-GCM helpers for the Gmail refresh token at rest. The key is
* HQ_SECRET_KEY (64 hex chars = 32 bytes); payload format is
* `iv.tag.ciphertext` hex-joined by `.` so it stores in one TEXT column.
* Never log the plaintext or the key.
*/
function keyFromHex(keyHex: string): Buffer {
const key = Buffer.from(keyHex, 'hex')
if (key.length !== 32 || keyHex.length !== 64) {
throw new Error('Encryption key must be 64 hex chars (32 bytes)')
}
return key
}
export function encrypt(plain: string, keyHex: string): string {
const iv = randomBytes(12) // 96-bit nonce, the GCM-recommended size
const cipher = createCipheriv('aes-256-gcm', keyFromHex(keyHex), iv)
const ciphertext = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()])
return [iv.toString('hex'), cipher.getAuthTag().toString('hex'), ciphertext.toString('hex')].join('.')
}
export function decrypt(payload: string, keyHex: string): string {
const [ivHex, tagHex, ctHex] = payload.split('.')
if (ivHex === undefined || tagHex === undefined || ctHex === undefined) {
throw new Error('Malformed encrypted payload (expected iv.tag.ciphertext)')
}
const decipher = createDecipheriv('aes-256-gcm', keyFromHex(keyHex), Buffer.from(ivHex, 'hex'))
decipher.setAuthTag(Buffer.from(tagHex, 'hex'))
return Buffer.concat([decipher.update(Buffer.from(ctHex, 'hex')), decipher.final()]).toString('utf8')
}