mirror your GitHub repos to tangled.org automatically
1import crypto from 'node:crypto'
2import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
3
4/**
5 * Authenticated encryption with a key from runtime config (`NUXT_ENCRYPTION_KEY`,
6 * base64-encoded 32 bytes). Used to wrap anything sensitive at the app layer
7 * before it lands in the DB: AT Proto session blobs, SSH private keys.
8 *
9 * The KEK is held only in env. If it's lost, every encrypted row becomes
10 * unreadable. KEK rotation is a future concern; see PLAN.md.
11 */
12const NONCE_BYTES = 24
13let cachedKey: Uint8Array | undefined
14
15function getKey(): Uint8Array {
16 if (cachedKey) return cachedKey
17 // Read process.env directly rather than via useRuntimeConfig() so this helper
18 // is callable from outside a Nitro request context (e.g. tests, scripts).
19 // Nuxt's runtime config still declares the var for documentation; the env
20 // name is the same.
21 const raw = process.env.NUXT_ENCRYPTION_KEY
22 if (!raw) {
23 throw new Error('NUXT_ENCRYPTION_KEY is not set (expected base64-encoded 32 bytes)')
24 }
25 const decoded = Buffer.from(raw, 'base64')
26 if (decoded.length !== 32) {
27 throw new Error(`NUXT_ENCRYPTION_KEY must decode to 32 bytes, got ${decoded.length}`)
28 }
29 cachedKey = new Uint8Array(decoded)
30 return cachedKey
31}
32
33export function encrypt(plaintext: string): { ciphertext: Buffer, nonce: Buffer } {
34 const nonce = crypto.randomBytes(NONCE_BYTES)
35 const cipher = xchacha20poly1305(getKey(), new Uint8Array(nonce))
36 const ciphertext = cipher.encrypt(new TextEncoder().encode(plaintext))
37 return { ciphertext: Buffer.from(ciphertext), nonce }
38}
39
40export function decrypt(ciphertext: Buffer, nonce: Buffer): string {
41 const cipher = xchacha20poly1305(getKey(), new Uint8Array(nonce))
42 const plaintext = cipher.decrypt(new Uint8Array(ciphertext))
43 return new TextDecoder().decode(plaintext)
44}
45
46/** Test/utility hook: drop the cached key so the next call re-reads runtime config. */
47export function clearEncryptionKeyCache() {
48 cachedKey = undefined
49}