diff --git a/Taskfile.yml b/Taskfile.yml index e075d5a..7b8dbfa 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -289,27 +289,51 @@ tasks: - task: build-appimage electron-legacy: - desc: Install legacy Electron version + desc: Ensure legacy Electron version is available (will be downloaded by electron-builder if needed) cmds: - - "{{.NPM}} install --no-save electron@{{.LEGACY_ELECTRON_VERSION}}" + - 'echo "Building with legacy Electron version: {{.LEGACY_ELECTRON_VERSION}}"' build-appimage-legacy: desc: Build Linux AppImage with legacy Electron version deps: [build-frontend, electron-legacy] cmds: - - "{{.NPM}} run electron-postinstall" + - "npx electron-builder install-app-deps --config package-legacy.json" - "PLATFORM=linux {{.NPM}} run build-backend" - - "{{.NPM}} run dist -- --linux AppImage -c.extraMetadata.main=electron/main-legacy.js" - - "./scripts/rename_legacy_artifacts.sh" + - "npx electron-builder --config package-legacy.json --linux AppImage --publish=never" build-exe-legacy: desc: Build Windows portable executable with legacy Electron version deps: [build-frontend, electron-legacy] cmds: - - "{{.NPM}} run electron-postinstall" + - "npx electron-builder install-app-deps --config package-legacy.json" - "PLATFORM=win32 {{.NPM}} run build-backend" - - "{{.NPM}} run dist -- --win portable -c.extraMetadata.main=electron/main-legacy.js" - - "./scripts/rename_legacy_artifacts.sh" + - "npx electron-builder --config package-legacy.json --win portable --publish=never" + + build-exe-legacy-wine: + desc: Build Windows portable executable and NSIS installer using Wine (Legacy) + deps: [build-frontend, electron-legacy] + cmds: + - "npx electron-builder install-app-deps --config package-legacy.json" + - "PLATFORM=win32 PYTHON_CMD='{{.WINE_PYTHON}}' {{.NPM}} run build-backend" + - "npx electron-builder --config package-legacy.json --win portable nsis --publish=never" + + build-legacy-all: + desc: Build all legacy Electron apps (Linux and Windows) + deps: [build-frontend, electron-legacy] + cmds: + - "npx electron-builder install-app-deps --config package-legacy.json" + - "PLATFORM=linux {{.NPM}} run build-backend" + - "PLATFORM=win32 {{.NPM}} run build-backend" + - "npx electron-builder --config package-legacy.json --linux AppImage deb --win portable nsis --publish=never" + + build-legacy-all-wine: + desc: Build all legacy Electron apps (Linux + Windows via Wine) + deps: [build-frontend, electron-legacy] + cmds: + - "npx electron-builder install-app-deps --config package-legacy.json" + - "PLATFORM=linux {{.NPM}} run build-backend" + - "PLATFORM=win32 PYTHON_CMD='{{.WINE_PYTHON}}' {{.NPM}} run build-backend" + - "npx electron-builder --config package-legacy.json --linux AppImage deb --win portable nsis --publish=never" forge-start: desc: Run the application with Electron Forge diff --git a/electron/main-legacy.js b/electron/main-legacy.js index 9679e03..f6e9528 100644 --- a/electron/main-legacy.js +++ b/electron/main-legacy.js @@ -1,15 +1,76 @@ -const { app, BrowserWindow, dialog, ipcMain, shell, systemPreferences } = require("electron"); +const { + app, + BrowserWindow, + dialog, + ipcMain, + shell, + systemPreferences, + Notification, + powerSaveBlocker, +} = require("electron"); const electronPrompt = require("electron-prompt"); const { spawn } = require("child_process"); const fs = require("fs"); const path = require("node:path"); +const crypto = require("crypto"); // remember main window var mainWindow = null; +// power save blocker id +var activePowerSaveBlockerId = null; + // remember child process for exe so we can kill it when app exits var exeChildProcess = null; +// store integrity status +var integrityStatus = { + backend: { ok: true, issues: [] }, + data: { ok: true, issues: [] }, +}; + +function verifyBackendIntegrity(exeDir) { + const manifestPath = path.join(exeDir, "backend-manifest.json"); + if (!fs.existsSync(manifestPath)) { + log("Backend integrity manifest missing, skipping check."); + return { ok: true, issues: ["Manifest missing"] }; + } + + try { + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + const issues = []; + + const filesToVerify = manifest.files || manifest; + const metadata = manifest._metadata || {}; + + for (const [relPath, expectedHash] of Object.entries(filesToVerify)) { + const fullPath = path.join(exeDir, relPath); + if (!fs.existsSync(fullPath)) { + issues.push(`Missing: ${relPath}`); + continue; + } + + const fileBuffer = fs.readFileSync(fullPath); + const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex"); + if (actualHash !== expectedHash) { + issues.push(`Modified: ${relPath}`); + } + } + + if (issues.length > 0 && metadata.date && metadata.time) { + issues.unshift(`Backend build timestamp: ${metadata.date} ${metadata.time}`); + } + + return { + ok: issues.length === 0, + issues: issues, + }; + } catch (error) { + log(`Backend integrity check failed: ${error.message}`); + return { ok: false, issues: [error.message] }; + } +} + // allow fetching app version via ipc ipcMain.handle("app-version", () => { return app.getVersion(); @@ -24,12 +85,43 @@ ipcMain.handle("is-hardware-acceleration-enabled", () => { return true; // Assume true for older versions }); -// allow fetching integrity status (Stub for legacy) +// allow fetching integrity status ipcMain.handle("get-integrity-status", () => { - return { - backend: { ok: true, issues: ["Not supported in legacy mode"] }, - data: { ok: true, issues: ["Not supported in legacy mode"] }, - }; + return integrityStatus; +}); + +// Native Notification IPC +ipcMain.handle("show-notification", (event, { title, body, silent }) => { + const notification = new Notification({ + title: title, + body: body, + silent: silent, + }); + notification.show(); + + notification.on("click", () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + }); +}); + +// Power Management IPC +ipcMain.handle("set-power-save-blocker", (event, enabled) => { + if (enabled) { + if (activePowerSaveBlockerId === null) { + activePowerSaveBlockerId = powerSaveBlocker.start("prevent-app-suspension"); + log("Power save blocker started."); + } + } else { + if (activePowerSaveBlockerId !== null) { + powerSaveBlocker.stop(activePowerSaveBlockerId); + activePowerSaveBlockerId = null; + log("Power save blocker stopped."); + } + } + return activePowerSaveBlockerId !== null; }); // ignore ssl errors @@ -80,6 +172,19 @@ ipcMain.handle("relaunch", () => { app.exit(); }); +ipcMain.handle("relaunch-emergency", () => { + app.relaunch({ args: process.argv.slice(1).concat(["--emergency"]) }); + app.exit(); +}); + +ipcMain.handle("shutdown", () => { + quit(); +}); + +ipcMain.handle("get-memory-usage", async () => { + return process.getProcessMemoryInfo(); +}); + // allow showing a file path in os file manager ipcMain.handle("showPathInFolder", (event, path) => { shell.showItemInFolder(path); @@ -245,6 +350,13 @@ app.whenReady().then(async () => { log(`Found executable at: ${exe}`); + // Verify backend integrity before spawning + const exeDir = path.dirname(exe); + integrityStatus.backend = verifyBackendIntegrity(exeDir); + if (!integrityStatus.backend.ok) { + log(`INTEGRITY WARNING: Backend tampering detected! Issues: ${integrityStatus.backend.issues.join(", ")}`); + } + try { // arguments we always want to pass in const requiredArguments = [ diff --git a/package-legacy.json b/package-legacy.json new file mode 100644 index 0000000..e2237b4 --- /dev/null +++ b/package-legacy.json @@ -0,0 +1,60 @@ +{ + "appId": "com.sudoivan.reticulummeshchat-legacy", + "productName": "Reticulum MeshChatX Legacy", + "electronVersion": "30.0.8", + "extraMetadata": { + "main": "electron/main-legacy.js" + }, + "asar": true, + "electronFuses": { + "runAsNode": false, + "enableCookieEncryption": true, + "enableNodeOptionsEnvironmentVariable": false, + "enableNodeCliInspectArguments": false, + "enableEmbeddedAsarIntegrityValidation": true, + "onlyLoadAppFromAsar": true + }, + "files": ["electron/**/*"], + "directories": { + "buildResources": "electron/build" + }, + "win": { + "artifactName": "ReticulumMeshChat-v${version}-legacy-${os}.${ext}", + "target": [ + { + "target": "portable" + }, + { + "target": "nsis" + } + ], + "extraResources": [ + { + "from": "build/exe/win32", + "to": "backend", + "filter": ["**/*"] + } + ] + }, + "linux": { + "artifactName": "ReticulumMeshChatX-v${version}-legacy-${os}.${ext}", + "target": ["AppImage", "deb"], + "maintainer": "Sudo-Ivan", + "category": "Network", + "extraResources": [ + { + "from": "build/exe/linux", + "to": "backend", + "filter": ["**/*"] + } + ] + }, + "portable": { + "artifactName": "ReticulumMeshChatX-v${version}-legacy-${os}-portable.${ext}" + }, + "nsis": { + "artifactName": "ReticulumMeshChatX-v${version}-legacy-${os}-installer.${ext}", + "oneClick": false, + "allowToChangeInstallationDirectory": true + } +}