import crypto from 'crypto' // Verschlüsselungskonfiguration // v1 (legacy): aes-256-cbc (ohne Authentizitätsschutz) const LEGACY_ALGORITHM = 'aes-256-cbc' const LEGACY_IV_LENGTH = 16 // v2 (default): aes-256-gcm (AEAD, state-of-the-art) const ALGORITHM = 'aes-256-gcm' const IV_LENGTH = 12 const AUTH_TAG_LENGTH = 16 const SALT_LENGTH = 32 const VERSION_PREFIX = 'v2:' /** * Generiert einen Schlüssel aus einem Passwort und Salt */ function deriveKey(password, salt) { return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha512') } function encryptV2GCM(text, password) { // Salt generieren const salt = crypto.randomBytes(SALT_LENGTH) // Schlüssel ableiten const key = deriveKey(password, salt) // IV generieren (12 bytes ist Best Practice für GCM) const iv = crypto.randomBytes(IV_LENGTH) // Cipher erstellen const cipher = crypto.createCipheriv(ALGORITHM, key, iv) // Verschlüsseln const encrypted = Buffer.concat([ cipher.update(text, 'utf8'), cipher.final() ]) // Auth-Tag holen const tag = cipher.getAuthTag() // Salt + IV + Tag + Ciphertext kombinieren const combined = Buffer.concat([salt, iv, tag, encrypted]) return `${VERSION_PREFIX}${combined.toString('base64')}` } function decryptLegacyCBC(encryptedData, password) { try { // Base64 dekodieren const combined = Buffer.from(encryptedData, 'base64') // 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})`) } // 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 ob verschlüsselte Daten vorhanden sind if (encrypted.length === 0) { throw new Error('Keine verschlüsselten Daten gefunden') } // Schlüssel ableiten const key = deriveKey(password, salt) // 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) { try { const b64 = encryptedData.slice(VERSION_PREFIX.length) const combined = Buffer.from(b64, 'base64') // 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})`) } // 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 ob verschlüsselte Daten vorhanden sind if (encrypted.length === 0) { throw new Error('Keine verschlüsselten Daten gefunden') } 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}`) } } /** * Verschlüsselt einen Text */ export function encrypt(text, password) { try { return encryptV2GCM(text, password) } catch (error) { console.error('Verschlüsselungsfehler:', error) throw new Error('Fehler beim Verschlüsseln der Daten') } } /** * Entschlüsselt einen Text */ export function decrypt(encryptedData, password) { try { 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) } // Fallback: legacy CBC ohne Prefix return decryptLegacyCBC(encryptedData, password) } catch (error) { console.error('Entschlüsselungsfehler:', error) // 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}`) } } /** * Verschlüsselt ein Objekt (konvertiert zu JSON) */ export function encryptObject(obj, password) { const jsonString = JSON.stringify(obj) return encrypt(jsonString, password) } /** * Entschlüsselt ein Objekt (konvertiert von JSON) */ export function decryptObject(encryptedData, password) { const jsonString = decrypt(encryptedData, password) return JSON.parse(jsonString) } /** * Generiert einen sicheren Schlüssel für die Datenverschlüsselung */ export function generateEncryptionKey() { return crypto.randomBytes(32).toString('hex') }