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.
34 lines
1.5 KiB
TypeScript
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')
|
|
}
|