release: v2.1.12 reliability and security hardening
CI / verify (push) Waiting to run

Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
This commit is contained in:
Sucukdeluxe
2026-08-11 22:36:21 +02:00
parent 2c124c848c
commit 2ba0106ef0
47 changed files with 2069 additions and 1192 deletions
+17 -10
View File
@@ -1,6 +1,7 @@
const crypto = require('crypto');
const MAGIC = Buffer.from('MHU1');
const MAGIC_V1 = Buffer.from('MHU1');
const MAGIC_V2 = Buffer.from('MHU2');
const SALT_LEN = 16;
const IV_LEN = 12;
const TAG_LEN = 16;
@@ -22,11 +23,15 @@ function deriveKey(passphrase, salt) {
* Encrypt a config object.
* Returns a Buffer: MHU1 | salt(16) | iv(12) | tag(16) | ciphertext
*/
function encrypt(config) {
function encrypt(config, userPassword) {
if (userPassword !== undefined && (typeof userPassword !== 'string' || !userPassword.trim())) {
throw new Error('Das Backup-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(APP_PASSPHRASE, salt);
const passwordProtected = typeof userPassword === 'string';
const key = deriveKey(passwordProtected ? userPassword : APP_PASSPHRASE, salt);
const cipher = crypto.createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
@@ -34,7 +39,7 @@ function encrypt(config) {
plaintext.fill(0);
key.fill(0);
return Buffer.concat([MAGIC, salt, iv, tag, encrypted]);
return Buffer.concat([passwordProtected ? MAGIC_V2 : MAGIC_V1, salt, iv, tag, encrypted]);
}
/**
@@ -45,16 +50,17 @@ function encrypt(config) {
* given, so callers can prompt the user for the legacy password.
*/
function decrypt(buffer, userPassword) {
if (buffer.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN + 1) {
if (buffer.length < MAGIC_V1.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)) {
const passwordProtected = magic.equals(MAGIC_V2);
if (!magic.equals(MAGIC_V1) && !passwordProtected) {
throw new Error('Keine gültige .mhu Backup-Datei');
}
let offset = MAGIC.length;
let offset = MAGIC_V1.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);
@@ -76,9 +82,10 @@ function decrypt(buffer, userPassword) {
}
};
// 1) Try the app-internal key (new format, no password required).
const fromApp = tryPassphrase(APP_PASSPHRASE);
if (fromApp) return fromApp;
if (!passwordProtected) {
const fromApp = tryPassphrase(APP_PASSPHRASE);
if (fromApp) return fromApp;
}
// 2) Legacy format: user had set their own password.
if (userPassword) {