chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
# InSAR License Issuer
This folder contains the offline license issuing tool for the LIC2 scheme used by the backend.
## Files
```text
license-issuer/
├── issue_license.py # CLI entry and reusable signing logic
├── license_issuer_gui.pyw # Desktop GUI
├── start_gui.bat # Windows launcher for the GUI
├── private_key.b64 # Private key, keep offline and do not distribute
├── public_key.b64 # Public key, can be synced to backend
└── README.md
```
## Requirements
```bash
pip install cryptography
```
The GUI uses the Python standard library `tkinter`, so no extra GUI dependency is required.
## GUI Usage
Windows:
```bat
start_gui.bat
```
Or directly open:
```text
license_issuer_gui.pyw
```
The GUI provides these flows:
- Read the current machine fingerprint
- Issue a `.lic` file
- Verify an existing `.lic` file
- Rotate key pairs
- Sync `public_key.b64` into `backend/app/license_service.py`
The backend sync target is configurable. If the issuer tool is copied to another machine or another folder layout, choose the target `license_service.py` manually in the GUI, or use `--target` in CLI.
## CLI Usage
Show help:
```bash
python issue_license.py --help
```
Get local fingerprint:
```bash
python issue_license.py fingerprint
```
Issue a license:
```bash
python issue_license.py issue ^
--to "XX省自然资源厅" ^
--fingerprint <fingerprint> ^
--days 365 ^
--output license_xx.lic
```
Verify a license:
```bash
python issue_license.py verify license_xx.lic
```
Generate a new key pair:
```bash
python issue_license.py rotate-key
```
Force rotate an existing key pair:
```bash
python issue_license.py rotate-key --force
```
Rotate and immediately sync the new public key to backend:
```bash
python issue_license.py rotate-key --force --sync-backend
```
Sync the current `public_key.b64` to backend without rotating:
```bash
python issue_license.py sync-public-key
```
## Standard Flow
1. Run `fingerprint` on the target machine and collect the value.
2. Run `issue` on the issuer machine and generate the `.lic` file.
3. Upload the `.lic` file through the admin page, or replace `backend/license/license.lic`.
4. If keys are rotated, sync the new public key to `backend/app/license_service.py` and redeploy the backend.
## Notes
- The private key must stay offline and should not be committed or distributed.
- Rotating the private key invalidates old licenses. All customer licenses must then be reissued.
- The fingerprint algorithm is intentionally kept consistent with `backend/app/license_service.py`.
+423
View File
@@ -0,0 +1,423 @@
"""
InSAR license issuer for the LIC2 format.
This module keeps the existing CLI workflow, and also exposes reusable
functions for the desktop GUI.
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
import re
import subprocess
import sys
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, Optional
ISSUER_DIR = Path(__file__).resolve().parent
PRIVATE_KEY_FILE = ISSUER_DIR / "private_key.b64"
PUBLIC_KEY_FILE = ISSUER_DIR / "public_key.b64"
BACKEND_LICENSE_SERVICE_FILE = ISSUER_DIR.parent / "backend" / "app" / "license_service.py"
LICENSE_HEADER = b"LIC2"
PUBLIC_KEY_PATTERN = re.compile(r'^_PUBLIC_KEY_B64\s*=\s*"([^"]*)"', re.MULTILINE)
def _load_cryptography():
try:
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
return Ed25519PrivateKey, Ed25519PublicKey, InvalidSignature, serialization
except ImportError:
raise RuntimeError("Missing dependency: pip install cryptography")
def resolve_backend_license_service_file(target: str | Path | None = None) -> Path:
if target is None:
env_target = str(os.environ.get("LICENSE_ISSUER_BACKEND_FILE", "")).strip()
if env_target:
return Path(env_target).expanduser()
return BACKEND_LICENSE_SERVICE_FILE
return Path(target).expanduser()
def _run(args: list[str]) -> str:
try:
output = subprocess.check_output(
args,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
shell=False,
)
return output.decode("utf-8", errors="ignore").strip()
except Exception:
return ""
def _run_ps(command: str) -> str:
return _run(["powershell", "-NoProfile", "-Command", command])
def _pick_value(text: str) -> str:
lines = [line.strip() for line in text.splitlines() if line.strip()]
if len(lines) <= 1:
return ""
values = [value for value in lines[1:] if value.lower() not in {"serialnumber", "uuid"}]
return values[0] if values else ""
def get_machine_fingerprint() -> str:
uuid_text = _run(["wmic", "csproduct", "get", "uuid"])
if not uuid_text:
uuid_text = _run_ps("(Get-CimInstance Win32_ComputerSystemProduct).UUID")
disk_text = _run(["wmic", "diskdrive", "get", "serialnumber"])
if not disk_text:
disk_text = _run_ps(
"(Get-CimInstance Win32_DiskDrive | Select-Object -First 1 -ExpandProperty SerialNumber)"
)
uuid_value = _pick_value(uuid_text)
disk_value = _pick_value(disk_text)
mac_value = f"{uuid.getnode():012x}"
if not mac_value or mac_value == "000000000000":
mac_value = _run_ps(
"(Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | "
"Select-Object -First 1 -ExpandProperty MacAddress)"
) or ""
raw = "|".join([uuid_value, disk_value, mac_value])
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def read_public_key_b64(path: Path = PUBLIC_KEY_FILE) -> str:
if not path.exists():
raise FileNotFoundError(f"Public key file not found: {path}")
return path.read_text(encoding="utf-8").strip()
def read_private_key_b64(path: Path = PRIVATE_KEY_FILE) -> str:
if not path.exists():
raise FileNotFoundError(f"Private key file not found: {path}")
return path.read_text(encoding="utf-8").strip()
def read_backend_public_key_b64(path: str | Path | None = None) -> Optional[str]:
target_path = resolve_backend_license_service_file(path)
if not target_path.exists():
return None
content = target_path.read_text(encoding="utf-8")
match = PUBLIC_KEY_PATTERN.search(content)
return match.group(1) if match else None
def get_key_status(backend_target: str | Path | None = None) -> Dict[str, Any]:
backend_path = resolve_backend_license_service_file(backend_target)
public_key_b64 = None
backend_public_key_b64 = None
if PUBLIC_KEY_FILE.exists():
public_key_b64 = read_public_key_b64(PUBLIC_KEY_FILE)
if backend_path.exists():
backend_public_key_b64 = read_backend_public_key_b64(backend_path)
return {
"private_key_exists": PRIVATE_KEY_FILE.exists(),
"public_key_exists": PUBLIC_KEY_FILE.exists(),
"private_key_path": str(PRIVATE_KEY_FILE),
"public_key_path": str(PUBLIC_KEY_FILE),
"backend_license_service_path": str(backend_path),
"backend_license_service_exists": backend_path.exists(),
"public_key_b64": public_key_b64,
"backend_public_key_b64": backend_public_key_b64,
"backend_synced": bool(public_key_b64 and public_key_b64 == backend_public_key_b64),
}
def _load_private_key():
Ed25519PrivateKey, _, _, _ = _load_cryptography()
raw = base64.b64decode(read_private_key_b64(PRIVATE_KEY_FILE))
return Ed25519PrivateKey.from_private_bytes(raw)
def rotate_key_pair(force: bool = False) -> Dict[str, Any]:
Ed25519PrivateKey, _, _, serialization = _load_cryptography()
if PRIVATE_KEY_FILE.exists() and not force:
raise FileExistsError(
"Private key already exists. Use force=True only if you really want to rotate it."
)
key = Ed25519PrivateKey.generate()
private_key_b64 = base64.b64encode(
key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
).decode("utf-8")
public_key_b64 = base64.b64encode(
key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("utf-8")
PRIVATE_KEY_FILE.write_text(private_key_b64, encoding="utf-8")
PUBLIC_KEY_FILE.write_text(public_key_b64, encoding="utf-8")
return {
"private_key_path": str(PRIVATE_KEY_FILE),
"public_key_path": str(PUBLIC_KEY_FILE),
"public_key_b64": public_key_b64,
"force": force,
}
def sync_backend_public_key(
*,
public_key_b64: Optional[str] = None,
target_path: str | Path | None = None,
) -> Dict[str, Any]:
resolved_target_path = resolve_backend_license_service_file(target_path)
key_b64 = (public_key_b64 or read_public_key_b64(PUBLIC_KEY_FILE)).strip()
if not resolved_target_path.exists():
raise FileNotFoundError(f"Backend license service file not found: {resolved_target_path}")
content = resolved_target_path.read_text(encoding="utf-8")
match = PUBLIC_KEY_PATTERN.search(content)
if not match:
raise ValueError("Could not find _PUBLIC_KEY_B64 in backend license service.")
old_key_b64 = match.group(1)
updated_content = PUBLIC_KEY_PATTERN.sub(
f'_PUBLIC_KEY_B64 = "{key_b64}"',
content,
count=1,
)
resolved_target_path.write_text(updated_content, encoding="utf-8")
return {
"target_path": str(resolved_target_path),
"old_public_key_b64": old_key_b64,
"public_key_b64": key_b64,
"changed": old_key_b64 != key_b64,
}
def _normalize_output_path(issued_to: str, output: Optional[str]) -> Path:
if output:
return Path(output)
safe_name = re.sub(r"[^0-9A-Za-z_\-\u4e00-\u9fff]+", "_", issued_to).strip("_")
safe_name = safe_name[:32] or "license"
return ISSUER_DIR / f"license_{safe_name}.lic"
def issue_license_file(
*,
issued_to: str,
fingerprint: str,
days: int = 365,
output: Optional[str] = None,
) -> Dict[str, Any]:
if not issued_to.strip():
raise ValueError("issued_to is required")
if not fingerprint.strip():
raise ValueError("fingerprint is required")
private_key = _load_private_key()
issued_at = datetime.now(timezone.utc)
expires_at = issued_at + timedelta(days=int(days))
payload = {
"issued_to": issued_to.strip(),
"fingerprint": fingerprint.strip(),
"expires_at": expires_at.isoformat(),
"issued_at": issued_at.isoformat(),
}
payload_b64 = base64.b64encode(
json.dumps(payload, ensure_ascii=False).encode("utf-8")
)
signature = private_key.sign(payload_b64)
signature_b64 = base64.b64encode(signature)
blob = LICENSE_HEADER + b"|" + signature_b64 + b"|" + payload_b64
output_path = _normalize_output_path(payload["issued_to"], output)
output_path.write_bytes(blob)
return {
"output_path": str(output_path),
"payload": payload,
}
def verify_license_file(license_file: str | Path) -> Dict[str, Any]:
_, Ed25519PublicKey, InvalidSignature, _ = _load_cryptography()
license_path = Path(license_file)
if not license_path.exists():
raise FileNotFoundError(f"License file not found: {license_path}")
public_key_raw = base64.b64decode(read_public_key_b64(PUBLIC_KEY_FILE))
public_key = Ed25519PublicKey.from_public_bytes(public_key_raw)
try:
blob = license_path.read_bytes().strip()
header, signature_b64, payload_b64 = blob.split(b"|", 2)
if header != LICENSE_HEADER:
raise ValueError("Invalid license header")
public_key.verify(base64.b64decode(signature_b64), payload_b64)
payload = json.loads(base64.b64decode(payload_b64))
except (InvalidSignature, ValueError, json.JSONDecodeError) as exc:
return {
"ok": False,
"license_file": str(license_path),
"reason": str(exc),
}
except Exception as exc:
return {
"ok": False,
"license_file": str(license_path),
"reason": f"Failed to parse license: {exc}",
}
expires_at_text = str(payload.get("expires_at") or "")
expires_at = datetime.fromisoformat(expires_at_text)
expired = datetime.now(timezone.utc) > expires_at
return {
"ok": True,
"license_file": str(license_path),
"issued_to": payload.get("issued_to"),
"fingerprint": payload.get("fingerprint"),
"issued_at": payload.get("issued_at"),
"expires_at": expires_at_text,
"expired": expired,
}
def cmd_rotate_key(args: argparse.Namespace) -> int:
result = rotate_key_pair(force=bool(args.force))
print("=" * 60)
print("New key pair generated")
print(f"Private key: {result['private_key_path']}")
print(f"Public key : {result['public_key_path']}")
print()
print("Update backend/app/license_service.py with:")
print(f'_PUBLIC_KEY_B64 = "{result["public_key_b64"]}"')
print("=" * 60)
if getattr(args, "sync_backend", False):
sync_result = sync_backend_public_key(
public_key_b64=result["public_key_b64"],
target_path=args.target,
)
print(f"Backend public key synced: {sync_result['target_path']}")
return 0
def cmd_sync_public_key(args: argparse.Namespace) -> int:
result = sync_backend_public_key(target_path=args.target)
status = "updated" if result["changed"] else "already synced"
print(f"Backend public key {status}: {result['target_path']}")
return 0
def cmd_issue(args: argparse.Namespace) -> int:
result = issue_license_file(
issued_to=args.to,
fingerprint=args.fingerprint,
days=int(args.days),
output=args.output,
)
payload = result["payload"]
print(f"License file generated: {result['output_path']}")
print(f"Issued to : {payload['issued_to']}")
print(f"Fingerprint : {payload['fingerprint']}")
print(f"Expires at : {payload['expires_at']}")
return 0
def cmd_verify(args: argparse.Namespace) -> int:
result = verify_license_file(args.license_file)
if not result["ok"]:
print(f"[invalid] {result['reason']}")
return 1
print("[valid] Signature verified")
print(f"Issued to : {result.get('issued_to')}")
print(f"Fingerprint : {result.get('fingerprint')}")
print(f"Issued at : {result.get('issued_at')}")
print(f"Expires at : {result.get('expires_at')}")
print(f"Expired : {'yes' if result.get('expired') else 'no'}")
return 0
def cmd_fingerprint(_args: argparse.Namespace) -> int:
print(get_machine_fingerprint())
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="InSAR license issuer")
sub = parser.add_subparsers(dest="cmd")
rotate = sub.add_parser("rotate-key", help="Generate a new key pair")
rotate.add_argument("--force", action="store_true", help="Overwrite existing private/public key files")
rotate.add_argument(
"--sync-backend",
action="store_true",
help="After rotation, also update backend/app/license_service.py",
)
rotate.add_argument("--target", default=None, help="Optional backend/app/license_service.py path")
sync_public = sub.add_parser("sync-public-key", help="Sync public_key.b64 into backend/app/license_service.py")
sync_public.add_argument("--target", default=None, help="Optional target file path")
issue = sub.add_parser("issue", help="Issue a .lic file")
issue.add_argument("--to", required=True, help="Organization or customer name")
issue.add_argument("--fingerprint", required=True, help="Target machine fingerprint")
issue.add_argument("--days", default=365, help="Validity in days")
issue.add_argument("--output", default=None, help="Output .lic path")
verify = sub.add_parser("verify", help="Verify a .lic file against public_key.b64")
verify.add_argument("license_file", help="Path to .lic file")
sub.add_parser("fingerprint", help="Print the current machine fingerprint")
return parser
def main(argv: Optional[list[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
if args.cmd == "rotate-key":
return cmd_rotate_key(args)
if args.cmd == "sync-public-key":
return cmd_sync_public_key(args)
if args.cmd == "issue":
return cmd_issue(args)
if args.cmd == "verify":
return cmd_verify(args)
if args.cmd == "fingerprint":
return cmd_fingerprint(args)
parser.print_help()
return 0
except Exception as exc:
print(f"[error] {exc}")
return 1
if __name__ == "__main__":
raise SystemExit(main())
+404
View File
@@ -0,0 +1,404 @@
from __future__ import annotations
import json
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from tkinter.scrolledtext import ScrolledText
from issue_license import (
BACKEND_LICENSE_SERVICE_FILE,
PUBLIC_KEY_FILE,
get_key_status,
get_machine_fingerprint,
issue_license_file,
rotate_key_pair,
sync_backend_public_key,
verify_license_file,
)
class LicenseIssuerApp(tk.Tk):
def __init__(self) -> None:
super().__init__()
self.title("InSAR 授权签发工具")
self.geometry("920x720")
self.minsize(860, 620)
self.fingerprint_var = tk.StringVar()
self.issue_to_var = tk.StringVar()
self.issue_fingerprint_var = tk.StringVar()
self.issue_days_var = tk.StringVar(value="365")
self.issue_output_var = tk.StringVar()
self.verify_path_var = tk.StringVar()
self.backend_target_var = tk.StringVar(value=str(BACKEND_LICENSE_SERVICE_FILE))
self.status_var = tk.StringVar(value="就绪")
self.key_summary_var = tk.StringVar(value="")
self._build_ui()
self.refresh_fingerprint()
self.refresh_keys()
def _build_ui(self) -> None:
container = ttk.Frame(self, padding=12)
container.pack(fill="both", expand=True)
header = ttk.Frame(container)
header.pack(fill="x")
ttk.Label(
header,
text="InSAR 授权签发工具",
font=("Segoe UI", 16, "bold"),
).pack(anchor="w")
ttk.Label(
header,
text="基于现有 LIC2 协议,通过桌面窗体完成指纹获取、签发、验签和密钥管理。",
).pack(anchor="w", pady=(4, 10))
notebook = ttk.Notebook(container)
notebook.pack(fill="both", expand=True)
self._build_fingerprint_tab(notebook)
self._build_issue_tab(notebook)
self._build_verify_tab(notebook)
self._build_keys_tab(notebook)
status_bar = ttk.Label(
container,
textvariable=self.status_var,
relief="sunken",
anchor="w",
padding=(8, 4),
)
status_bar.pack(fill="x", pady=(10, 0))
def _build_fingerprint_tab(self, notebook: ttk.Notebook) -> None:
frame = ttk.Frame(notebook, padding=12)
notebook.add(frame, text="机器指纹")
ttk.Label(
frame,
text="获取当前机器指纹。该值可以发给签发端,用于生成绑定本机的授权文件。",
wraplength=760,
).pack(anchor="w")
row = ttk.Frame(frame)
row.pack(fill="x", pady=(16, 8))
ttk.Entry(row, textvariable=self.fingerprint_var, state="readonly").pack(
side="left",
fill="x",
expand=True,
)
button_row = ttk.Frame(frame)
button_row.pack(fill="x", pady=(4, 8))
ttk.Button(button_row, text="刷新", command=self.refresh_fingerprint).pack(side="left")
ttk.Button(button_row, text="复制", command=self.copy_fingerprint).pack(side="left", padx=(8, 0))
ttk.Button(
button_row,
text="填入签发表单",
command=self.use_current_fingerprint_for_issue,
).pack(side="left", padx=(8, 0))
self.fingerprint_details = ScrolledText(frame, height=18, wrap="word")
self.fingerprint_details.pack(fill="both", expand=True, pady=(12, 0))
self._set_text(
self.fingerprint_details,
"当前指纹由硬件标识计算得到,算法与 backend/app/license_service.py 保持一致。\n",
)
def _build_issue_tab(self, notebook: ttk.Notebook) -> None:
frame = ttk.Frame(notebook, padding=12)
notebook.add(frame, text="签发授权")
form = ttk.Frame(frame)
form.pack(fill="x")
form.columnconfigure(1, weight=1)
ttk.Label(form, text="授权对象").grid(row=0, column=0, sticky="w", pady=6)
ttk.Entry(form, textvariable=self.issue_to_var).grid(row=0, column=1, sticky="ew", pady=6)
ttk.Label(form, text="机器指纹").grid(row=1, column=0, sticky="w", pady=6)
ttk.Entry(form, textvariable=self.issue_fingerprint_var).grid(row=1, column=1, sticky="ew", pady=6)
ttk.Label(form, text="有效天数").grid(row=2, column=0, sticky="w", pady=6)
ttk.Entry(form, textvariable=self.issue_days_var, width=12).grid(row=2, column=1, sticky="w", pady=6)
ttk.Label(form, text="输出文件").grid(row=3, column=0, sticky="w", pady=6)
output_row = ttk.Frame(form)
output_row.grid(row=3, column=1, sticky="ew", pady=6)
output_row.columnconfigure(0, weight=1)
ttk.Entry(output_row, textvariable=self.issue_output_var).grid(row=0, column=0, sticky="ew")
ttk.Button(output_row, text="浏览", command=self.browse_issue_output).grid(row=0, column=1, padx=(8, 0))
button_row = ttk.Frame(frame)
button_row.pack(fill="x", pady=(10, 8))
ttk.Button(button_row, text="使用本机指纹", command=self.use_current_fingerprint_for_issue).pack(side="left")
ttk.Button(button_row, text="生成授权文件", command=self.issue_license_action).pack(side="left", padx=(8, 0))
self.issue_output_box = ScrolledText(frame, height=20, wrap="word")
self.issue_output_box.pack(fill="both", expand=True, pady=(8, 0))
self._set_text(
self.issue_output_box,
"填写表单后,点击“生成授权文件”。\n",
)
def _build_verify_tab(self, notebook: ttk.Notebook) -> None:
frame = ttk.Frame(notebook, padding=12)
notebook.add(frame, text="验证授权")
ttk.Label(
frame,
text="使用当前目录中的 public_key.b64 验证已有的 .lic 授权文件。",
wraplength=760,
).pack(anchor="w")
row = ttk.Frame(frame)
row.pack(fill="x", pady=(14, 8))
row.columnconfigure(0, weight=1)
ttk.Entry(row, textvariable=self.verify_path_var).grid(row=0, column=0, sticky="ew")
ttk.Button(row, text="浏览", command=self.browse_verify_file).grid(row=0, column=1, padx=(8, 0))
ttk.Button(row, text="开始验证", command=self.verify_license_action).grid(row=0, column=2, padx=(8, 0))
self.verify_output_box = ScrolledText(frame, height=24, wrap="word")
self.verify_output_box.pack(fill="both", expand=True, pady=(8, 0))
self._set_text(
self.verify_output_box,
"选择一个授权文件,然后点击“开始验证”。\n",
)
def _build_keys_tab(self, notebook: ttk.Notebook) -> None:
frame = ttk.Frame(notebook, padding=12)
notebook.add(frame, text="密钥管理")
ttk.Label(
frame,
text="管理签发密钥,并可选地将 public_key.b64 同步到 backend/app/license_service.py。",
wraplength=760,
).pack(anchor="w")
target_row = ttk.Frame(frame)
target_row.pack(fill="x", pady=(14, 8))
target_row.columnconfigure(1, weight=1)
ttk.Label(target_row, text="后端目标文件").grid(row=0, column=0, sticky="w")
ttk.Entry(target_row, textvariable=self.backend_target_var).grid(row=0, column=1, sticky="ew", padx=(8, 0))
ttk.Button(target_row, text="浏览", command=self.browse_backend_target).grid(row=0, column=2, padx=(8, 0))
ttk.Label(frame, textvariable=self.key_summary_var, wraplength=780).pack(anchor="w", pady=(6, 8))
button_row = ttk.Frame(frame)
button_row.pack(fill="x", pady=(0, 8))
ttk.Button(button_row, text="刷新状态", command=self.refresh_keys).pack(side="left")
ttk.Button(button_row, text="轮换密钥", command=self.rotate_key_action).pack(side="left", padx=(8, 0))
ttk.Button(button_row, text="强制轮换", command=lambda: self.rotate_key_action(force=True)).pack(side="left", padx=(8, 0))
ttk.Button(button_row, text="同步公钥到后端", command=self.sync_backend_action).pack(side="left", padx=(8, 0))
self.key_output_box = ScrolledText(frame, height=24, wrap="word")
self.key_output_box.pack(fill="both", expand=True, pady=(8, 0))
self._set_text(
self.key_output_box,
f"后端文件:{BACKEND_LICENSE_SERVICE_FILE}\n公钥文件:{PUBLIC_KEY_FILE}\n",
)
def _set_status(self, message: str) -> None:
self.status_var.set(message)
def _set_text(self, widget: ScrolledText, text: str) -> None:
widget.configure(state="normal")
widget.delete("1.0", "end")
widget.insert("1.0", text)
widget.configure(state="disabled")
def _copy_text(self, text: str) -> None:
self.clipboard_clear()
self.clipboard_append(text)
self.update()
def refresh_fingerprint(self) -> None:
fingerprint = get_machine_fingerprint()
self.fingerprint_var.set(fingerprint)
self._set_text(
self.fingerprint_details,
"当前机器指纹:\n\n"
f"{fingerprint}\n\n"
"可以点击“复制”发给签发端,也可以点击“填入签发表单”做本机测试。\n",
)
self._set_status("机器指纹已刷新")
def copy_fingerprint(self) -> None:
if not self.fingerprint_var.get().strip():
self.refresh_fingerprint()
self._copy_text(self.fingerprint_var.get().strip())
self._set_status("机器指纹已复制")
def use_current_fingerprint_for_issue(self) -> None:
if not self.fingerprint_var.get().strip():
self.refresh_fingerprint()
self.issue_fingerprint_var.set(self.fingerprint_var.get().strip())
self._set_status("签发表单已填入当前指纹")
def browse_issue_output(self) -> None:
path = filedialog.asksaveasfilename(
title="选择输出 .lic 文件",
defaultextension=".lic",
filetypes=[("授权文件", "*.lic"), ("所有文件", "*.*")],
initialfile="license.lic",
)
if path:
self.issue_output_var.set(path)
def browse_verify_file(self) -> None:
path = filedialog.askopenfilename(
title="选择 .lic 文件",
filetypes=[("授权文件", "*.lic"), ("所有文件", "*.*")],
)
if path:
self.verify_path_var.set(path)
def browse_backend_target(self) -> None:
path = filedialog.askopenfilename(
title="选择 backend/app/license_service.py",
filetypes=[("Python 文件", "*.py"), ("所有文件", "*.*")],
initialfile="license_service.py",
)
if path:
self.backend_target_var.set(path)
def issue_license_action(self) -> None:
try:
days = int(self.issue_days_var.get().strip() or "365")
result = issue_license_file(
issued_to=self.issue_to_var.get().strip(),
fingerprint=self.issue_fingerprint_var.get().strip(),
days=days,
output=self.issue_output_var.get().strip() or None,
)
except Exception as exc:
messagebox.showerror("签发授权", str(exc))
self._set_status("生成授权失败")
return
payload = result["payload"]
output_path = result["output_path"]
self.issue_output_var.set(output_path)
text = (
"授权文件生成成功。\n\n"
f"输出文件:{output_path}\n"
f"授权对象:{payload['issued_to']}\n"
f"机器指纹:{payload['fingerprint']}\n"
f"签发时间:{payload['issued_at']}\n"
f"到期时间:{payload['expires_at']}\n"
)
self._set_text(self.issue_output_box, text)
self._set_status("授权文件已生成")
messagebox.showinfo("签发授权", f"授权文件已生成:\n{output_path}")
def verify_license_action(self) -> None:
path = self.verify_path_var.get().strip()
if not path:
messagebox.showwarning("验证授权", "请先选择一个 .lic 授权文件。")
return
try:
result = verify_license_file(path)
except Exception as exc:
messagebox.showerror("验证授权", str(exc))
self._set_status("授权验证失败")
return
if not result["ok"]:
self._set_text(
self.verify_output_box,
json.dumps(result, ensure_ascii=False, indent=2),
)
self._set_status("授权文件无效")
messagebox.showerror("验证授权", result["reason"])
return
text = (
"授权验证通过。\n\n"
f"授权文件:{result['license_file']}\n"
f"授权对象:{result.get('issued_to')}\n"
f"机器指纹:{result.get('fingerprint')}\n"
f"签发时间:{result.get('issued_at')}\n"
f"到期时间:{result.get('expires_at')}\n"
f"是否过期:{'' if result.get('expired') else ''}\n"
)
self._set_text(self.verify_output_box, text)
self._set_status("授权验证通过")
messagebox.showinfo("验证授权", "授权验证通过。")
def refresh_keys(self) -> None:
status = get_key_status(self.backend_target_var.get().strip() or None)
lines = [
f"私钥存在:{'' if status['private_key_exists'] else ''}",
f"公钥存在:{'' if status['public_key_exists'] else ''}",
f"公钥已同步:{'' if status['backend_synced'] else ''}",
f"私钥路径:{status['private_key_path']}",
f"公钥路径:{status['public_key_path']}",
f"后端文件:{status['backend_license_service_path']}",
]
self.key_summary_var.set("\n".join(lines))
key_details = {
"public_key_b64": status.get("public_key_b64"),
"backend_public_key_b64": status.get("backend_public_key_b64"),
"backend_synced": status.get("backend_synced"),
}
self._set_text(self.key_output_box, json.dumps(key_details, ensure_ascii=False, indent=2))
self._set_status("密钥状态已刷新")
def rotate_key_action(self, force: bool = False) -> None:
if force:
confirmed = messagebox.askyesno(
"强制轮换密钥",
"这会覆盖现有密钥对,并使旧授权文件失效。确认继续吗?",
)
if not confirmed:
return
try:
result = rotate_key_pair(force=force)
except Exception as exc:
messagebox.showerror("轮换密钥", str(exc))
self._set_status("密钥轮换失败")
return
self.refresh_keys()
self._copy_text(result["public_key_b64"])
self._set_status("密钥轮换完成,公钥已复制")
sync_now = messagebox.askyesno(
"轮换密钥",
"新密钥对已生成,公钥也已复制到剪贴板。\n\n现在要同步到 backend/app/license_service.py 吗?",
)
if sync_now:
self.sync_backend_action()
def sync_backend_action(self) -> None:
try:
result = sync_backend_public_key(
target_path=self.backend_target_var.get().strip() or None,
)
except Exception as exc:
messagebox.showerror("同步公钥", str(exc))
self._set_status("后端公钥同步失败")
return
self.refresh_keys()
changed_text = "已更新" if result["changed"] else "已是最新"
self._set_status(f"后端公钥{changed_text}")
messagebox.showinfo(
"同步公钥",
f"后端公钥{changed_text}\n{result['target_path']}",
)
def main() -> None:
app = LicenseIssuerApp()
app.mainloop()
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
nlrJTGv+6iPE5J3X6KTXb23Jw5MWdFO9meUJVNp/cRc=
+17
View File
@@ -0,0 +1,17 @@
@echo off
setlocal
cd /d "%~dp0"
where pyw >nul 2>nul
if %errorlevel%==0 (
start "" pyw -3 "%~dp0license_issuer_gui.pyw"
exit /b 0
)
where pythonw >nul 2>nul
if %errorlevel%==0 (
start "" pythonw "%~dp0license_issuer_gui.pyw"
exit /b 0
)
python "%~dp0license_issuer_gui.pyw"