#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Snipaste 2.11.3 离线授权码生成器(一键激活版)
用法:
    python snipaste_offline_keygen.py                 # 一键激活
    python snipaste_offline_keygen.py --no-patch      # 只生成授权码,不动客户端
依赖: pip install pynacl  (或 pip install cryptography)
"""

import argparse
import base64
import hashlib
import json
import os
import sys
import zlib
from datetime import datetime, timezone, timedelta

try:
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

# 自签密钥对(配合下方补丁使用,补丁后任意合法授权码都能通过验证)
PRIVATE_KEY_HEX = "951743f1381818ec1af9a32a2eafce42c6261f5089468ef863e7b903c183b3f6"
PUBLIC_KEY_HEX  = "1ebf8f1197d33a99aee6c625e306739eb9bf1c2806324eb7b83a09933062aa04"

# 补丁位置与字节:把"取验签结果"改成"恒为 1",0x24AC80 即 RVA 0x14024B880
PATCH_OFFSET = 0x24AC80
PATCH_ORIG   = bytes.fromhex("0F B6 D8")
PATCH_NEW    = bytes.fromhex("B3 01 90")


def calc_checksum(payload: str) -> str:
    """校验字符:相邻字符 ASCII 差绝对值之和 % 长度,取对应位置字符"""
    total = sum(abs(ord(payload[i]) - ord(payload[i - 1])) for i in range(1, len(payload)))
    return payload[total % len(payload)]


def xor_bytes(key: bytes, data: bytes) -> bytes:
    """循环密钥异或"""
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))


def gzip_compress(data: bytes) -> bytes:
    """gzip 压缩(wbits=31,与客户端一致)"""
    co = zlib.compressobj(9, zlib.DEFLATED, 31)
    return co.compress(data) + co.flush()


def gzip_decompress(data: bytes) -> bytes:
    return zlib.decompress(data, 31)


def format_qt_time(dt: datetime) -> str:
    """Qt 时间格式:yyyy-MM-ddTHH:mm:ss.zzz"""
    return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}"


def get_machine_guid() -> str | None:
    """机器唯一标识:注册表 MachineGuid"""
    try:
        import winreg
        with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
                            r"SOFTWARE\Microsoft\Cryptography") as key:
            return winreg.QueryValueEx(key, "MachineGuid")[0]
    except Exception:
        return None


def get_workgroup() -> str | None:
    """工作组:NetWkstaGetInfo(level=100),别用 WMI"""
    try:
        import ctypes
        from ctypes import wintypes

        class WKSTA_INFO_100(ctypes.Structure):
            _fields_ = [
                ("wki100_platform_id", wintypes.DWORD),
                ("wki100_computername", wintypes.LPWSTR),
                ("wki100_langroup", wintypes.LPWSTR),
                ("wki100_ver_major", wintypes.DWORD),
                ("wki100_ver_minor", wintypes.DWORD),
            ]

        netapi32 = ctypes.windll.netapi32
        buf = ctypes.c_void_p()
        if netapi32.NetWkstaGetInfo(None, 100, ctypes.byref(buf)) != 0:
            return None
        info = ctypes.cast(buf, ctypes.POINTER(WKSTA_INFO_100)).contents
        wg = info.wki100_langroup
        netapi32.NetApiBufferFree(buf)
        return wg
    except Exception:
        return None


def find_snipaste_exe(hint: str | None) -> str | None:
    """定位 Snipaste.exe:显式路径 > 脚本同目录 > 上级目录 > 当前目录"""
    if hint:
        return os.path.abspath(hint) if os.path.isfile(hint) else None
    if getattr(sys, "frozen", False):
        here = os.path.dirname(os.path.abspath(sys.executable))
    else:
        here = os.path.dirname(os.path.abspath(__file__))
    for cand in (
        os.path.join(here, "Snipaste.exe"),
        os.path.join(here, "..", "Snipaste.exe"),
        os.path.join(os.getcwd(), "Snipaste.exe"),
    ):
        if os.path.isfile(cand):
            return os.path.abspath(cand)
    return None


def kill_snipaste() -> bool:
    """结束运行中的 Snipaste.exe(改 exe 前必须)"""
    import subprocess
    _nw = getattr(subprocess, "CREATE_NO_WINDOW", 0)
    try:
        out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq Snipaste.exe"],
                             capture_output=True, text=True, creationflags=_nw).stdout
    except Exception:
        return False
    if "Snipaste.exe" not in out:
        return False
    subprocess.run(["taskkill", "/F", "/IM", "Snipaste.exe"],
                   capture_output=True, text=True, creationflags=_nw)
    return True


def patch_snipaste_exe(exe_path: str) -> str:
    """打补丁:0x24AC80 处 0F B6 D8 → B3 01 90,原文件先备份为 .bak"""
    with open(exe_path, "rb") as f:
        data = bytearray(f.read())
    cur = bytes(data[PATCH_OFFSET:PATCH_OFFSET + 3])
    if cur == PATCH_NEW:
        return "already_patched"
    if cur != PATCH_ORIG:
        return f"unexpected({cur.hex(' ')})"
    bak = exe_path + ".bak"
    if not os.path.exists(bak):
        with open(bak, "wb") as f:
            f.write(data)
    data[PATCH_OFFSET:PATCH_OFFSET + 3] = PATCH_NEW
    with open(exe_path, "wb") as f:
        f.write(data)
    return "patched"


def _fmt_sep(s: str, first: int, step: int) -> str:
    """按 first/step 步长插入分隔符"""
    out, pos, st = [], 0, first
    while pos < len(s):
        if pos:
            out.append('-')
        out.append(s[pos:pos + st])
        pos += st
        st = step
    return ''.join(out)


def gen_device_id(machine_guid: str, length: int = 10) -> str:
    """生成验证码(客户端真正比较的值,不是界面显示码):
    blake2s('Snipaste 2','1',MachineGuid)[-length:] → hex → 4-8 分段 → +校验 → +'1'"""
    h = hashlib.blake2s(digest_size=16)
    h.update(b'Snipaste 2')
    h.update(b'1')
    h.update(machine_guid.encode('utf-8'))
    s = h.digest()[-length:].hex().upper()
    s = _fmt_sep(s, 4, 8)
    s += calc_checksum(s.replace('-', ''))
    s += '1'
    return s


def make_license_json(name: str, email: str, domain: str, machine_id: str,
                      iat: datetime | None = None) -> bytes:
    """构造授权 JSON。客户端只检查:eml 非空、platform==1、dom 匹配工作组、
    machineid 匹配本机验证码、iat 不超 now+20min、exp 未过期。
    ref 必须设远未来(设为 exp),否则会触发联网刷新导致离线掉授权。"""
    now = datetime.now(timezone.utc)
    if iat is None:
        iat = now - timedelta(minutes=5)
    exp = now + timedelta(days=365 * 10)
    data = {
        "nam": name,
        "eml": email,
        "lic": "",
        "dev": 1,
        "pln": "Personal",
        "dom": domain,
        "api": 0,
        "iat": format_qt_time(iat),
        "exp": format_qt_time(exp),
        "exx": format_qt_time(exp),
        "ref": format_qt_time(exp),
        "hwi": {"machineid": machine_id, "platform": 1, "domain": ""}
    }
    return json.dumps(data, separators=(',', ':')).encode('utf-8')


def ed25519_sign(private_key_hex: str, message: bytes) -> bytes:
    try:
        from nacl.signing import SigningKey
        return SigningKey(bytes.fromhex(private_key_hex)).sign(message).signature
    except ImportError:
        pass
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
        return Ed25519PrivateKey.from_private_bytes(
            bytes.fromhex(private_key_hex)).sign(message)
    except ImportError:
        raise ImportError("缺少 Ed25519 库:pip install pynacl 或 cryptography")


def ed25519_verify(public_key_hex: str, message: bytes, sig: bytes) -> bool:
    try:
        from nacl.signing import VerifyKey
        VerifyKey(bytes.fromhex(public_key_hex)).verify(message, sig)
        return True
    except ImportError:
        pass
    except Exception:
        return False
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
        Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex)).verify(sig, message)
        return True
    except ImportError:
        raise ImportError("缺少 Ed25519 库:pip install pynacl 或 cryptography")
    except Exception:
        return False


def generate_license_key(private_key_hex: str, json_bytes: bytes) -> str:
    """生成授权码:body = gzip(JSON) ^ sig,整体 base64 后加校验字符。
    客户端解码时对 body 做 3 次 XOR,后两次抵消,最终验签的是明文 JSON。"""
    sig = ed25519_sign(private_key_hex, json_bytes)
    body = xor_bytes(sig, gzip_compress(json_bytes))
    b64 = base64.standard_b64encode(sig + body).decode('ascii')
    return f'0{calc_checksum(b64)}-{b64}'


def verify_license_key(code: str, json_bytes: bytes) -> bool:
    """生成后自验:格式、校验字符、验签、时间"""
    if len(code) <= 3 or code[2] != '-':
        return False
    if code[1] != calc_checksum(code[3:]):
        return False
    data = base64.standard_b64decode(code[3:])
    sig, body = data[:64], data[64:]
    msg = xor_bytes(sig, xor_bytes(sig, gzip_decompress(xor_bytes(sig, body))))
    if not ed25519_verify(PUBLIC_KEY_HEX, msg, sig):
        return False
    info = json.loads(msg)
    now = datetime.now(timezone.utc)
    fmt = "%Y-%m-%dT%H:%M:%S.%f"
    iat = datetime.strptime(info["iat"], fmt).replace(tzinfo=timezone.utc)
    exp = datetime.strptime(info["exp"], fmt).replace(tzinfo=timezone.utc)
    return iat <= now + timedelta(minutes=20) and exp >= now


def main():
    parser = argparse.ArgumentParser(
        description="Snipaste 2.11.3 离线授权码生成器")
    parser.add_argument("--exe", metavar="PATH",
                        help="Snipaste.exe 路径(默认自动定位:脚本同目录/上级/当前目录)")
    parser.add_argument("--no-patch", action="store_true",
                        help="只生成授权码,不打补丁不动客户端")
    parser.add_argument("--machine-id", metavar="TEXT",
                        help="指定验证码(默认自动生成)")
    parser.add_argument("--dom", metavar="TEXT",
                        help="指定工作组(默认自动获取;传空串跳过工作组匹配)")
    parser.add_argument("--iat-offset", type=float, default=-5.0,
                        help="签发时间偏移分钟(默认 -5;状态码 8 时改 +1)")
    parser.add_argument("--name", "-n", default="小野博客",
                        help="授权用户名")
    parser.add_argument("--email", default="vb77@qq.com",
                        help="授权邮箱(须非空)")
    parser.add_argument("--out", default="snipaste_license.txt",
                        help="输出文件")
    args = parser.parse_args()

    print("=" * 60)
    print("Snipaste 2.11.3 离线授权码生成器")
    print("=" * 60)
    print("本软件需要放置到截图软件(即 Snipaste)在同一个目录才可以。")
    print()

    # 定位 exe 并打补丁
    exe_path = find_snipaste_exe(args.exe)
    if args.no_patch:
        print("[*] --no-patch:跳过打补丁,请自行确保 Snipaste.exe 已打补丁")
    elif exe_path is None:
        print("[!] 未找到 Snipaste.exe(可用 --exe 指定路径)")
        print("    未打补丁的 exe 无法激活!")
    else:
        print(f"[*] Snipaste.exe: {exe_path}")
        if kill_snipaste():
            print("[*] 已结束运行中的 Snipaste.exe")
        status = patch_snipaste_exe(exe_path)
        if status == "already_patched":
            print("[*] 补丁已存在")
        elif status == "patched":
            print("[*] 补丁完成(原版已备份为 .bak)")
        else:
            print(f"[!] 补丁失败:{status},请确认是 Snipaste 2.11.3 x64 版本")
    print()

    # 本机信息
    machine_guid = get_machine_guid()
    if not machine_guid:
        machine_guid = input("无法读取 MachineGuid,请手动输入: ").strip()
    workgroup = args.dom if args.dom is not None else get_workgroup()
    if not workgroup:
        workgroup = input("无法读取工作组,请手动输入(可留空): ").strip()
    machine_id = args.machine_id or gen_device_id(machine_guid)

    # 生成授权码
    json_bytes = make_license_json(
        name=args.name or "小野博客",
        email=args.email,
        domain=workgroup,
        machine_id=machine_id,
        iat=datetime.now(timezone.utc) + timedelta(minutes=args.iat_offset),
    )
    code = generate_license_key(PRIVATE_KEY_HEX, json_bytes)

    print(f"[*] License Key:")
    print(f"    {code}")
    print()

    # 保存结果
    out_path = args.out
    if not os.path.isabs(out_path):
        out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), out_path)
    try:
        with open(out_path, "w", encoding="utf-8") as f:
            f.write("=" * 50 + "\n")
            f.write("  Snipaste 2.11.3 离线授权码\n")
            f.write("=" * 50 + "\n")
            f.write(f"MachineGuid : {machine_guid}\n")
            f.write(f"Workgroup   : {workgroup}\n")
            f.write(f"MachineID   : {machine_id}\n")
            f.write(f"LicenseKey  : {code}\n")
            f.write("=" * 50 + "\n")
        print(f"[*] 已保存到: {out_path}")
    except OSError as e:
        print(f"[!] 保存失败: {e}")

    # 自验
    ok = verify_license_key(code, json_bytes)
    print(f"[*] 自验: {'通过' if ok else '失败'}")
    print()

    print("[*] 完成!下一步:启动 Snipaste → 偏好设置 → 授权 → 粘贴上方 License Key")
    print("    自动重启后标题变 PRO 即激活成功。")
    return 0


if __name__ == "__main__":
    sys.exit(main())
