Preview: Variablen verschluesselt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e358514240
commit
20ca1de694
2 files changed
+152
No files matched your search
@@ -0,0 +1,71 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user