72 lines
2.7 KiB
JavaScript
72 lines
2.7 KiB
JavaScript
// Preview-Variablen: Verschluesselung (AES-256-GCM), Validierung, Log-Maskierung
|
|
import crypto from 'node:crypto'
|
|
|
|
const NAME_RE = /^[A-Z_][A-Z0-9_]*$/
|
|
const MAX_VARS = 30
|
|
const MAX_BYTES = 4096
|
|
|
|
// Schluessel aus PREVIEW_ENV_KEY (base64, genau 32 Byte) oder null
|
|
export function envKey() {
|
|
const raw = process.env.PREVIEW_ENV_KEY
|
|
if (!raw) return null
|
|
const buf = Buffer.from(raw, 'base64')
|
|
return buf.length === 32 ? buf : null
|
|
}
|
|
|
|
export function encryptEnv(obj, key) {
|
|
const iv = crypto.randomBytes(12)
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
const ct = Buffer.concat([cipher.update(JSON.stringify(obj || {}), 'utf8'), cipher.final()])
|
|
const tag = cipher.getAuthTag()
|
|
return ['v1', iv, tag, ct].map((p) => (typeof p === 'string' ? p : p.toString('base64'))).join(':')
|
|
}
|
|
|
|
export function decryptEnv(str, key) {
|
|
if (!str) return {}
|
|
const parts = String(str).split(':')
|
|
if (parts.length !== 4 || parts[0] !== 'v1') throw new Error('Ungueltiges Variablenformat')
|
|
const [, iv, tag, ct] = parts.map((p, i) => (i === 0 ? p : Buffer.from(p, 'base64')))
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
decipher.setAuthTag(tag)
|
|
const plain = Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8')
|
|
return JSON.parse(plain)
|
|
}
|
|
|
|
export function envNames(str, key) {
|
|
return Object.keys(decryptEnv(str, key)).sort()
|
|
}
|
|
|
|
export function validateEnvChange(current, { set = {}, remove = [] } = {}) {
|
|
const env = { ...(current || {}) }
|
|
for (const name of remove || []) delete env[name]
|
|
for (const [name, value] of Object.entries(set || {})) {
|
|
if (!NAME_RE.test(name)) {
|
|
return { ok: false, error: `Ungültiger Variablenname „${name}“ (erlaubt: Großbuchstaben, Ziffern, Unterstrich; nicht mit Ziffer beginnen).` }
|
|
}
|
|
if (typeof value !== 'string') {
|
|
return { ok: false, error: `Der Wert von ${name} muss Text sein.` }
|
|
}
|
|
if (Buffer.byteLength(value, 'utf8') > MAX_BYTES) {
|
|
return { ok: false, error: `Der Wert von ${name} ist zu lang (maximal ${MAX_BYTES} Byte).` }
|
|
}
|
|
if (/[\0\r\n]/.test(value)) {
|
|
return { ok: false, error: `Der Wert von ${name} darf keine Zeilenumbrüche oder Nullzeichen enthalten.` }
|
|
}
|
|
env[name] = value
|
|
}
|
|
if (Object.keys(env).length > MAX_VARS) {
|
|
return { ok: false, error: `Zu viele Variablen (maximal ${MAX_VARS}).` }
|
|
}
|
|
return { ok: true, env }
|
|
}
|
|
|
|
// Werte (>= 4 Zeichen) im Protokoll durch ••• ersetzen, laengste zuerst
|
|
export function maskLog(text, env) {
|
|
let out = String(text ?? '')
|
|
const values = Object.values(env || {})
|
|
.filter((v) => typeof v === 'string' && v.length >= 4)
|
|
.sort((a, b) => b.length - a.length)
|
|
for (const v of values) out = out.split(v).join('•••')
|
|
return out
|
|
}
|