diff --git a/ToolHub.js b/ToolHub.js index b083cdc..7ffaa29 100644 --- a/ToolHub.js +++ b/ToolHub.js @@ -4,7 +4,11 @@ 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 TRUSTED_PUBLIC_KEYS = { + "toolhub-targets-2026-rsa3072": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE=" +}; +var DEFAULT_TRUSTED_KEY_ID = "toolhub-targets-2026-rsa3072"; +var MIN_TRUSTED_MANIFEST_VERSION = 20260507152251; var __dirChecked = false; var __trustedManifest = null; var __securityStatus = { ok: false, msg: "安全清单尚未校验" }; @@ -194,9 +198,17 @@ function base64Decode(s) { return android.util.Base64.decode(String(s).replace(/\s+/g, ""), android.util.Base64.DEFAULT); } -function verifyManifestSignature(manifestText, sigText) { +function getTrustedPublicKeyB64(keyId) { + var kid = keyId ? String(keyId) : DEFAULT_TRUSTED_KEY_ID; + if (TRUSTED_PUBLIC_KEYS.hasOwnProperty(kid)) return TRUSTED_PUBLIC_KEYS[kid]; + return null; +} + +function verifyManifestSignature(manifestText, sigText, keyId) { try { - var pubBytes = base64Decode(TRUSTED_PUBLIC_KEY_B64); + var pubB64 = getTrustedPublicKeyB64(keyId); + if (!pubB64) throw "unknown manifest keyId: " + String(keyId); + var pubBytes = base64Decode(pubB64); var sigBytes = base64Decode(sigText); var spec = new java.security.spec.X509EncodedKeySpec(pubBytes); var pubKey = java.security.KeyFactory.getInstance("RSA").generatePublic(spec); @@ -215,16 +227,19 @@ function fetchTrustedManifest() { 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 keyId = String(manifest.keyId || DEFAULT_TRUSTED_KEY_ID); + if (!getTrustedPublicKeyB64(keyId)) throw "untrusted manifest keyId: " + keyId; + if (!verifyManifestSignature(manifestText, sigText, keyId)) throw "manifest signature invalid"; var version = parseInt(String(manifest.version || "0"), 10); if (isNaN(version) || version <= 0) throw "invalid manifest version"; + if (version < MIN_TRUSTED_MANIFEST_VERSION) throw "manifest below minimum trusted version: remote=" + version + ", min=" + MIN_TRUSTED_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 }; + __securityStatus = { ok: true, msg: "安全清单验签通过,version=" + version + ", keyId=" + keyId, version: version, keyId: keyId }; writeLog(__securityStatus.msg); return manifest; } catch (e) { @@ -393,6 +408,8 @@ var __out = (function() { msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败", securityMsg: __securityStatus.msg, manifestVersion: __securityStatus.version || 0, + manifestKeyId: __securityStatus.keyId || "", + minManifestVersion: MIN_TRUSTED_MANIFEST_VERSION, syncMsg: syncInfo.msg, updatedCount: syncInfo.count, updatedModules: syncInfo.modules, diff --git a/ToolHub.js.sha256 b/ToolHub.js.sha256 new file mode 100644 index 0000000..f370d21 --- /dev/null +++ b/ToolHub.js.sha256 @@ -0,0 +1 @@ +279b1a5e7a9dee01bbfc8c67fab650285e9e1a899ec91f4fee08ac569287a393 ToolHub.js diff --git a/manifest.json b/manifest.json index 6f3d230..aa6c256 100644 --- a/manifest.json +++ b/manifest.json @@ -66,6 +66,7 @@ "size": 11125 } }, - "schema": 1, - "version": 20260507152251 + "keyId": "toolhub-targets-2026-rsa3072", + "schema": 2, + "version": 20260507154625 } diff --git a/manifest.sig b/manifest.sig index 866db50..1de1ad4 100644 --- a/manifest.sig +++ b/manifest.sig @@ -1 +1 @@ -bXIc036dCe2RPuVJUV3QJO5Q3FZ3kc08337NRrjtYC03xV7evj7kYb1aa2jBadL6IaDSypE1BNtX4OzN0qDTXf4PxfPDVGcJrGLVJcipk+HpMDx2h2hlZZQq5A+1wx5R4XeA2NrMvclcUsLL2elRH/SzBr7pgR93hkzDMAJwJjyZrMUO2aa40EdmHWcFjNmYf3GzLd/8JD3IdRmi/WrVaikggnln2Pd0bsBNKJ1C3HGIKf+ZpZ2nFH5Y2NLgrvfDy+WVYiE3n4qnk8iwYGfEw3XqGG6zQy8QzrXVHmUyhQQ5YvqbcA79xmI9mqu7tEkXXcr3DLQBI0RZ5uYJGDxVmUVz0in2Mx/eBwPBpL4ivQ991BPy2ye8hZBt1WuXBCn8CRb00RL5P2iMUpEFv5b534Kw28JJap/T2KZQ8UZmODk8DWDQ6LOQfAmEShdrP7PqK/3uYYG0493uHqCD6BKpWo6YEnLvi2F/1DR9gPwHuoqjysF6gRNmKPAbQk5/BVv+ +HleQZVymedTYdJbRyd2SNcndHqUMH1FuVjy7MJi9NQtgZKVNdKRBwns9w6le7cZZW8a2Xb3fqqlsBjJaDYQ3x/LveeE18u2SfNRoC97Q0lDVj9sxPRD/vsw6HQgDDuCMdqezmfWlROobd5CHHAjJvmaV+zHH6A3VYga6tAnY3/4JwVcoDua2W1Sjk3PB2Lqi9YcIGy3ub2gfm+RKakyBvYy9k/8cb7KbUbovPDU4oviS3Xn26aF3lN3o8tbTWBg9XYgsXM3gAqWZ+Y0YVXuZZB4VjgDw9cUITlZ8vxCxloo66nJSGy7/7CwDJ69F0i+vbipibracv2wut/RMqqkUaGy+PSd+EVFUFuLxoMFTs5eT4ybiSqOfj0/wdRfv//8XNa/s9sMZru+/PgKyNmSMTivAcdxjip43YNvRd/uWtQZTaHC72iVlHEeqNSPAR5gHPWcKuJ19EPecuVRr3wPK9i8Ois4QCpgdibCJ7cE2341x56ertHJQRAfjTN/UJ7eb diff --git a/scripts/generate_signed_manifest.py b/scripts/generate_signed_manifest.py index 05fcfa0..9e47fe4 100755 --- a/scripts/generate_signed_manifest.py +++ b/scripts/generate_signed_manifest.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 -"""Generate ToolHub signed manifest. +"""Generate ToolHub signed manifest and entry checksum. -Requires openssl CLI. Private key is kept outside the repo by default: - ~/.hermes/toolhub_signing/private_key.pem +Security notes: +- Private key stays outside the repo by default: ~/.hermes/toolhub_signing/private_key.pem +- The script prints git status/diff summary before signing. Use --yes in automation. """ +import argparse import base64 import hashlib import json import subprocess +import sys import time from pathlib import Path @@ -16,6 +19,9 @@ CODE_DIR = ROOT / "code" PRIVATE_KEY = Path.home() / ".hermes" / "toolhub_signing" / "private_key.pem" MANIFEST = ROOT / "manifest.json" SIG = ROOT / "manifest.sig" +ENTRY = ROOT / "ToolHub.js" +ENTRY_SHA = ROOT / "ToolHub.js.sha256" +DEFAULT_KEY_ID = "toolhub-targets-2026-rsa3072" MODULES = [ "th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", @@ -34,9 +40,43 @@ def sha256_file(path: Path) -> str: return h.hexdigest() +def git_output(args): + try: + return subprocess.check_output(["git", *args], cwd=ROOT, text=True, stderr=subprocess.STDOUT) + except Exception as e: + return f"\n" + + +def print_review_summary() -> None: + print("== ToolHub signing review ==") + print("-- git status --") + status = git_output(["status", "--short"]) + print(status.rstrip() or "clean") + print("-- git diff --stat --") + diff_stat = git_output(["diff", "--stat"]) + print(diff_stat.rstrip() or "no unstaged diff") + print("-- staged diff --stat --") + staged = git_output(["diff", "--cached", "--stat"]) + print(staged.rstrip() or "no staged diff") + + def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--yes", action="store_true", help="skip interactive confirmation after review summary") + ap.add_argument("--key-id", default=DEFAULT_KEY_ID) + ap.add_argument("--version", type=int, default=0, help="manifest version; default UTC yyyyMMddHHmmss") + args = ap.parse_args() + if not PRIVATE_KEY.exists(): raise SystemExit(f"Private key not found: {PRIVATE_KEY}") + if not ENTRY.exists(): + raise SystemExit(f"Entry file not found: {ENTRY}") + + print_review_summary() + if not args.yes: + ans = input("Sign this manifest? Type YES to continue: ").strip() + if ans != "YES": + raise SystemExit("aborted") files = {} for name in MODULES: @@ -48,9 +88,11 @@ def main() -> None: "size": path.stat().st_size, } + version = args.version or int(time.strftime("%Y%m%d%H%M%S", time.gmtime())) manifest = { - "schema": 1, - "version": int(time.strftime("%Y%m%d%H%M%S", time.gmtime())), + "schema": 2, + "version": version, + "keyId": args.key_id, "alg": "SHA256withRSA", "files": files, } @@ -61,8 +103,15 @@ def main() -> None: "openssl", "dgst", "-sha256", "-sign", str(PRIVATE_KEY), str(MANIFEST) ]) SIG.write_text(base64.b64encode(sig_bin).decode("ascii") + "\n", encoding="utf-8") + + entry_hash = sha256_file(ENTRY) + ENTRY_SHA.write_text(f"{entry_hash} ToolHub.js\n", encoding="utf-8") + + print("== signed manifest ==") print(f"manifest_version={manifest['version']}") + print(f"key_id={manifest['keyId']}") print(f"signed_files={len(files)}") + print(f"ToolHub.js_sha256={entry_hash}") if __name__ == "__main__":