chore: initialize insar management system v2
This commit is contained in:
@@ -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`.
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
nlrJTGv+6iPE5J3X6KTXb23Jw5MWdFO9meUJVNp/cRc=
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user