- Single atomic write instead of two-phase (prevents split state on crash) - Timestamped pre-import backup (multiple imports don't overwrite safety net) - Fix UI refresh: correct function names + refresh globalSettings/alwaysOnTop - Zero sensitive buffers (key, plaintext, decrypted) after use Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
77 lines
2.1 KiB
JavaScript
77 lines
2.1 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();
|
|
|
|
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 (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 };
|