149 lines
3.9 KiB
JavaScript
149 lines
3.9 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) {
|
|
// Base64 dekodieren
|
|
const combined = Buffer.from(encryptedData, 'base64')
|
|
|
|
// 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)
|
|
|
|
// 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')
|
|
}
|
|
|
|
function decryptV2GCM(encryptedData, password) {
|
|
const b64 = encryptedData.slice(VERSION_PREFIX.length)
|
|
const combined = Buffer.from(b64, 'base64')
|
|
|
|
// 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)
|
|
|
|
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')
|
|
}
|
|
|
|
/**
|
|
* 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 (typeof encryptedData === 'string' && encryptedData.startsWith(VERSION_PREFIX)) {
|
|
return decryptV2GCM(encryptedData, password)
|
|
}
|
|
|
|
// Fallback: legacy CBC ohne Prefix
|
|
return decryptLegacyCBC(encryptedData, password)
|
|
} catch (error) {
|
|
console.error('Entschlüsselungsfehler:', error)
|
|
throw new Error('Fehler beim Entschlüsseln der Daten')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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')
|
|
}
|
|
|