license_qt5.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # license_manager.py (PyQt5 版本)
  2. import os
  3. import json
  4. import base64
  5. import hashlib
  6. import uuid
  7. import platform
  8. import subprocess
  9. import sys
  10. from datetime import datetime, timedelta
  11. try:
  12. from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  13. except ImportError:
  14. raise ImportError("请先安装依赖: pip install cryptography")
  15. ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef"
  16. def get_app_dir():
  17. if getattr(sys, 'frozen', False):
  18. return os.path.dirname(sys.executable)
  19. return os.path.dirname(os.path.abspath(__file__))
  20. LICENSE_FILE = os.path.join(get_app_dir(), "license.lic")
  21. def get_machine_guid():
  22. system = platform.system()
  23. if system == "Windows":
  24. try:
  25. import winreg
  26. with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key:
  27. return winreg.QueryValueEx(key, "MachineGuid")[0]
  28. except Exception:
  29. pass
  30. elif system == "Linux":
  31. for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
  32. if os.path.exists(path):
  33. with open(path, "r", encoding="utf-8") as f:
  34. return f.read().strip()
  35. elif system == "Darwin":
  36. try:
  37. result = subprocess.run(
  38. ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
  39. capture_output=True, text=True
  40. )
  41. for line in result.stdout.split("\n"):
  42. if "IOPlatformUUID" in line:
  43. return line.split('"')[-2]
  44. except Exception:
  45. pass
  46. return str(uuid.getnode())
  47. def get_machine_code():
  48. guid = get_machine_guid()
  49. h = hashlib.sha256()
  50. h.update(b"LicenseSystem")
  51. h.update(guid.encode("utf-8"))
  52. return h.hexdigest()
  53. def decrypt_key(encrypted_key: str):
  54. try:
  55. data = base64.b64decode(encrypted_key)
  56. except Exception:
  57. raise ValueError("Base64 解码失败")
  58. if len(data) < 12:
  59. raise ValueError("密文太短")
  60. aesgcm = AESGCM(ENCRYPTION_KEY)
  61. nonce = data[:12]
  62. ciphertext = data[12:]
  63. try:
  64. plaintext = aesgcm.decrypt(nonce, ciphertext, None)
  65. except Exception:
  66. raise ValueError("解密失败,密钥无效或已被篡改")
  67. try:
  68. return json.loads(plaintext.decode("utf-8"))
  69. except Exception:
  70. raise ValueError("许可证数据解析失败")
  71. def validate_key(key: str):
  72. machine_code = get_machine_code()
  73. license_info = decrypt_key(key)
  74. if license_info.get("machine_code") != machine_code:
  75. raise ValueError("机器码不匹配,该密钥无法在本机使用")
  76. expire_str = license_info.get("expire_time")
  77. if not expire_str:
  78. raise ValueError("许可证信息不完整")
  79. expire_time = datetime.fromisoformat(expire_str)
  80. if datetime.now() > expire_time:
  81. raise ValueError("许可证已过期")
  82. return license_info
  83. def save_license(license_info: dict):
  84. with open(LICENSE_FILE, "w", encoding="utf-8") as f:
  85. json.dump(license_info, f, indent=2, ensure_ascii=False)
  86. def load_license():
  87. if not os.path.exists(LICENSE_FILE):
  88. return None
  89. try:
  90. with open(LICENSE_FILE, "r", encoding="utf-8") as f:
  91. return json.load(f)
  92. except Exception:
  93. return None
  94. def verify_license():
  95. """
  96. PyQt5 弹窗验证许可证。
  97. 返回 True:验证通过。
  98. 返回 False:用户关闭窗口,程序应退出。
  99. """
  100. from PyQt5.QtWidgets import (QApplication, QDialog, QVBoxLayout, QLabel,
  101. QLineEdit, QPushButton, QHBoxLayout, QMessageBox)
  102. from PyQt5.QtCore import Qt
  103. from PyQt5.QtGui import QFont
  104. # 1. 先尝试本地缓存
  105. cached = load_license()
  106. if cached:
  107. try:
  108. if cached.get("machine_code") != get_machine_code():
  109. raise ValueError("机器码不匹配")
  110. if datetime.now() > datetime.fromisoformat(cached.get("expire_time")):
  111. raise ValueError("许可证已过期")
  112. return True
  113. except Exception:
  114. if os.path.exists(LICENSE_FILE):
  115. os.remove(LICENSE_FILE)
  116. machine_code = get_machine_code()
  117. # 确保有 QApplication 实例
  118. app = QApplication.instance()
  119. if not app:
  120. app = QApplication(sys.argv)
  121. dialog = QDialog()
  122. dialog.setWindowTitle("软件激活")
  123. dialog.setFixedSize(450, 300)
  124. dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
  125. # 窗口居中
  126. screen = app.primaryScreen().geometry()
  127. dialog.move(
  128. (screen.width() - dialog.width()) // 2,
  129. (screen.height() - dialog.height()) // 2
  130. )
  131. layout = QVBoxLayout()
  132. # 机器码(可复制)
  133. layout.addWidget(QLabel("您的机器码(可复制):"))
  134. mc_edit = QLineEdit(machine_code)
  135. mc_edit.setReadOnly(True)
  136. mc_edit.setFont(QFont("Consolas", 10))
  137. mc_edit.setAlignment(Qt.AlignCenter)
  138. layout.addWidget(mc_edit)
  139. tip = QLabel("请将机器码发给管理员获取密钥")
  140. tip.setAlignment(Qt.AlignCenter)
  141. tip.setStyleSheet("color: gray;")
  142. layout.addWidget(tip)
  143. # 密钥输入
  144. layout.addWidget(QLabel("许可证密钥:"))
  145. key_edit = QLineEdit()
  146. key_edit.setEchoMode(QLineEdit.Password)
  147. key_edit.setFont(QFont("Consolas", 10))
  148. key_edit.setAlignment(Qt.AlignCenter)
  149. key_edit.setPlaceholderText("在此粘贴密钥")
  150. layout.addWidget(key_edit)
  151. # 错误提示
  152. error_label = QLabel("")
  153. error_label.setStyleSheet("color: red;")
  154. error_label.setAlignment(Qt.AlignCenter)
  155. layout.addWidget(error_label)
  156. # 按钮
  157. btn_layout = QHBoxLayout()
  158. ok_btn = QPushButton("确认激活")
  159. close_btn = QPushButton("关闭")
  160. btn_layout.addWidget(ok_btn)
  161. btn_layout.addWidget(close_btn)
  162. layout.addLayout(btn_layout)
  163. dialog.setLayout(layout)
  164. # 结果标志
  165. result = [False]
  166. def on_verify():
  167. key = key_edit.text().strip()
  168. if not key:
  169. error_label.setText("密钥不能为空")
  170. return
  171. try:
  172. info = validate_key(key)
  173. save_license(info)
  174. expire = datetime.fromisoformat(info["expire_time"])
  175. days_left = (expire - datetime.now()).days
  176. QMessageBox.information(
  177. dialog, "激活成功",
  178. f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天"
  179. )
  180. result[0] = True
  181. dialog.accept()
  182. except Exception as e:
  183. error_label.setText(str(e))
  184. key_edit.clear()
  185. key_edit.setFocus()
  186. def on_close():
  187. dialog.reject()
  188. ok_btn.clicked.connect(on_verify)
  189. close_btn.clicked.connect(on_close)
  190. key_edit.returnPressed.connect(on_verify)
  191. ret = dialog.exec_()
  192. return result[0] and ret == QDialog.Accepted