# license_manager.py (PyQt5 版本) import os import json import base64 import hashlib import uuid import platform import subprocess import sys from datetime import datetime, timedelta try: from cryptography.hazmat.primitives.ciphers.aead import AESGCM except ImportError: raise ImportError("请先安装依赖: pip install cryptography") ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef" def get_app_dir(): if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) LICENSE_FILE = os.path.join(get_app_dir(), "license.lic") def get_machine_guid(): system = platform.system() if system == "Windows": try: import winreg with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key: return winreg.QueryValueEx(key, "MachineGuid")[0] except Exception: pass elif system == "Linux": for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]: if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: return f.read().strip() elif system == "Darwin": try: result = subprocess.run( ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], capture_output=True, text=True ) for line in result.stdout.split("\n"): if "IOPlatformUUID" in line: return line.split('"')[-2] except Exception: pass return str(uuid.getnode()) def get_machine_code(): guid = get_machine_guid() h = hashlib.sha256() h.update(b"LicenseSystem") h.update(guid.encode("utf-8")) return h.hexdigest() def decrypt_key(encrypted_key: str): try: data = base64.b64decode(encrypted_key) except Exception: raise ValueError("Base64 解码失败") if len(data) < 12: raise ValueError("密文太短") aesgcm = AESGCM(ENCRYPTION_KEY) nonce = data[:12] ciphertext = data[12:] try: plaintext = aesgcm.decrypt(nonce, ciphertext, None) except Exception: raise ValueError("解密失败,密钥无效或已被篡改") try: return json.loads(plaintext.decode("utf-8")) except Exception: raise ValueError("许可证数据解析失败") def validate_key(key: str): machine_code = get_machine_code() license_info = decrypt_key(key) if license_info.get("machine_code") != machine_code: raise ValueError("机器码不匹配,该密钥无法在本机使用") expire_str = license_info.get("expire_time") if not expire_str: raise ValueError("许可证信息不完整") expire_time = datetime.fromisoformat(expire_str) if datetime.now() > expire_time: raise ValueError("许可证已过期") return license_info def save_license(license_info: dict): with open(LICENSE_FILE, "w", encoding="utf-8") as f: json.dump(license_info, f, indent=2, ensure_ascii=False) def load_license(): if not os.path.exists(LICENSE_FILE): return None try: with open(LICENSE_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: return None def verify_license(): """ PyQt5 弹窗验证许可证。 返回 True:验证通过。 返回 False:用户关闭窗口,程序应退出。 """ from PyQt5.QtWidgets import (QApplication, QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QHBoxLayout, QMessageBox) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont # 1. 先尝试本地缓存 cached = load_license() if cached: try: if cached.get("machine_code") != get_machine_code(): raise ValueError("机器码不匹配") if datetime.now() > datetime.fromisoformat(cached.get("expire_time")): raise ValueError("许可证已过期") return True except Exception: if os.path.exists(LICENSE_FILE): os.remove(LICENSE_FILE) machine_code = get_machine_code() # 确保有 QApplication 实例 app = QApplication.instance() if not app: app = QApplication(sys.argv) dialog = QDialog() dialog.setWindowTitle("软件激活") dialog.setFixedSize(450, 300) dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) # 窗口居中 screen = app.primaryScreen().geometry() dialog.move( (screen.width() - dialog.width()) // 2, (screen.height() - dialog.height()) // 2 ) layout = QVBoxLayout() # 机器码(可复制) layout.addWidget(QLabel("您的机器码(可复制):")) mc_edit = QLineEdit(machine_code) mc_edit.setReadOnly(True) mc_edit.setFont(QFont("Consolas", 10)) mc_edit.setAlignment(Qt.AlignCenter) layout.addWidget(mc_edit) tip = QLabel("请将机器码发给管理员获取密钥") tip.setAlignment(Qt.AlignCenter) tip.setStyleSheet("color: gray;") layout.addWidget(tip) # 密钥输入 layout.addWidget(QLabel("许可证密钥:")) key_edit = QLineEdit() key_edit.setEchoMode(QLineEdit.Password) key_edit.setFont(QFont("Consolas", 10)) key_edit.setAlignment(Qt.AlignCenter) key_edit.setPlaceholderText("在此粘贴密钥") layout.addWidget(key_edit) # 错误提示 error_label = QLabel("") error_label.setStyleSheet("color: red;") error_label.setAlignment(Qt.AlignCenter) layout.addWidget(error_label) # 按钮 btn_layout = QHBoxLayout() ok_btn = QPushButton("确认激活") close_btn = QPushButton("关闭") btn_layout.addWidget(ok_btn) btn_layout.addWidget(close_btn) layout.addLayout(btn_layout) dialog.setLayout(layout) # 结果标志 result = [False] def on_verify(): key = key_edit.text().strip() if not key: error_label.setText("密钥不能为空") return try: info = validate_key(key) save_license(info) expire = datetime.fromisoformat(info["expire_time"]) days_left = (expire - datetime.now()).days QMessageBox.information( dialog, "激活成功", f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天" ) result[0] = True dialog.accept() except Exception as e: error_label.setText(str(e)) key_edit.clear() key_edit.setFocus() def on_close(): dialog.reject() ok_btn.clicked.connect(on_verify) close_btn.clicked.connect(on_close) key_edit.returnPressed.connect(on_verify) ret = dialog.exec_() return result[0] and ret == QDialog.Accepted