Files
harheimertc/server/utils/encryption.js
Torsten Schulz (local) 01cf0e58cb
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 1m1s
Add support for multiple encryption keys in data handling
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.
2026-01-09 09:05:05 +01:00

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')
}