From a1d6f31d028dcccb499e589775ec509f4bd462ef Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 09:12:00 +0100 Subject: [PATCH] Move os and arch detection to downloader, add checksum verification. --- .../src/installation/downloadAgent.js | 82 ++++++++++++++++++- .../src/installation/installOnWindows.js | 49 +++++------ 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index d74cbf7..2441f7d 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -1,9 +1,25 @@ -import { createWriteStream } from "fs"; +import { createWriteStream, createReadStream } from "fs"; +import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.0"; +const DOWNLOAD_URLS = { + win32: { + x64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-amd64.msi", + checksum: + "sha256:c699f74a3666d85b70b8ede076a2192a6a023f1b395e8e6c7556927ee698a020", + }, + arm64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-arm64.msi", + checksum: + "sha256:5b08dd4749c8befe5379bc01f7a8a5ac1d6a35b6bee37c6c72a4ba8744c3b052", + }, + }, +}; + /** * Builds the download URL for the SafeChain Agent installer. * @param {string} fileName @@ -31,3 +47,67 @@ export async function downloadFile(url, destPath) { export function getAgentVersion() { return ULTIMATE_VERSION; } + +/** + * Returns download info (url, checksum) for the current OS and architecture. + * @returns {{ url: string, checksum: string } | null} + */ +export function getDownloadInfoForCurrentPlatform() { + const platform = process.platform; + const arch = process.arch; + + if (!Object.hasOwn(DOWNLOAD_URLS, platform)) { + return null; + } + const platformUrls = + DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)]; + + if (!Object.hasOwn(platformUrls, arch)) { + return null; + } + + return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)]; +} + +/** + * Verifies the checksum of a file. + * @param {string} filePath + * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...") + * @returns {Promise} + */ +async function verifyChecksum(filePath, expectedChecksum) { + const [algorithm, expected] = expectedChecksum.split(":"); + + const hash = createHash(algorithm); + + if (filePath.includes("..")) throw new Error("Invalid file path"); + const stream = createReadStream(filePath); + + for await (const chunk of stream) { + hash.update(chunk); + } + + const actual = hash.digest("hex"); + return actual === expected; +} + +/** + * Downloads the SafeChain agent for the current OS/arch and verifies its checksum. + * @param {string} fileName - Destination file path + * @returns {Promise} The file path if successful, null if no download URL for current platform + */ +export async function downloadAgentToFile(fileName) { + const info = getDownloadInfoForCurrentPlatform(); + if (!info) { + return null; + } + + await downloadFile(info.url, fileName); + + const isValid = await verifyChecksum(fileName, info.checksum); + if (!isValid) { + throw new Error("Checksum verification failed"); + } + + return fileName; +} diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 3cd3428..33ae293 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,14 +1,13 @@ -import { arch, tmpdir } from "os"; +import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; -import { - getAgentDownloadUrl, - getAgentVersion, - downloadFile, -} from "./downloadAgent.js"; +import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; + +const WINDOWS_SERVICE_NAME = "SafeChainAgent"; +const WINDOWS_APP_NAME = "SafeChain Agent"; export async function installOnWindows() { if (!(await isRunningAsAdmin())) { @@ -19,18 +18,17 @@ export async function installOnWindows() { return; } - const architecture = getWindowsArchitecture(); - const fileName = `SafeChainAgent-windows-${architecture}.msi`; - const downloadUrl = getAgentDownloadUrl(fileName); - const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`); ui.emptyLine(); - ui.writeInformation( - `📥 Downloading SafeChain Ultimate ${getAgentVersion()} (${architecture})...`, - ); - ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`); ui.writeVerbose(`Destination: ${msiPath}`); - await downloadFile(downloadUrl, msiPath); + + const result = await downloadAgentToFile(msiPath); + if (!result) { + ui.writeError("No download available for this platform/architecture."); + return; + } try { ui.emptyLine(); @@ -73,13 +71,6 @@ async function isRunningAsAdmin() { return result.status === 0 && result.stdout.trim() === "True"; } -function getWindowsArchitecture() { - const nodeArch = arch(); - if (nodeArch === "x64") return "amd64"; - if (nodeArch === "arm64") return "arm64"; - throw new Error(`Unsupported architecture: ${nodeArch}`); -} - async function uninstallIfInstalled() { // Query Win32_Product via WMI to find the installed SafeChain Agent. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. @@ -88,7 +79,7 @@ async function uninstallIfInstalled() { let productCode; try { productCode = execSync( - `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, + `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, { encoding: "utf8" }, ).trim(); } catch { @@ -132,7 +123,7 @@ async function runMsiInstaller(msiPath) { async function stopServiceIfRunning() { ui.writeInformation("⏹️ Stopping running service..."); - const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { + const result = await safeSpawn("net", ["stop", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -142,8 +133,10 @@ async function stopServiceIfRunning() { } async function startService() { - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { + ui.writeVerbose( + `Checking service status: sc query "${WINDOWS_SERVICE_NAME}"`, + ); + const queryResult = await safeSpawn("sc", ["query", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -152,8 +145,8 @@ async function startService() { return; } - ui.writeVerbose('Running: net start "SafeChainAgent"'); - const startResult = await safeSpawn("net", ["start", "SafeChainAgent"], { + ui.writeVerbose(`Running: net start "${WINDOWS_SERVICE_NAME}"`); + const startResult = await safeSpawn("net", ["start", WINDOWS_SERVICE_NAME], { stdio: "pipe", });