diff --git a/desktop-app/neutralino.config.json b/desktop-app/neutralino.config.json index 069bfd0..e0b0742 100644 --- a/desktop-app/neutralino.config.json +++ b/desktop-app/neutralino.config.json @@ -10,8 +10,8 @@ "enableNativeAPI": true, "tokenSecurity": "one-time", "logging": { - "enabled": true, - "writeToLogFile": true + "enabled": false, + "writeToLogFile": false }, "nativeAllowList": [ "app.exit", @@ -40,7 +40,7 @@ "maximize": false, "hidden": false, "resizable": true, - "exitProcessOnClose": false + "exitProcessOnClose": true }, "browser": { "globalVariables": {}, diff --git a/desktop-app/prepare.js b/desktop-app/prepare.js index 278c076..a202e8f 100644 --- a/desktop-app/prepare.js +++ b/desktop-app/prepare.js @@ -5,13 +5,14 @@ * * Copies shared browser-version files (script.js, styles.css, assets/) * from the repo root into desktop-app/resources/, downloads all remote CDN - * libraries locally for 100% offline capabilities, and generates a - * Neutralinojs-compatible index.html. + * libraries locally for 100% offline capabilities, validates their cryptographic + * integrity using SRI hashes (SHA-384), and generates a Neutralinojs-compatible index.html. */ const fs = require("fs"); const path = require("path"); const https = require("https"); +const crypto = require("crypto"); const ROOT_DIR = path.resolve(__dirname, ".."); const RESOURCES_DIR = path.resolve(__dirname, "resources"); @@ -45,43 +46,113 @@ console.log("✓ Copied styles.css → resources/styles.css"); copyDirSync(path.join(ROOT_DIR, "assets"), path.join(RESOURCES_DIR, "assets")); console.log("✓ Copied assets/ → resources/assets/"); -// Download helper -function downloadFile(url, destPath) { +/** + * Validates the cryptographic integrity of a file against an expected SHA-384 hash. + */ +function verifyIntegrity(filePath, expectedSha384) { return new Promise((resolve, reject) => { - if (fs.existsSync(destPath) && fs.statSync(destPath).size > 0) { - resolve(); + if (!expectedSha384) { + resolve(true); // Skip validation if no hash is provided (e.g., relative fonts) return; } - console.log(`Downloading offline dependency: ${path.basename(destPath)}...`); - https.get(url, (res) => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to load ${url} (${res.statusCode})`)); - return; + + const hash = crypto.createHash("sha384"); + const stream = fs.createReadStream(filePath); + + stream.on("data", data => hash.update(data)); + stream.on("end", () => { + const calculated = "sha384-" + hash.digest("base64"); + if (calculated === expectedSha384) { + resolve(true); + } else { + reject(new Error(`Integrity mismatch for ${path.basename(filePath)}:\nExpected: ${expectedSha384}\nCalculated: ${calculated}`)); } - const stream = fs.createWriteStream(destPath); - res.pipe(stream); - stream.on("finish", () => { - stream.close(); - resolve(); + }); + stream.on("error", reject); + }); +} + +/** + * Downloads a file from a URL and verifies its integrity. + */ +function downloadFile(url, destPath, expectedSha384) { + return new Promise((resolve, reject) => { + // If file already exists, verify its integrity before skipping + if (fs.existsSync(destPath) && fs.statSync(destPath).size > 0) { + verifyIntegrity(destPath, expectedSha384) + .then(() => resolve()) + .catch(() => { + console.log(`↻ Cached file ${path.basename(destPath)} failed integrity check. Re-downloading...`); + fs.unlinkSync(destPath); + downloadAndVerify(); + }); + return; + } + + downloadAndVerify(); + + function downloadAndVerify() { + console.log(`Downloading offline dependency: ${path.basename(destPath)}...`); + const req = https.get(url, (res) => { + if (res.statusCode !== 200) { + res.resume(); // Drain response to free up the socket + reject(new Error(`Failed to load ${url} (${res.statusCode})`)); + return; + } + const stream = fs.createWriteStream(destPath); + + // Handle stream and response errors + stream.on("error", reject); + res.on("error", reject); + + res.pipe(stream); + stream.on("finish", () => { + stream.close(); + + // Verify integrity of downloaded file + verifyIntegrity(destPath, expectedSha384) + .then(() => resolve()) + .catch(err => { + // Delete corrupted file + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + reject(err); + }); + }); }); - }).on("error", reject); + req.on("error", reject); + } }); } async function prepareOfflineDependencies() { - console.log("\nStarting Offline Assets Preparation..."); + console.log("\nStarting Secure Offline Assets Preparation..."); let html = fs.readFileSync(path.join(ROOT_DIR, "index.html"), "utf-8"); - // Find all CDN script and link tags - const cdnRegex = /(href|src)="(https:\/\/(?:cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net)\/[^"]+)"/g; + // Find all CDN script and link tags that match standard script/stylesheet declarations + const tagRegex = /<(link|script)[^>]+(?:href|src)="https:\/\/(?:cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net)\/[^"]+"[^>]*>/g; let match; const downloads = []; const replacements = []; - while ((match = cdnRegex.exec(html)) !== null) { - const attr = match[1]; - const url = match[2]; + while ((match = tagRegex.exec(html)) !== null) { + const fullTag = match[0]; + // Extract url + const urlMatch = /(?:href|src)="([^"]+)"/.exec(fullTag); + if (!urlMatch) continue; + const url = urlMatch[1]; + + // Extract integrity hash + const integrityMatch = /integrity="([^"]+)"/.exec(fullTag); + const expectedSha384 = integrityMatch ? integrityMatch[1] : null; + + if (!expectedSha384) { + console.warn(`⚠ Warning: CDN dependency is missing an integrity hash: ${url}`); + throw new Error(`CDN dependency is missing an integrity hash: ${url}`); + } + // Determine local filename - sanitize package version tags or query strings const urlPath = new URL(url).pathname; let filename = path.basename(urlPath); @@ -90,27 +161,29 @@ async function prepareOfflineDependencies() { } const localDest = path.join(LIBS_DIR, filename); - downloads.push(downloadFile(url, localDest)); + downloads.push(downloadFile(url, localDest, expectedSha384)); // Queue replacement in HTML to point to local libs folder + const attr = fullTag.includes("href=") ? "href" : "src"; replacements.push({ original: `${attr}="${url}"`, replaced: `${attr}="/libs/${filename}"` }); } - // Also download the relative fonts loaded by bootstrap-icons + // Also download the relative fonts loaded by bootstrap-icons (these are loaded by the stylesheet and do not have SRI tags) const fontDir = path.join(LIBS_DIR, "fonts"); fs.mkdirSync(fontDir, { recursive: true }); - downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2", path.join(fontDir, "bootstrap-icons.woff2"))); - downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff", path.join(fontDir, "bootstrap-icons.woff"))); + downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2", path.join(fontDir, "bootstrap-icons.woff2"), null)); + downloads.push(downloadFile("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff", path.join(fontDir, "bootstrap-icons.woff"), null)); - // Wait for all downloads to finish + // Wait for all downloads and cryptographic validations to finish try { await Promise.all(downloads); - console.log("✓ All offline libraries successfully prepared."); + console.log("✓ All offline libraries successfully downloaded and cryptographically validated."); } catch (err) { - console.warn("⚠ Failed to bundle some dependencies offline. Fallback to CDNs will occur.", err.message); + console.error("✗ Critical Security Error: Dependency integrity check failed!", err.message); + process.exit(1); // Abort execution if a download fails validation } // Apply replacements in HTML @@ -142,4 +215,7 @@ async function prepareOfflineDependencies() { console.log("\nDone! Run `npm run dev` to start the desktop app."); } -prepareOfflineDependencies().catch(console.error); +prepareOfflineDependencies().catch(err => { + console.error("✗ Fatal Prepare Error:", err); + process.exit(1); +}); diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index 1232aa7..fd2e215 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -909,7 +909,7 @@ - + diff --git a/index.html b/index.html index d40ce77..8e89a3d 100644 --- a/index.html +++ b/index.html @@ -906,7 +906,7 @@ - + \ No newline at end of file