security: verify signed manifest before module updates
This commit is contained in:
447
ToolHub.js
447
ToolHub.js
@@ -1,69 +1,23 @@
|
|||||||
// ToolHub - 入口文件 (加载子模块并执行)
|
// ToolHub - 入口文件 (加载子模块并执行)
|
||||||
// 将本文件粘贴到 ShortX 任务,子模块会自动从 git 下载到 ToolHub/code/
|
// 安全更新机制:入口内置 RSA 公钥,先验证 manifest.json/manifest.sig,再按 SHA256 下载子模块。
|
||||||
// 更新机制:HEAD 请求对比 Last-Modified,入口文件无需更新版本号
|
// 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 __dirChecked = false;
|
||||||
|
var __trustedManifest = null;
|
||||||
|
var __securityStatus = { ok: false, msg: "安全清单尚未校验" };
|
||||||
|
|
||||||
function buildNoCacheUrl(urlStr) {
|
function buildNoCacheUrl(urlStr) {
|
||||||
var sep = String(urlStr).indexOf("?") >= 0 ? "&" : "?";
|
var sep = String(urlStr).indexOf("?") >= 0 ? "&" : "?";
|
||||||
return String(urlStr) + sep + "_toolhub_ts=" + java.lang.System.currentTimeMillis();
|
return String(urlStr) + sep + "_toolhub_ts=" + java.lang.System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogPath() {
|
function getLogPath() { return shortx.getShortXDir() + "/ToolHub/logs/init.log"; }
|
||||||
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 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 writeLog(msg) {
|
function writeLog(msg) {
|
||||||
try {
|
try {
|
||||||
@@ -75,7 +29,7 @@ function writeLog(msg) {
|
|||||||
var writer = new java.io.FileWriter(f, true);
|
var writer = new java.io.FileWriter(f, true);
|
||||||
writer.write("[" + ts + "] " + String(msg) + "\n");
|
writer.write("[" + ts + "] " + String(msg) + "\n");
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runShell(cmdArr) {
|
function runShell(cmdArr) {
|
||||||
@@ -95,11 +49,9 @@ function checkDirPerms(path) {
|
|||||||
reader.close();
|
reader.close();
|
||||||
if (line) {
|
if (line) {
|
||||||
var parts = String(line).trim().split(/\s+/);
|
var parts = String(line).trim().split(/\s+/);
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) return String(parts[0]) === "1000" && String(parts[1]) === "1000" && String(parts[2]) === "700";
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,44 +60,104 @@ function setDirPerms(path) {
|
|||||||
runShell(["chown", "1000:1000", path]);
|
runShell(["chown", "1000:1000", path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteLastModified(urlStr) {
|
function ensureCodeDir() {
|
||||||
try {
|
var dir = new java.io.File(getCodeDirPath());
|
||||||
var url = new java.net.URL(buildNoCacheUrl(urlStr));
|
if (!__dirChecked) {
|
||||||
var conn = url.openConnection();
|
if (!dir.exists()) {
|
||||||
conn.setUseCaches(false);
|
dir.mkdirs();
|
||||||
conn.setRequestMethod("HEAD");
|
setDirPerms(dir.getAbsolutePath());
|
||||||
conn.setConnectTimeout(5000);
|
writeLog("Created dir: " + dir.getAbsolutePath());
|
||||||
conn.setReadTimeout(10000);
|
} else if (!checkDirPerms(dir.getAbsolutePath())) {
|
||||||
conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0");
|
setDirPerms(dir.getAbsolutePath());
|
||||||
conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate");
|
writeLog("Fixed dir perms: " + dir.getAbsolutePath());
|
||||||
conn.setRequestProperty("Pragma", "no-cache");
|
}
|
||||||
var code = conn.getResponseCode();
|
__dirChecked = true;
|
||||||
if (code !== 200) return null;
|
|
||||||
var lm = conn.getHeaderField("Last-Modified");
|
|
||||||
return lm ? String(lm) : null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
if (!dir.canWrite()) throw "Dir not writable: " + dir.getAbsolutePath();
|
||||||
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocalLastModified(relPath) {
|
function readTextFile(path) {
|
||||||
try {
|
try {
|
||||||
var f = new java.io.File(getLmPath(relPath));
|
var f = new java.io.File(path);
|
||||||
if (!f.exists()) return null;
|
if (!f.exists()) return null;
|
||||||
var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
|
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();
|
r.close();
|
||||||
return lm ? String(lm).trim() : null;
|
return String(sb.toString());
|
||||||
} catch (e) { return null; }
|
} catch (e) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveLocalLastModified(relPath, lm) {
|
function writeTextFile(path, text) {
|
||||||
try {
|
try {
|
||||||
var f = new java.io.File(getLmPath(relPath));
|
var f = new java.io.File(path);
|
||||||
var w = new java.io.FileWriter(f, false);
|
var parent = f.getParentFile();
|
||||||
w.write(String(lm || ""));
|
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();
|
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("<!DOCTYPE") >= 0 || prefix.indexOf("<html") >= 0) throw "Downloaded content is HTML";
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(urlStr, destFile) {
|
function downloadFile(urlStr, destFile) {
|
||||||
@@ -154,7 +166,7 @@ function downloadFile(urlStr, destFile) {
|
|||||||
conn.setUseCaches(false);
|
conn.setUseCaches(false);
|
||||||
conn.setConnectTimeout(10000);
|
conn.setConnectTimeout(10000);
|
||||||
conn.setReadTimeout(30000);
|
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("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
conn.setRequestProperty("Pragma", "no-cache");
|
conn.setRequestProperty("Pragma", "no-cache");
|
||||||
var code = conn.getResponseCode();
|
var code = conn.getResponseCode();
|
||||||
@@ -164,106 +176,137 @@ function downloadFile(urlStr, destFile) {
|
|||||||
var outStream = new java.io.FileOutputStream(destFile);
|
var outStream = new java.io.FileOutputStream(destFile);
|
||||||
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
|
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
|
||||||
var n, total = 0;
|
var n, total = 0;
|
||||||
while ((n = inStream.read(buf)) !== -1) {
|
while ((n = inStream.read(buf)) !== -1) { outStream.write(buf, 0, n); total += n; }
|
||||||
outStream.write(buf, 0, n); total += n;
|
|
||||||
}
|
|
||||||
outStream.close(); inStream.close();
|
outStream.close(); inStream.close();
|
||||||
if (expectedLen > 0 && total !== expectedLen) {
|
if (expectedLen > 0 && total !== expectedLen) throw "Size mismatch: expected=" + expectedLen + ", got=" + total;
|
||||||
throw "Size mismatch: expected=" + expectedLen + ", got=" + total;
|
|
||||||
}
|
|
||||||
var checkStream = new java.io.FileInputStream(destFile);
|
var checkStream = new java.io.FileInputStream(destFile);
|
||||||
var checkBuf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 200);
|
var checkBuf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 200);
|
||||||
var checkRead = checkStream.read(checkBuf);
|
var checkRead = checkStream.read(checkBuf);
|
||||||
checkStream.close();
|
checkStream.close();
|
||||||
if (checkRead > 0) {
|
if (checkRead > 0) {
|
||||||
var prefix = new java.lang.String(checkBuf, 0, checkRead, "UTF-8");
|
var prefix = new java.lang.String(checkBuf, 0, checkRead, "UTF-8");
|
||||||
if (prefix.indexOf("<!DOCTYPE") >= 0 || prefix.indexOf("<html") >= 0) {
|
if (prefix.indexOf("<!DOCTYPE") >= 0 || prefix.indexOf("<html") >= 0) throw "Downloaded content is HTML, not JS";
|
||||||
throw "Downloaded content is HTML, not JS";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return total;
|
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) {
|
function loadScript(relPath) {
|
||||||
try {
|
try {
|
||||||
var base = shortx.getShortXDir();
|
var dir = ensureCodeDir();
|
||||||
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 f = new java.io.File(dir, relPath);
|
var f = new java.io.File(dir, relPath);
|
||||||
var needsDownload = !f.exists();
|
var result;
|
||||||
var isNew = !f.exists();
|
if (__trustedManifest) result = ensureVerifiedModule(relPath, f);
|
||||||
|
else result = ensureLocalTrustedModule(relPath, f);
|
||||||
|
|
||||||
// 本地文件存在时,HEAD 检查远程是否有更新
|
if (result.updated) {
|
||||||
if (!needsDownload) {
|
__moduleUpdates.push({ module: relPath, isNew: !!result.isNew, size: result.size });
|
||||||
try {
|
writeLog("Verified update " + relPath + " (" + result.size + " bytes, sha256=" + result.hash + ")");
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSize = f.length();
|
var fileSize = f.length();
|
||||||
if (fileSize > 200 * 1024) {
|
if (fileSize > 200 * 1024) writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting");
|
||||||
writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting");
|
|
||||||
}
|
|
||||||
|
|
||||||
var actualHash = sha256File(f.getAbsolutePath());
|
var code = readTextFile(f.getAbsolutePath());
|
||||||
var cachedHash = getLocalSha256(relPath);
|
if (code === null) throw "read failed: " + f.getAbsolutePath();
|
||||||
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 geval = eval;
|
var geval = eval;
|
||||||
geval(String(sb.toString()));
|
geval(String(code));
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
var errMsg = "loadScript(" + relPath + ") failed: " + e;
|
var errMsg = "loadScript(" + relPath + ") failed: " + e;
|
||||||
try { android.util.Log.e("ToolHub", errMsg); } catch(eLog) {}
|
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 __moduleUpdates = [];
|
||||||
var loadErrors = [];
|
var loadErrors = [];
|
||||||
var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true };
|
var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true };
|
||||||
|
fetchTrustedManifest();
|
||||||
|
|
||||||
for (var i = 0; i < modules.length; i++) {
|
for (var i = 0; i < modules.length; i++) {
|
||||||
try {
|
try {
|
||||||
loadScript(modules[i]);
|
loadScript(modules[i]);
|
||||||
@@ -286,41 +331,22 @@ for (var i = 0; i < modules.length; i++) {
|
|||||||
writeLog(modErr);
|
writeLog(modErr);
|
||||||
try { android.util.Log.e("ToolHub", modErr); } catch(eLog) {}
|
try { android.util.Log.e("ToolHub", modErr); } catch(eLog) {}
|
||||||
loadErrors.push({ module: modules[i], err: String(e) });
|
loadErrors.push({ module: modules[i], err: String(e) });
|
||||||
if (criticalModules[modules[i]]) {
|
if (criticalModules[modules[i]]) throw "Critical module failed: " + modules[i] + " (" + String(e) + ")";
|
||||||
throw "Critical module failed: " + modules[i] + " (" + String(e) + ")";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (__trustedManifest && loadErrors.length === 0) saveTrustedVersion(__trustedManifest.version);
|
||||||
|
|
||||||
var __out = (function() {
|
var __out = (function() {
|
||||||
// 关键函数未加载成功时提前返回友好错误,避免 ReferenceError
|
|
||||||
if (typeof getProcessInfo !== "function") {
|
if (typeof getProcessInfo !== "function") {
|
||||||
return {
|
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功" };
|
||||||
ok: false,
|
|
||||||
started: false,
|
|
||||||
msg: "ToolHub 启动失败",
|
|
||||||
err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功(网络下载失败或文件缺失)"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (typeof ToolHubLogger !== "function") {
|
if (typeof ToolHubLogger !== "function") {
|
||||||
return {
|
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功" };
|
||||||
ok: false,
|
|
||||||
started: false,
|
|
||||||
msg: "ToolHub 启动失败",
|
|
||||||
err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (typeof FloatBallAppWM !== "function") {
|
if (typeof FloatBallAppWM !== "function") {
|
||||||
return {
|
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 FloatBallAppWM 未定义,请检查 th_02_core.js / th_16_entry.js 是否加载成功" };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
function optStr(v) { return (v === undefined || v === null) ? "" : String(v); }
|
||||||
function summarizeModuleUpdates(list) {
|
function summarizeModuleUpdates(list) {
|
||||||
var names = [];
|
var names = [];
|
||||||
var created = 0;
|
var created = 0;
|
||||||
@@ -332,18 +358,8 @@ var __out = (function() {
|
|||||||
if (name) names.push(name);
|
if (name) names.push(name);
|
||||||
if (item.isNew) created++; else overwritten++;
|
if (item.isNew) created++; else overwritten++;
|
||||||
}
|
}
|
||||||
if (names.length === 0) {
|
if (names.length === 0) return { count: 0, modules: [], msg: "子模块已是最新,本次未覆盖更新。" };
|
||||||
return {
|
return { count: names.length, modules: names, msg: "本次已通过签名校验并覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "):" + names.join("、") };
|
||||||
count: 0,
|
|
||||||
modules: [],
|
|
||||||
msg: "子模块已是最新,本次未覆盖更新。"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
count: names.length,
|
|
||||||
modules: names,
|
|
||||||
msg: "本次已覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "):" + names.join("、")
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
function summarizeLoadErrors(list) {
|
function summarizeLoadErrors(list) {
|
||||||
var names = [];
|
var names = [];
|
||||||
@@ -353,11 +369,7 @@ var __out = (function() {
|
|||||||
var name = optStr(item.module);
|
var name = optStr(item.module);
|
||||||
if (name) names.push(name);
|
if (name) names.push(name);
|
||||||
}
|
}
|
||||||
return {
|
return { count: names.length, modules: names, msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。" };
|
||||||
count: names.length,
|
|
||||||
modules: names,
|
|
||||||
msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryInfo = getProcessInfo("entry");
|
var entryInfo = getProcessInfo("entry");
|
||||||
@@ -366,10 +378,9 @@ var __out = (function() {
|
|||||||
var app = new FloatBallAppWM(logger);
|
var app = new FloatBallAppWM(logger);
|
||||||
var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE");
|
var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE");
|
||||||
var startRet = null;
|
var startRet = null;
|
||||||
try {
|
try { startRet = app.startAsync(entryInfo, closeRule); }
|
||||||
startRet = app.startAsync(entryInfo, closeRule);
|
catch (eTop) {
|
||||||
} catch (eTop) {
|
try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) {}
|
||||||
try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) { safeLog(null, 'e', "catch " + String(eLog)); }
|
|
||||||
startRet = { ok: false, err: String(eTop) };
|
startRet = { ok: false, err: String(eTop) };
|
||||||
}
|
}
|
||||||
var syncInfo = summarizeModuleUpdates(__moduleUpdates);
|
var syncInfo = summarizeModuleUpdates(__moduleUpdates);
|
||||||
@@ -380,6 +391,8 @@ var __out = (function() {
|
|||||||
ok: started,
|
ok: started,
|
||||||
started: started,
|
started: started,
|
||||||
msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败",
|
msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败",
|
||||||
|
securityMsg: __securityStatus.msg,
|
||||||
|
manifestVersion: __securityStatus.version || 0,
|
||||||
syncMsg: syncInfo.msg,
|
syncMsg: syncInfo.msg,
|
||||||
updatedCount: syncInfo.count,
|
updatedCount: syncInfo.count,
|
||||||
updatedModules: syncInfo.modules,
|
updatedModules: syncInfo.modules,
|
||||||
@@ -389,9 +402,7 @@ var __out = (function() {
|
|||||||
if (loadInfo.count > 0) {
|
if (loadInfo.count > 0) {
|
||||||
out.loadMsg = loadInfo.msg;
|
out.loadMsg = loadInfo.msg;
|
||||||
out.loadErrors = loadInfo.modules;
|
out.loadErrors = loadInfo.modules;
|
||||||
if (!started) {
|
if (!started) out.err = loadInfo.modules.join(", ");
|
||||||
out.err = loadInfo.modules.join(", ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!started && !out.err) out.err = optStr(startRet && startRet.err) || "未知错误";
|
if (!started && !out.err) out.err = optStr(startRet && startRet.err) || "未知错误";
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
71
manifest.json
Normal file
71
manifest.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
1
manifest.sig
Normal file
1
manifest.sig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bXIc036dCe2RPuVJUV3QJO5Q3FZ3kc08337NRrjtYC03xV7evj7kYb1aa2jBadL6IaDSypE1BNtX4OzN0qDTXf4PxfPDVGcJrGLVJcipk+HpMDx2h2hlZZQq5A+1wx5R4XeA2NrMvclcUsLL2elRH/SzBr7pgR93hkzDMAJwJjyZrMUO2aa40EdmHWcFjNmYf3GzLd/8JD3IdRmi/WrVaikggnln2Pd0bsBNKJ1C3HGIKf+ZpZ2nFH5Y2NLgrvfDy+WVYiE3n4qnk8iwYGfEw3XqGG6zQy8QzrXVHmUyhQQ5YvqbcA79xmI9mqu7tEkXXcr3DLQBI0RZ5uYJGDxVmUVz0in2Mx/eBwPBpL4ivQ991BPy2ye8hZBt1WuXBCn8CRb00RL5P2iMUpEFv5b534Kw28JJap/T2KZQ8UZmODk8DWDQ6LOQfAmEShdrP7PqK/3uYYG0493uHqCD6BKpWo6YEnLvi2F/1DR9gPwHuoqjysF6gRNmKPAbQk5/BVv+
|
||||||
69
scripts/generate_signed_manifest.py
Executable file
69
scripts/generate_signed_manifest.py
Executable file
@@ -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()
|
||||||
Reference in New Issue
Block a user