"""授权校验(专业版技能 · 79 元买断)。 为什么技能版要单独一套: - 企业版绑 CPU + 主板 + 系统盘,走 PowerShell CIM,重; - 技能版是个人电脑上的命令行工具,用「系统盘卷序列号 + 网卡 MAC」就够, 纯 ctypes,没有外部依赖。 为什么**不**把主机名算进去:改个电脑名(很常见的操作)就会让授权失效, 客户会觉得"我什么都没换就用不了了"。少一个因子,少一类售后。 安全模型和一个诚实的说明: - 私钥只在卖家手里,签好的 license.json 改一个字节就失效; - 但脚本本身是明文 Python,**能改的人就能绕**。这挡的是"买一份到处装", 不是"专业破解"。79 元的东西,不值得上更强的保护,也不该假装有。 """ from __future__ import annotations import base64 import ctypes import hashlib import json import os import uuid from dataclasses import dataclass from datetime import date from pathlib import Path # 公钥:与私钥 tools/keys/skill-private.key 配对(私钥不进包) PUBLIC_KEY_B64 = "JwsMs1BizzTzhY0P3gbDLboNqVhwft4tcqqx+1DfblE=" TRIAL_DOCS = 20 # 未授权时单次最多处理份数 PRODUCT = "document-archive-pro" def data_dir() -> Path: return Path(os.environ.get("DOCARCH_HOME") or (Path.home() / ".document-archive")) def license_path() -> Path: return data_dir() / "license.json" def _volume_serial() -> str: """系统盘卷序列号(格式化重建会变,换盘也会变)。""" try: root = os.environ.get("SystemDrive", "C:") + "\\" serial = ctypes.c_ulong(0) ok = ctypes.windll.kernel32.GetVolumeInformationW( ctypes.c_wchar_p(root), None, 0, ctypes.byref(serial), None, None, None, 0) return f"{serial.value:08X}" if ok else "0" except Exception: # noqa: BLE001 return "0" def machine_code() -> str: # 只用两个跟硬件/系统盘有关的因子: # 卷序列号(重装系统才变)+ MAC(换网卡才变)。 # 主机名、用户名都不参与——那些太容易变,会把正常用户锁在门外。 raw = "|".join([ f"{uuid.getnode():012X}", _volume_serial(), ]) return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16].upper() def _canonical(payload: dict) -> bytes: body = {k: v for k, v in payload.items() if k != "signature"} return json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") @dataclass class Status: ok: bool reason: str customer: str = "" expires_at: str = "" days_left: int = 0 machine_code: str = "" def check() -> Status: mc = machine_code() path = license_path() if not path.exists(): return Status(False, f"未授权(试用模式:单次最多 {TRIAL_DOCS} 份)", machine_code=mc) try: payload = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: return Status(False, f"授权文件读不了:{exc}", machine_code=mc) sig_b64 = payload.get("signature", "") if not sig_b64: return Status(False, "授权文件缺少签名", machine_code=mc) try: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIC_KEY_B64)) try: pub.verify(base64.b64decode(sig_b64), _canonical(payload)) except InvalidSignature: return Status(False, "授权签名无效(文件被改过或不是本产品签发)", machine_code=mc) except ImportError: return Status(False, "缺少 cryptography 依赖,跑 scripts/install.py 安装", machine_code=mc) if payload.get("product") != PRODUCT: return Status(False, "授权不对应本产品", machine_code=mc) if str(payload.get("machine_code", "")).upper() != mc: return Status(False, f"授权绑定的另一台机器(本机 {mc})", machine_code=mc) expires = str(payload.get("expires_at", "")) if expires: try: left = (date.fromisoformat(expires) - date.today()).days except ValueError: return Status(False, f"授权到期日格式不对:{expires}", machine_code=mc) if left < 0: return Status(False, f"授权已于 {expires} 到期", machine_code=mc) else: left = 36500 return Status(True, f"已授权给 {payload.get('customer', '')}", customer=payload.get("customer", ""), expires_at=expires, days_left=left, machine_code=mc) def trial_cap() -> int: """未授权时单次处理的文档上限;已授权返回 0 表示不限制。""" return 0 if check().ok else TRIAL_DOCS