const crypto = require('crypto'); const MAGIC = Buffer.from('MHU1'); const SALT_LEN = 16; const IV_LEN = 12; const TAG_LEN = 16; const KEY_LEN = 32; const ITERATIONS = 100_000; const DIGEST = 'sha512'; const ALGO = 'aes-256-gcm'; // Fixed app-internal passphrase — backups are opaque without the app, which is // enough protection for API keys stored locally. We keep the AES-GCM envelope // (with random salt/iv) so each export is still distinct and authenticated. const APP_PASSPHRASE = 'multi-hoster-upload::backup::v1'; function deriveKey(salt) { return crypto.pbkdf2Sync(APP_PASSPHRASE, salt, ITERATIONS, KEY_LEN, DIGEST); } /** * Encrypt a config object. * Returns a Buffer: MHU1 | salt(16) | iv(12) | tag(16) | ciphertext */ function encrypt(config) { const plaintext = Buffer.from(JSON.stringify(config), 'utf-8'); const salt = crypto.randomBytes(SALT_LEN); const iv = crypto.randomBytes(IV_LEN); const key = deriveKey(salt); const cipher = crypto.createCipheriv(ALGO, key, iv); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag(); plaintext.fill(0); key.fill(0); return Buffer.concat([MAGIC, salt, iv, tag, encrypted]); } /** * Decrypt a .mhu buffer. * Returns the config object or throws on invalid data. */ function decrypt(buffer) { if (buffer.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN + 1) { throw new Error('Ungültiges Backup-Format'); } const magic = buffer.subarray(0, 4); if (!magic.equals(MAGIC)) { throw new Error('Keine gültige .mhu Backup-Datei'); } let offset = MAGIC.length; const salt = buffer.subarray(offset, offset += SALT_LEN); const iv = buffer.subarray(offset, offset += IV_LEN); const tag = buffer.subarray(offset, offset += TAG_LEN); const ciphertext = buffer.subarray(offset); const key = deriveKey(salt); const decipher = crypto.createDecipheriv(ALGO, key, iv); decipher.setAuthTag(tag); let decrypted; try { decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); } catch { throw new Error('Backup-Datei beschädigt oder aus einer inkompatiblen Version'); } try { return JSON.parse(decrypted.toString('utf-8')); } catch { throw new Error('Entschlüsselte Daten sind kein gültiges JSON'); } finally { decrypted.fill(0); key.fill(0); } } module.exports = { encrypt, decrypt };