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