Multi-Hoster-Upload/lib/backup-crypto.js
Administrator 816f675d90 🐛 fix: broken tests, empty password validation, asset URL check
- Fix 3 failing config-store tests: update expectations to match
  multi-account array format (tests passed with old single-object format)
- backup-crypto: reject empty/null passwords on encrypt+decrypt
  instead of producing weak keys silently
- updater: validate assetUrl and assetName before downloading
  to prevent crash on incomplete update metadata

All 59 tests now passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:05:33 +01:00

79 lines
2.3 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) {
if (!password || typeof password !== 'string') throw new Error('Passwort darf nicht leer sein');
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();
plaintext.fill(0);
key.fill(0);
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 (!password || typeof password !== 'string') throw new Error('Passwort darf nicht leer sein');
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');
} finally {
decrypted.fill(0);
key.fill(0);
}
}
module.exports = { encrypt, decrypt };