Files
harheimertc/server/utils/encryption.js

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, { authTagLength: AUTH_TAG_LENGTH })
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')
}