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.
193 lines
5.6 KiB
JavaScript
193 lines
5.6 KiB
JavaScript
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')
|
|
}
|
|
|