diff --git a/server/utils/auth.js b/server/utils/auth.js index e698615..4684a75 100644 --- a/server/utils/auth.js +++ b/server/utils/auth.js @@ -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=""') + 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') diff --git a/server/utils/encryption.js b/server/utils/encryption.js index 739ca51..1ae326a 100644 --- a/server/utils/encryption.js +++ b/server/utils/encryption.js @@ -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}`) } } diff --git a/server/utils/members.js b/server/utils/members.js index a820ee0..ff88dfb 100644 --- a/server/utils/members.js +++ b/server/utils/members.js @@ -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=""') + 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 diff --git a/server/utils/newsletter.js b/server/utils/newsletter.js index 5e0f9ab..6f841a3 100644 --- a/server/utils/newsletter.js +++ b/server/utils/newsletter.js @@ -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=""') + 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)