| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- # 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
|