Add support for multiple encryption keys in data handling
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 1m1s

This commit introduces a mechanism to handle multiple possible encryption keys for data decryption across various modules, including auth.js, members.js, newsletter.js, and encryption.js. It adds functions to retrieve potential old keys for migration purposes and updates the decryption logic to attempt decryption with these keys. Additionally, it includes warnings for users when old keys are used and provides guidance for re-encrypting data. This enhancement improves data migration capabilities and ensures backward compatibility with previously encrypted data.
This commit is contained in:
Torsten Schulz (local)
2026-01-09 09:05:05 +01:00
parent dee760d51a
commit 01cf0e58cb
4 changed files with 324 additions and 66 deletions

View File

@@ -52,6 +52,30 @@ function getEncryptionKey() {
return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
}
// Liste möglicher alter Verschlüsselungsschlüssel (für Migration)
function getPossibleEncryptionKeys() {
const currentKey = getEncryptionKey()
const oldKeys = [
'default-key-change-in-production',
'local_development_encryption_key_change_in_production'
]
// Aktueller Schlüssel zuerst, dann alte Schlüssel
const keys = [currentKey]
for (const oldKey of oldKeys) {
if (oldKey !== currentKey) {
keys.push(oldKey)
}
}
// Optional: Alter Schlüssel aus Environment-Variable
if (process.env.OLD_ENCRYPTION_KEY && process.env.OLD_ENCRYPTION_KEY !== currentKey) {
keys.push(process.env.OLD_ENCRYPTION_KEY)
}
return keys
}
// Check if data is encrypted by trying to parse as JSON first
function isEncrypted(data) {
try {
@@ -78,11 +102,47 @@ export async function readUsers() {
let users = []
if (encrypted) {
// Versuche mit verschiedenen Schlüsseln zu entschlüsseln (für Migration)
const possibleKeys = getPossibleEncryptionKeys()
const encryptionKey = getEncryptionKey()
try {
users = decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Benutzerdaten:', decryptError)
let lastError = null
for (let i = 0; i < possibleKeys.length; i++) {
const key = possibleKeys[i]
try {
users = decryptObject(data, key)
// Wenn mit altem Schlüssel entschlüsselt wurde, warnen und neu verschlüsseln
if (i > 0 && key !== encryptionKey) {
console.warn(`⚠️ Benutzerdaten wurden mit altem Schlüssel entschlüsselt. Automatische Neuverschlüsselung...`)
try {
await writeUsers(users)
console.log('✅ Benutzerdaten erfolgreich mit neuem Schlüssel neu verschlüsselt')
} catch (writeError) {
console.error('❌ Fehler beim Neuverschlüsseln:', writeError.message)
console.error(' Bitte führen Sie manuell aus: node scripts/re-encrypt-data.js')
}
}
break // Erfolgreich entschlüsselt
} catch (decryptError) {
lastError = decryptError
// Versuche nächsten Schlüssel
continue
}
}
// Wenn alle Schlüssel fehlgeschlagen sind
if (!users || users.length === 0) {
console.error('Fehler beim Entschlüsseln der Benutzerdaten:')
console.error(' Versuchte Schlüssel:', possibleKeys.length)
console.error(' Letzter Fehler:', lastError?.message || 'Unbekannter Fehler')
console.error('')
console.error('💡 Lösung: Führen Sie das Re-Encrypt-Skript aus:')
console.error(' node scripts/re-encrypt-data.js --old-key="<alter-schlüssel>"')
console.error(' Oder setzen Sie OLD_ENCRYPTION_KEY als Environment-Variable')
// Fallback: try to read as plain JSON
try {
users = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')

View File

@@ -48,50 +48,81 @@ function encryptV2GCM(text, password) {
}
function decryptLegacyCBC(encryptedData, password) {
// Base64 dekodieren
const combined = Buffer.from(encryptedData, 'base64')
try {
// Base64 dekodieren
const combined = Buffer.from(encryptedData, 'base64')
// Komponenten extrahieren (v1: salt(32) + iv(16) + ciphertext)
const salt = combined.subarray(0, SALT_LENGTH)
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + LEGACY_IV_LENGTH)
const encrypted = combined.subarray(SALT_LENGTH + LEGACY_IV_LENGTH)
// Prüfe minimale Länge (salt + iv = 32 + 16 = 48 bytes)
if (combined.length < SALT_LENGTH + LEGACY_IV_LENGTH) {
throw new Error(`Ungültige Datenlänge: ${combined.length} bytes (erwartet mindestens ${SALT_LENGTH + LEGACY_IV_LENGTH})`)
}
// Schlüssel ableiten
const key = deriveKey(password, salt)
// Komponenten extrahieren (v1: salt(32) + iv(16) + ciphertext)
const salt = combined.subarray(0, SALT_LENGTH)
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + LEGACY_IV_LENGTH)
const encrypted = combined.subarray(SALT_LENGTH + LEGACY_IV_LENGTH)
// Decipher erstellen
const decipher = crypto.createDecipheriv(LEGACY_ALGORITHM, key, iv)
// Prüfe ob verschlüsselte Daten vorhanden sind
if (encrypted.length === 0) {
throw new Error('Keine verschlüsselten Daten gefunden')
}
// Entschlüsseln
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
])
// Schlüssel ableiten
const key = deriveKey(password, salt)
return decrypted.toString('utf8')
// Decipher erstellen
const decipher = crypto.createDecipheriv(LEGACY_ALGORITHM, key, iv)
// Entschlüsseln
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
])
return decrypted.toString('utf8')
} catch (error) {
// Re-throw mit mehr Kontext
throw new Error(`Legacy CBC Entschlüsselung fehlgeschlagen: ${error.message}`)
}
}
function decryptV2GCM(encryptedData, password) {
const b64 = encryptedData.slice(VERSION_PREFIX.length)
const combined = Buffer.from(b64, 'base64')
try {
const b64 = encryptedData.slice(VERSION_PREFIX.length)
const combined = Buffer.from(b64, 'base64')
// v2: salt(32) + iv(12) + tag(16) + ciphertext
const salt = combined.subarray(0, SALT_LENGTH)
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
const tagStart = SALT_LENGTH + IV_LENGTH
const tag = combined.subarray(tagStart, tagStart + AUTH_TAG_LENGTH)
const encrypted = combined.subarray(tagStart + AUTH_TAG_LENGTH)
// Prüfe minimale Länge (salt + iv + tag = 32 + 12 + 16 = 60 bytes)
const minLength = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
if (combined.length < minLength) {
throw new Error(`Ungültige Datenlänge: ${combined.length} bytes (erwartet mindestens ${minLength})`)
}
const key = deriveKey(password, salt)
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
decipher.setAuthTag(tag)
// v2: salt(32) + iv(12) + tag(16) + ciphertext
const salt = combined.subarray(0, SALT_LENGTH)
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
const tagStart = SALT_LENGTH + IV_LENGTH
const tag = combined.subarray(tagStart, tagStart + AUTH_TAG_LENGTH)
const encrypted = combined.subarray(tagStart + AUTH_TAG_LENGTH)
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
])
// Prüfe ob verschlüsselte Daten vorhanden sind
if (encrypted.length === 0) {
throw new Error('Keine verschlüsselten Daten gefunden')
}
return decrypted.toString('utf8')
const key = deriveKey(password, salt)
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
decipher.setAuthTag(tag)
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
])
return decrypted.toString('utf8')
} catch (error) {
// Re-throw mit mehr Kontext
throw new Error(`GCM v2 Entschlüsselung fehlgeschlagen: ${error.message}`)
}
}
/**
@@ -111,7 +142,16 @@ export function encrypt(text, password) {
*/
export function decrypt(encryptedData, password) {
try {
if (typeof encryptedData === 'string' && encryptedData.startsWith(VERSION_PREFIX)) {
if (!encryptedData || typeof encryptedData !== 'string') {
throw new Error('Ungültige verschlüsselte Daten: muss ein String sein')
}
if (!password || typeof password !== 'string') {
throw new Error('Verschlüsselungsschlüssel fehlt oder ist ungültig')
}
if (encryptedData.startsWith(VERSION_PREFIX)) {
// v2 GCM Format
return decryptV2GCM(encryptedData, password)
}
@@ -119,7 +159,11 @@ export function decrypt(encryptedData, password) {
return decryptLegacyCBC(encryptedData, password)
} catch (error) {
console.error('Entschlüsselungsfehler:', error)
throw new Error('Fehler beim Entschlüsseln der Daten')
// Re-throw mit mehr Details für besseres Debugging
if (error.message.includes('Entschlüsselung fehlgeschlagen')) {
throw error
}
throw new Error(`Fehler beim Entschlüsseln der Daten: ${error.message}`)
}
}

View File

@@ -27,11 +27,51 @@ function getEncryptionKey() {
return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
}
// Check if data is encrypted by trying to parse as JSON first
// Liste möglicher alter Verschlüsselungsschlüssel (für Migration)
function getPossibleEncryptionKeys() {
const currentKey = getEncryptionKey()
const oldKeys = [
'default-key-change-in-production',
'local_development_encryption_key_change_in_production'
]
// Aktueller Schlüssel zuerst, dann alte Schlüssel
const keys = [currentKey]
for (const oldKey of oldKeys) {
if (oldKey !== currentKey) {
keys.push(oldKey)
}
}
// Optional: Alter Schlüssel aus Environment-Variable
if (process.env.OLD_ENCRYPTION_KEY && process.env.OLD_ENCRYPTION_KEY !== currentKey) {
keys.push(process.env.OLD_ENCRYPTION_KEY)
}
return keys
}
// Check if data is encrypted by checking format indicators
function isEncrypted(data) {
if (!data || typeof data !== 'string') {
return false
}
const trimmed = data.trim()
// Empty or whitespace only
if (!trimmed) {
return false
}
// Check for v2 prefix (v2:base64...)
if (trimmed.startsWith('v2:')) {
return true
}
// Try to parse as JSON - if successful, it's likely unencrypted
try {
// Try to parse as JSON - if successful and looks like member data, it's unencrypted
const parsed = JSON.parse(data.trim())
const parsed = JSON.parse(trimmed)
// If it's an array (members list) or object with member-like structure, it's unencrypted
if (Array.isArray(parsed)) {
return false // Unencrypted array
@@ -42,8 +82,15 @@ function isEncrypted(data) {
}
return false
} catch (_e) {
// JSON parsing failed - likely encrypted base64
return true
// JSON parsing failed - check if it looks like base64 (encrypted)
// Base64 strings typically don't contain spaces and have specific character set
const base64Pattern = /^[A-Za-z0-9+/=]+$/
if (base64Pattern.test(trimmed) && trimmed.length > 50) {
// Looks like base64 and long enough to be encrypted data
return true
}
// If it's not valid JSON and doesn't look like base64, assume unencrypted but malformed
return false
}
}
@@ -52,29 +99,78 @@ export async function readMembers() {
try {
const data = await fs.readFile(MEMBERS_FILE, 'utf-8')
// Check if file is empty or whitespace only
if (!data || !data.trim()) {
console.warn('Mitgliederdaten-Datei ist leer')
return []
}
// Check if data is encrypted or plain JSON
const encrypted = isEncrypted(data)
if (encrypted) {
// Decrypt and parse
// Versuche mit verschiedenen Schlüsseln zu entschlüsseln (für Migration)
const possibleKeys = getPossibleEncryptionKeys()
const encryptionKey = getEncryptionKey()
try {
return decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Mitgliederdaten:', decryptError)
// Fallback: try to read as plain JSON (migration scenario)
// Warn if using default encryption key in production
if (process.env.NODE_ENV === 'production' &&
encryptionKey === 'local_development_encryption_key_change_in_production') {
console.error('WARNUNG: Produktionsumgebung verwendet Standard-Verschlüsselungsschlüssel!')
}
let lastError = null
for (let i = 0; i < possibleKeys.length; i++) {
const key = possibleKeys[i]
try {
const plainData = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
return plainData
} catch (_parseError) {
console.error('Konnte Mitgliederdaten weder entschlüsseln noch als JSON lesen')
return []
const decrypted = decryptObject(data, key)
// Wenn mit altem Schlüssel entschlüsselt wurde, warnen und neu verschlüsseln
if (i > 0 && key !== encryptionKey) {
console.warn(`⚠️ Mitgliederdaten wurden mit altem Schlüssel entschlüsselt. Automatische Neuverschlüsselung...`)
try {
await writeMembers(decrypted)
console.log('✅ Mitgliederdaten erfolgreich mit neuem Schlüssel neu verschlüsselt')
} catch (writeError) {
console.error('❌ Fehler beim Neuverschlüsseln:', writeError.message)
console.error(' Bitte führen Sie manuell aus: node scripts/re-encrypt-data.js')
}
}
return decrypted
} catch (decryptError) {
lastError = decryptError
// Versuche nächsten Schlüssel
continue
}
}
// Alle Schlüssel fehlgeschlagen
console.error('Fehler beim Entschlüsseln der Mitgliederdaten:')
console.error(' Versuchte Schlüssel:', possibleKeys.length)
console.error(' Letzter Fehler:', lastError?.message || 'Unbekannter Fehler')
console.error(' Daten-Länge:', data.length)
console.error(' Daten-Präfix (erste 50 Zeichen):', data.substring(0, 50))
console.error('')
console.error('💡 Lösung: Führen Sie das Re-Encrypt-Skript aus:')
console.error(' node scripts/re-encrypt-data.js --old-key="<alter-schlüssel>"')
console.error(' Oder setzen Sie OLD_ENCRYPTION_KEY als Environment-Variable')
// Fallback: try to read as plain JSON (migration scenario)
try {
const plainData = JSON.parse(data.trim())
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
// Don't auto-migrate if decryption failed - might be wrong key
return plainData
} catch (parseError) {
console.error('Konnte Mitgliederdaten weder entschlüsseln noch als JSON lesen')
console.error('JSON-Parse-Fehler:', parseError.message)
// Return empty array to prevent complete failure
return []
}
} else {
// Plain JSON - migrate to encrypted format
const members = JSON.parse(data)
const members = JSON.parse(data.trim())
console.log('Migriere unverschlüsselte Mitgliederdaten zu verschlüsselter Speicherung...')
// Write back encrypted

View File

@@ -23,6 +23,30 @@ function getEncryptionKey() {
return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
}
// Liste möglicher alter Verschlüsselungsschlüssel (für Migration)
function getPossibleEncryptionKeys() {
const currentKey = getEncryptionKey()
const oldKeys = [
'default-key-change-in-production',
'local_development_encryption_key_change_in_production'
]
// Aktueller Schlüssel zuerst, dann alte Schlüssel
const keys = [currentKey]
for (const oldKey of oldKeys) {
if (oldKey !== currentKey) {
keys.push(oldKey)
}
}
// Optional: Alter Schlüssel aus Environment-Variable
if (process.env.OLD_ENCRYPTION_KEY && process.env.OLD_ENCRYPTION_KEY !== currentKey) {
keys.push(process.env.OLD_ENCRYPTION_KEY)
}
return keys
}
// Prüft ob Daten verschlüsselt sind
function isEncrypted(data) {
try {
@@ -45,20 +69,54 @@ export async function readSubscribers() {
const encrypted = isEncrypted(data)
if (encrypted) {
// Versuche mit verschiedenen Schlüsseln zu entschlüsseln (für Migration)
const possibleKeys = getPossibleEncryptionKeys()
const encryptionKey = getEncryptionKey()
try {
return decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Newsletter-Abonnenten:', decryptError)
let lastError = null
for (let i = 0; i < possibleKeys.length; i++) {
const key = possibleKeys[i]
try {
const plainData = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
return plainData
} catch (_parseError) {
console.error('Konnte Newsletter-Abonnenten weder entschlüsseln noch als JSON lesen')
return []
const decrypted = decryptObject(data, key)
// Wenn mit altem Schlüssel entschlüsselt wurde, warnen und neu verschlüsseln
if (i > 0 && key !== encryptionKey) {
console.warn(`⚠️ Newsletter-Abonnenten wurden mit altem Schlüssel entschlüsselt. Automatische Neuverschlüsselung...`)
try {
await writeSubscribers(decrypted)
console.log('✅ Newsletter-Abonnenten erfolgreich mit neuem Schlüssel neu verschlüsselt')
} catch (writeError) {
console.error('❌ Fehler beim Neuverschlüsseln:', writeError.message)
console.error(' Bitte führen Sie manuell aus: node scripts/re-encrypt-data.js')
}
}
return decrypted
} catch (decryptError) {
lastError = decryptError
// Versuche nächsten Schlüssel
continue
}
}
// Alle Schlüssel fehlgeschlagen
console.error('Fehler beim Entschlüsseln der Newsletter-Abonnenten:')
console.error(' Versuchte Schlüssel:', possibleKeys.length)
console.error(' Letzter Fehler:', lastError?.message || 'Unbekannter Fehler')
console.error('')
console.error('💡 Lösung: Führen Sie das Re-Encrypt-Skript aus:')
console.error(' node scripts/re-encrypt-data.js --old-key="<alter-schlüssel>"')
console.error(' Oder setzen Sie OLD_ENCRYPTION_KEY als Environment-Variable')
// Fallback: try to read as plain JSON
try {
const plainData = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
return plainData
} catch (_parseError) {
console.error('Konnte Newsletter-Abonnenten weder entschlüsseln noch als JSON lesen')
return []
}
} else {
// Plain JSON - migriere zu verschlüsselter Speicherung
const subscribers = JSON.parse(data)