Multi-Hoster-Upload/lib/backup-crypto.js
Administrator ffc5b5576b feat: encrypted backup import/export
AES-256-GCM + PBKDF2 encrypted config backup (.mhu files).
Export/import all accounts, settings, and history.
Pre-import safety backup of current config.
Password modal with confirmation for export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:20:41 +01:00

72 lines
2.0 KiB
JavaScript

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';
function deriveKey(password, salt) {
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST);
}
/**
* Encrypt a config object with a password.
* Returns a Buffer: MHU1 | salt(16) | iv(12) | tag(16) | ciphertext
*/
function encrypt(config, password) {
const plaintext = Buffer.from(JSON.stringify(config), 'utf-8');
const salt = crypto.randomBytes(SALT_LEN);
const iv = crypto.randomBytes(IV_LEN);
const key = deriveKey(password, salt);
const cipher = crypto.createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([MAGIC, salt, iv, tag, encrypted]);
}
/**
* Decrypt a .mhu buffer with a password.
* Returns the config object or throws on invalid password/data.
*/
function decrypt(buffer, password) {
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(password, 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('Falsches Passwort oder beschädigte Datei');
}
try {
return JSON.parse(decrypted.toString('utf-8'));
} catch {
throw new Error('Entschlüsselte Daten sind kein gültiges JSON');
}
}
module.exports = { encrypt, decrypt };