diff --git a/ToolHub.js b/ToolHub.js index 6054b7d..b083cdc 100644 --- a/ToolHub.js +++ b/ToolHub.js @@ -1,69 +1,23 @@ // ToolHub - 入口文件 (加载子模块并执行) -// 将本文件粘贴到 ShortX 任务,子模块会自动从 git 下载到 ToolHub/code/ -// 更新机制:HEAD 请求对比 Last-Modified,入口文件无需更新版本号 +// 安全更新机制:入口内置 RSA 公钥,先验证 manifest.json/manifest.sig,再按 SHA256 下载子模块。 +// Gitea 只负责分发;未通过签名/哈希/防回滚校验时,不覆盖本地模块。 -var GIT_BASE = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/code/"; +var GIT_ROOT = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/"; +var GIT_BASE = GIT_ROOT + "code/"; +var TRUSTED_PUBLIC_KEY_B64 = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE="; var __dirChecked = false; +var __trustedManifest = null; +var __securityStatus = { ok: false, msg: "安全清单尚未校验" }; function buildNoCacheUrl(urlStr) { var sep = String(urlStr).indexOf("?") >= 0 ? "&" : "?"; return String(urlStr) + sep + "_toolhub_ts=" + java.lang.System.currentTimeMillis(); } -function getLogPath() { - return shortx.getShortXDir() + "/ToolHub/logs/init.log"; -} - -function getLmPath(relPath) { - return shortx.getShortXDir() + "/ToolHub/code/.lm_" + relPath; -} - -function getShaPath(relPath) { - return shortx.getShortXDir() + "/ToolHub/code/.sha_" + relPath; -} - -function sha256File(path) { - try { - var md = java.security.MessageDigest.getInstance("SHA-256"); - var fis = new java.io.FileInputStream(path); - var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192); - var n; - while ((n = fis.read(buf)) !== -1) { - md.update(buf, 0, n); - } - fis.close(); - var digest = md.digest(); - var sb = new java.lang.StringBuilder(); - for (var i = 0; i < digest.length; i++) { - var hex = java.lang.Integer.toHexString(0xFF & digest[i]); - if (hex.length() === 1) sb.append("0"); - sb.append(hex); - } - return String(sb.toString()); - } catch (e) { - return null; - } -} - -function saveSha256(relPath, hash) { - try { - var f = new java.io.File(getShaPath(relPath)); - var w = new java.io.FileWriter(f, false); - w.write(String(hash || "")); - w.close(); - } catch (e) { safeLog(null, 'e', "catch " + String(e)); } -} - -function getLocalSha256(relPath) { - try { - var f = new java.io.File(getShaPath(relPath)); - if (!f.exists()) return null; - var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8")); - var hash = r.readLine(); - r.close(); - return hash ? String(hash).trim() : null; - } catch (e) { return null; } -} +function getLogPath() { return shortx.getShortXDir() + "/ToolHub/logs/init.log"; } +function getCodeDirPath() { return shortx.getShortXDir() + "/ToolHub/code/"; } +function getTrustedShaPath(relPath) { return getCodeDirPath() + ".trusted_sha_" + relPath; } +function getTrustedVersionPath() { return getCodeDirPath() + ".trusted_manifest_version"; } function writeLog(msg) { try { @@ -75,7 +29,7 @@ function writeLog(msg) { var writer = new java.io.FileWriter(f, true); writer.write("[" + ts + "] " + String(msg) + "\n"); writer.close(); - } catch(e) { safeLog(null, 'e', "catch " + String(e)); } + } catch (e) {} } function runShell(cmdArr) { @@ -95,11 +49,9 @@ function checkDirPerms(path) { reader.close(); if (line) { var parts = String(line).trim().split(/\s+/); - if (parts.length >= 3) { - return String(parts[0]) === "1000" && String(parts[1]) === "1000" && String(parts[2]) === "700"; - } + if (parts.length >= 3) return String(parts[0]) === "1000" && String(parts[1]) === "1000" && String(parts[2]) === "700"; } - } catch(e) { safeLog(null, 'e', "catch " + String(e)); } + } catch (e) {} return false; } @@ -108,44 +60,104 @@ function setDirPerms(path) { runShell(["chown", "1000:1000", path]); } -function getRemoteLastModified(urlStr) { - try { - var url = new java.net.URL(buildNoCacheUrl(urlStr)); - var conn = url.openConnection(); - conn.setUseCaches(false); - conn.setRequestMethod("HEAD"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(10000); - conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0"); - conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate"); - conn.setRequestProperty("Pragma", "no-cache"); - var code = conn.getResponseCode(); - if (code !== 200) return null; - var lm = conn.getHeaderField("Last-Modified"); - return lm ? String(lm) : null; - } catch (e) { - return null; +function ensureCodeDir() { + var dir = new java.io.File(getCodeDirPath()); + if (!__dirChecked) { + if (!dir.exists()) { + dir.mkdirs(); + setDirPerms(dir.getAbsolutePath()); + writeLog("Created dir: " + dir.getAbsolutePath()); + } else if (!checkDirPerms(dir.getAbsolutePath())) { + setDirPerms(dir.getAbsolutePath()); + writeLog("Fixed dir perms: " + dir.getAbsolutePath()); + } + __dirChecked = true; } + if (!dir.canWrite()) throw "Dir not writable: " + dir.getAbsolutePath(); + return dir; } -function getLocalLastModified(relPath) { +function readTextFile(path) { try { - var f = new java.io.File(getLmPath(relPath)); + var f = new java.io.File(path); if (!f.exists()) return null; var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8")); - var lm = r.readLine(); + var sb = new java.lang.StringBuilder(); + var line; + while ((line = r.readLine()) != null) sb.append(line).append("\n"); r.close(); - return lm ? String(lm).trim() : null; + return String(sb.toString()); } catch (e) { return null; } } -function saveLocalLastModified(relPath, lm) { +function writeTextFile(path, text) { try { - var f = new java.io.File(getLmPath(relPath)); - var w = new java.io.FileWriter(f, false); - w.write(String(lm || "")); + var f = new java.io.File(path); + var parent = f.getParentFile(); + if (parent && !parent.exists()) parent.mkdirs(); + var w = new java.io.OutputStreamWriter(new java.io.FileOutputStream(f, false), "UTF-8"); + w.write(String(text)); w.close(); - } catch(e) { safeLog(null, 'e', "catch " + String(e)); } + return true; + } catch (e) { return false; } +} + +function readFirstLine(path) { + var txt = readTextFile(path); + if (!txt) return null; + var parts = String(txt).split(/\r?\n/); + return parts.length > 0 ? String(parts[0]).trim() : null; +} + +function sha256File(fileOrPath) { + try { + var path = String(fileOrPath); + var md = java.security.MessageDigest.getInstance("SHA-256"); + var fis = new java.io.FileInputStream(path); + var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192); + var n; + while ((n = fis.read(buf)) !== -1) md.update(buf, 0, n); + fis.close(); + var digest = md.digest(); + var sb = new java.lang.StringBuilder(); + for (var i = 0; i < digest.length; i++) { + var hex = java.lang.Integer.toHexString(0xFF & digest[i]); + if (hex.length() === 1) sb.append("0"); + sb.append(hex); + } + return String(sb.toString()); + } catch (e) { return null; } +} + +function saveTrustedSha(relPath, hash) { writeTextFile(getTrustedShaPath(relPath), String(hash || "")); } +function getTrustedSha(relPath) { return readFirstLine(getTrustedShaPath(relPath)); } +function getTrustedVersion() { + var line = readFirstLine(getTrustedVersionPath()); + var v = line ? parseInt(String(line), 10) : 0; + return isNaN(v) ? 0 : v; +} +function saveTrustedVersion(v) { writeTextFile(getTrustedVersionPath(), String(v || 0)); } + +function downloadText(urlStr) { + var url = new java.net.URL(buildNoCacheUrl(urlStr)); + var conn = url.openConnection(); + conn.setUseCaches(false); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + conn.setRequestProperty("User-Agent", "ShortX-ToolHub/secure-updater"); + conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate"); + conn.setRequestProperty("Pragma", "no-cache"); + var code = conn.getResponseCode(); + if (code !== 200) throw "HTTP " + code; + var r = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), "UTF-8")); + var sb = new java.lang.StringBuilder(); + var line; + while ((line = r.readLine()) != null) sb.append(line).append("\n"); + r.close(); + var text = String(sb.toString()); + var prefix = text.length > 200 ? text.substring(0, 200) : text; + if (prefix.indexOf("= 0 || prefix.indexOf("= 0) throw "Downloaded content is HTML"; + return text; } function downloadFile(urlStr, destFile) { @@ -154,7 +166,7 @@ function downloadFile(urlStr, destFile) { conn.setUseCaches(false); conn.setConnectTimeout(10000); conn.setReadTimeout(30000); - conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0"); + conn.setRequestProperty("User-Agent", "ShortX-ToolHub/secure-updater"); conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate"); conn.setRequestProperty("Pragma", "no-cache"); var code = conn.getResponseCode(); @@ -164,106 +176,137 @@ function downloadFile(urlStr, destFile) { var outStream = new java.io.FileOutputStream(destFile); var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192); var n, total = 0; - while ((n = inStream.read(buf)) !== -1) { - outStream.write(buf, 0, n); total += n; - } + while ((n = inStream.read(buf)) !== -1) { outStream.write(buf, 0, n); total += n; } outStream.close(); inStream.close(); - if (expectedLen > 0 && total !== expectedLen) { - throw "Size mismatch: expected=" + expectedLen + ", got=" + total; - } + if (expectedLen > 0 && total !== expectedLen) throw "Size mismatch: expected=" + expectedLen + ", got=" + total; var checkStream = new java.io.FileInputStream(destFile); var checkBuf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 200); var checkRead = checkStream.read(checkBuf); checkStream.close(); if (checkRead > 0) { var prefix = new java.lang.String(checkBuf, 0, checkRead, "UTF-8"); - if (prefix.indexOf("= 0 || prefix.indexOf("= 0) { - throw "Downloaded content is HTML, not JS"; - } + if (prefix.indexOf("= 0 || prefix.indexOf("= 0) throw "Downloaded content is HTML, not JS"; } return total; } +function base64Decode(s) { + return android.util.Base64.decode(String(s).replace(/\s+/g, ""), android.util.Base64.DEFAULT); +} + +function verifyManifestSignature(manifestText, sigText) { + try { + var pubBytes = base64Decode(TRUSTED_PUBLIC_KEY_B64); + var sigBytes = base64Decode(sigText); + var spec = new java.security.spec.X509EncodedKeySpec(pubBytes); + var pubKey = java.security.KeyFactory.getInstance("RSA").generatePublic(spec); + var verifier = java.security.Signature.getInstance("SHA256withRSA"); + verifier.initVerify(pubKey); + verifier.update(new java.lang.String(String(manifestText)).getBytes("UTF-8")); + return verifier.verify(sigBytes); + } catch (e) { + writeLog("Manifest signature verify exception: " + String(e)); + return false; + } +} + +function fetchTrustedManifest() { + try { + ensureCodeDir(); + var manifestText = downloadText(GIT_ROOT + "manifest.json"); + var sigText = downloadText(GIT_ROOT + "manifest.sig"); + if (!verifyManifestSignature(manifestText, sigText)) throw "manifest signature invalid"; + var manifest = JSON.parse(String(manifestText)); + if (!manifest || !manifest.files) throw "manifest files missing"; + if (String(manifest.alg || "") !== "SHA256withRSA") throw "unsupported manifest alg: " + String(manifest.alg); + var version = parseInt(String(manifest.version || "0"), 10); + if (isNaN(version) || version <= 0) throw "invalid manifest version"; + var localVersion = getTrustedVersion(); + if (localVersion > 0 && version < localVersion) throw "manifest rollback: remote=" + version + ", local=" + localVersion; + __trustedManifest = manifest; + __securityStatus = { ok: true, msg: "安全清单验签通过,version=" + version, version: version }; + writeLog(__securityStatus.msg); + return manifest; + } catch (e) { + __trustedManifest = null; + __securityStatus = { ok: false, msg: "安全清单校验失败,已停止远程拉取:" + String(e) }; + writeLog(__securityStatus.msg); + return null; + } +} + +function replaceFile(tmpFile, destFile) { + try { + if (destFile.exists() && !destFile.delete()) throw "delete old file failed: " + destFile.getAbsolutePath(); + if (!tmpFile.renameTo(destFile)) throw "rename tmp failed: " + tmpFile.getAbsolutePath(); + return true; + } catch (e) { throw String(e); } +} + +function getManifestInfo(relPath) { + if (!__trustedManifest || !__trustedManifest.files) return null; + return __trustedManifest.files[relPath] || null; +} + +function ensureVerifiedModule(relPath, destFile) { + var info = getManifestInfo(relPath); + if (!info || !info.sha256) throw "module not in trusted manifest: " + relPath; + var expectedHash = String(info.sha256).toLowerCase(); + var expectedSize = Number(info.size || 0); + var actualHash = destFile.exists() ? sha256File(destFile.getAbsolutePath()) : null; + if (destFile.exists() && actualHash && String(actualHash).toLowerCase() === expectedHash) { + saveTrustedSha(relPath, expectedHash); + return { updated: false, size: destFile.length(), hash: actualHash }; + } + + var tmpFile = new java.io.File(destFile.getAbsolutePath() + ".tmp"); + try { if (tmpFile.exists()) tmpFile.delete(); } catch (eDelTmp0) {} + var size = downloadFile(GIT_BASE + relPath, tmpFile); + if (expectedSize > 0 && size !== expectedSize) { + try { tmpFile.delete(); } catch (eDelSize) {} + throw "manifest size mismatch for " + relPath + ": expected=" + expectedSize + ", got=" + size; + } + var tmpHash = sha256File(tmpFile.getAbsolutePath()); + if (!tmpHash || String(tmpHash).toLowerCase() !== expectedHash) { + try { tmpFile.delete(); } catch (eDelHash) {} + throw "manifest SHA256 mismatch for " + relPath + ": expected=" + expectedHash + ", actual=" + tmpHash; + } + var wasNew = !destFile.exists(); + replaceFile(tmpFile, destFile); + saveTrustedSha(relPath, expectedHash); + return { updated: true, isNew: wasNew, size: size, hash: tmpHash }; +} + +function ensureLocalTrustedModule(relPath, destFile) { + if (!destFile.exists()) throw "安全清单不可用且本地模块不存在: " + relPath; + var trustedHash = getTrustedSha(relPath); + var actualHash = sha256File(destFile.getAbsolutePath()); + if (!trustedHash || !actualHash || String(trustedHash).toLowerCase() !== String(actualHash).toLowerCase()) { + throw "安全清单不可用,本地模块也无可信 SHA256: " + relPath; + } + return { updated: false, size: destFile.length(), hash: actualHash }; +} + function loadScript(relPath) { try { - var base = shortx.getShortXDir(); - var dir = new java.io.File(base + "/ToolHub/code/"); - - if (!__dirChecked) { - if (!dir.exists()) { - dir.mkdirs(); - setDirPerms(dir.getAbsolutePath()); - writeLog("Created dir: " + dir.getAbsolutePath()); - } else if (!checkDirPerms(dir.getAbsolutePath())) { - setDirPerms(dir.getAbsolutePath()); - writeLog("Fixed dir perms: " + dir.getAbsolutePath()); - } - __dirChecked = true; - } - - if (!dir.canWrite()) throw "Dir not writable: " + dir.getAbsolutePath(); - + var dir = ensureCodeDir(); var f = new java.io.File(dir, relPath); - var needsDownload = !f.exists(); - var isNew = !f.exists(); + var result; + if (__trustedManifest) result = ensureVerifiedModule(relPath, f); + else result = ensureLocalTrustedModule(relPath, f); - // 本地文件存在时,HEAD 检查远程是否有更新 - if (!needsDownload) { - try { - var urlStr = GIT_BASE + relPath; - var remoteLm = getRemoteLastModified(urlStr); - var localLm = getLocalLastModified(relPath); - if (remoteLm && remoteLm !== localLm) { - needsDownload = true; - writeLog("Update detected for " + relPath + ": remote=" + remoteLm + ", local=" + localLm); - } - } catch (netErr) { - writeLog("Network check skipped for " + relPath + ": " + String(netErr)); - } - } - - if (needsDownload) { - try { - var urlStr = GIT_BASE + relPath; - writeLog("Downloading " + relPath + " from " + urlStr); - var size = downloadFile(urlStr, f); - var remoteLm = getRemoteLastModified(urlStr); - if (remoteLm) saveLocalLastModified(relPath, remoteLm); - var hash = sha256File(f.getAbsolutePath()); - if (hash) saveSha256(relPath, hash); - writeLog("Downloaded " + relPath + " (" + size + " bytes, sha256=" + (hash || "null") + ")"); - // 记录更新信息 - __moduleUpdates.push({ module: relPath, isNew: isNew, size: size }); - } catch (dlErr) { - if (!f.exists()) { - throw "Not found: " + f.getAbsolutePath() + ", download failed: " + dlErr; - } - writeLog("Download failed for " + relPath + ", using existing local file: " + String(dlErr)); - } + if (result.updated) { + __moduleUpdates.push({ module: relPath, isNew: !!result.isNew, size: result.size }); + writeLog("Verified update " + relPath + " (" + result.size + " bytes, sha256=" + result.hash + ")"); } var fileSize = f.length(); - if (fileSize > 200 * 1024) { - writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting"); - } + if (fileSize > 200 * 1024) writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting"); - var actualHash = sha256File(f.getAbsolutePath()); - var cachedHash = getLocalSha256(relPath); - if (cachedHash && actualHash && actualHash !== cachedHash) { - throw "SHA256 mismatch for " + relPath + ": expected=" + cachedHash + ", actual=" + actualHash; - } - if (actualHash && !cachedHash) { - saveSha256(relPath, actualHash); - } - - var r = new java.io.BufferedReader(new java.io.InputStreamReader( - new java.io.FileInputStream(f), "UTF-8")); - var sb = new java.lang.StringBuilder(); - var line; - while ((line = r.readLine()) != null) sb.append(line).append("\n"); - r.close(); + var code = readTextFile(f.getAbsolutePath()); + if (code === null) throw "read failed: " + f.getAbsolutePath(); var geval = eval; - geval(String(sb.toString())); + geval(String(code)); } catch(e) { var errMsg = "loadScript(" + relPath + ") failed: " + e; try { android.util.Log.e("ToolHub", errMsg); } catch(eLog) {} @@ -278,6 +321,8 @@ var modules = ["th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.j var __moduleUpdates = []; var loadErrors = []; var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true }; +fetchTrustedManifest(); + for (var i = 0; i < modules.length; i++) { try { loadScript(modules[i]); @@ -286,41 +331,22 @@ for (var i = 0; i < modules.length; i++) { writeLog(modErr); try { android.util.Log.e("ToolHub", modErr); } catch(eLog) {} loadErrors.push({ module: modules[i], err: String(e) }); - if (criticalModules[modules[i]]) { - throw "Critical module failed: " + modules[i] + " (" + String(e) + ")"; - } + if (criticalModules[modules[i]]) throw "Critical module failed: " + modules[i] + " (" + String(e) + ")"; } } +if (__trustedManifest && loadErrors.length === 0) saveTrustedVersion(__trustedManifest.version); var __out = (function() { - // 关键函数未加载成功时提前返回友好错误,避免 ReferenceError if (typeof getProcessInfo !== "function") { - return { - ok: false, - started: false, - msg: "ToolHub 启动失败", - err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功(网络下载失败或文件缺失)" - }; + return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功" }; } if (typeof ToolHubLogger !== "function") { - return { - ok: false, - started: false, - msg: "ToolHub 启动失败", - err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功" - }; + return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功" }; } if (typeof FloatBallAppWM !== "function") { - return { - ok: false, - started: false, - msg: "ToolHub 启动失败", - err: "核心类 FloatBallAppWM 未定义,请检查 th_02_core.js / th_16_entry.js 是否加载成功" - }; - } - function optStr(v) { - return (v === undefined || v === null) ? "" : String(v); + return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 FloatBallAppWM 未定义,请检查 th_02_core.js / th_16_entry.js 是否加载成功" }; } + function optStr(v) { return (v === undefined || v === null) ? "" : String(v); } function summarizeModuleUpdates(list) { var names = []; var created = 0; @@ -332,18 +358,8 @@ var __out = (function() { if (name) names.push(name); if (item.isNew) created++; else overwritten++; } - if (names.length === 0) { - return { - count: 0, - modules: [], - msg: "子模块已是最新,本次未覆盖更新。" - }; - } - return { - count: names.length, - modules: names, - msg: "本次已覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "):" + names.join("、") - }; + if (names.length === 0) return { count: 0, modules: [], msg: "子模块已是最新,本次未覆盖更新。" }; + return { count: names.length, modules: names, msg: "本次已通过签名校验并覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "):" + names.join("、") }; } function summarizeLoadErrors(list) { var names = []; @@ -353,11 +369,7 @@ var __out = (function() { var name = optStr(item.module); if (name) names.push(name); } - return { - count: names.length, - modules: names, - msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。" - }; + return { count: names.length, modules: names, msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。" }; } var entryInfo = getProcessInfo("entry"); @@ -366,10 +378,9 @@ var __out = (function() { var app = new FloatBallAppWM(logger); var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE"); var startRet = null; - try { - startRet = app.startAsync(entryInfo, closeRule); - } catch (eTop) { - try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) { safeLog(null, 'e', "catch " + String(eLog)); } + try { startRet = app.startAsync(entryInfo, closeRule); } + catch (eTop) { + try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) {} startRet = { ok: false, err: String(eTop) }; } var syncInfo = summarizeModuleUpdates(__moduleUpdates); @@ -380,6 +391,8 @@ var __out = (function() { ok: started, started: started, msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败", + securityMsg: __securityStatus.msg, + manifestVersion: __securityStatus.version || 0, syncMsg: syncInfo.msg, updatedCount: syncInfo.count, updatedModules: syncInfo.modules, @@ -389,9 +402,7 @@ var __out = (function() { if (loadInfo.count > 0) { out.loadMsg = loadInfo.msg; out.loadErrors = loadInfo.modules; - if (!started) { - out.err = loadInfo.modules.join(", "); - } + if (!started) out.err = loadInfo.modules.join(", "); } if (!started && !out.err) out.err = optStr(startRet && startRet.err) || "未知错误"; return out; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..6f3d230 --- /dev/null +++ b/manifest.json @@ -0,0 +1,71 @@ +{ + "alg": "SHA256withRSA", + "files": { + "th_01_base.js": { + "sha256": "30c121902ef5a0006a5daabca8443ed6cbf6e2508209604c48f89855d0690034", + "size": 52546 + }, + "th_02_core.js": { + "sha256": "aa16cd97d8afc7df98cb91c2209fa0434fdda3c0877e63b54da6c75d0766e35e", + "size": 3907 + }, + "th_03_icon.js": { + "sha256": "717f7f37474d3616c2cd944581455f600020a850ec8812100d0546ec1302c987", + "size": 5598 + }, + "th_04_theme.js": { + "sha256": "b815acfee30e56458e61c033ab3fddfe5d8d0c52ec172c419bad7187fcb341ba", + "size": 36083 + }, + "th_05_persistence.js": { + "sha256": "d80787c2810839ebbe499e93db3df33d6e8d2d6b6ae71644ce351db0f36e4d3e", + "size": 14077 + }, + "th_06_icon_parser.js": { + "sha256": "25b95a5df634a7ee359f3ab798e4d3154a71c24016f7b4bf8a658096644b2484", + "size": 22909 + }, + "th_07_shortcut.js": { + "sha256": "7b2dbd1e35c636cca4ccce335dfb9e0b972342972ce012116ff4bbcfc438caa1", + "size": 72992 + }, + "th_08_content.js": { + "sha256": "8a76f15dfd1292081cba4b2dd218424be66540350e2807065421a6176a86c2db", + "size": 7938 + }, + "th_09_animation.js": { + "sha256": "b647101561fdd426579601340ffbade9bf20ded9b1945130c9f0f959b9299df0", + "size": 23994 + }, + "th_10_shell.js": { + "sha256": "0ed793079c2f6ba7d29f4c0d411705cb72419f45f572cbe37ed32ac16527a8bc", + "size": 1094 + }, + "th_11_action.js": { + "sha256": "a0142d26621f3d076bd1b749f2885af2c0806c9f206e362a3b3680a5d2312b31", + "size": 13545 + }, + "th_12_rebuild.js": { + "sha256": "7b820e813d2dd8866778fefe8bfeb6aca227bb1a32a89d318de830178f19824f", + "size": 2362 + }, + "th_13_panel_ui.js": { + "sha256": "19e5e1c346051aafdda6253ed7c25ef5fbc4f883de66f656c9575d97e9d6ffc8", + "size": 20386 + }, + "th_14_panels.js": { + "sha256": "65528739877540cdd457017ebd083e1ad0f1f911706129d75a88647c48a59c51", + "size": 220509 + }, + "th_15_extra.js": { + "sha256": "c8e10fe54a965fe7b27f0996b2f2636655077e39df774d6cbb1e73c5b36553fc", + "size": 62625 + }, + "th_16_entry.js": { + "sha256": "8c7d2d8dfa1dc51b47a01be8639a9da78cb40670c66cc8f78d339afecfd83be5", + "size": 11125 + } + }, + "schema": 1, + "version": 20260507152251 +} diff --git a/manifest.sig b/manifest.sig new file mode 100644 index 0000000..866db50 --- /dev/null +++ b/manifest.sig @@ -0,0 +1 @@ +bXIc036dCe2RPuVJUV3QJO5Q3FZ3kc08337NRrjtYC03xV7evj7kYb1aa2jBadL6IaDSypE1BNtX4OzN0qDTXf4PxfPDVGcJrGLVJcipk+HpMDx2h2hlZZQq5A+1wx5R4XeA2NrMvclcUsLL2elRH/SzBr7pgR93hkzDMAJwJjyZrMUO2aa40EdmHWcFjNmYf3GzLd/8JD3IdRmi/WrVaikggnln2Pd0bsBNKJ1C3HGIKf+ZpZ2nFH5Y2NLgrvfDy+WVYiE3n4qnk8iwYGfEw3XqGG6zQy8QzrXVHmUyhQQ5YvqbcA79xmI9mqu7tEkXXcr3DLQBI0RZ5uYJGDxVmUVz0in2Mx/eBwPBpL4ivQ991BPy2ye8hZBt1WuXBCn8CRb00RL5P2iMUpEFv5b534Kw28JJap/T2KZQ8UZmODk8DWDQ6LOQfAmEShdrP7PqK/3uYYG0493uHqCD6BKpWo6YEnLvi2F/1DR9gPwHuoqjysF6gRNmKPAbQk5/BVv+ diff --git a/scripts/generate_signed_manifest.py b/scripts/generate_signed_manifest.py new file mode 100755 index 0000000..05fcfa0 --- /dev/null +++ b/scripts/generate_signed_manifest.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Generate ToolHub signed manifest. + +Requires openssl CLI. Private key is kept outside the repo by default: + ~/.hermes/toolhub_signing/private_key.pem +""" +import base64 +import hashlib +import json +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CODE_DIR = ROOT / "code" +PRIVATE_KEY = Path.home() / ".hermes" / "toolhub_signing" / "private_key.pem" +MANIFEST = ROOT / "manifest.json" +SIG = ROOT / "manifest.sig" + +MODULES = [ + "th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", + "th_05_persistence.js", "th_06_icon_parser.js", "th_07_shortcut.js", + "th_08_content.js", "th_09_animation.js", "th_10_shell.js", + "th_11_action.js", "th_12_rebuild.js", "th_13_panel_ui.js", + "th_14_panels.js", "th_15_extra.js", "th_16_entry.js", +] + + +def sha256_file(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def main() -> None: + if not PRIVATE_KEY.exists(): + raise SystemExit(f"Private key not found: {PRIVATE_KEY}") + + files = {} + for name in MODULES: + path = CODE_DIR / name + if not path.exists(): + raise SystemExit(f"Missing module: {path}") + files[name] = { + "sha256": sha256_file(path), + "size": path.stat().st_size, + } + + manifest = { + "schema": 1, + "version": int(time.strftime("%Y%m%d%H%M%S", time.gmtime())), + "alg": "SHA256withRSA", + "files": files, + } + data = (json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8") + MANIFEST.write_bytes(data) + + sig_bin = subprocess.check_output([ + "openssl", "dgst", "-sha256", "-sign", str(PRIVATE_KEY), str(MANIFEST) + ]) + SIG.write_text(base64.b64encode(sig_bin).decode("ascii") + "\n", encoding="utf-8") + print(f"manifest_version={manifest['version']}") + print(f"signed_files={len(files)}") + + +if __name__ == "__main__": + main()