-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlicensing.py
More file actions
256 lines (205 loc) · 9.1 KB
/
Copy pathlicensing.py
File metadata and controls
256 lines (205 loc) · 9.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""
licensing.py — Offline license system for PersonalCleaner Commercial (Pro).
Two key types (generated by the vendor with keygen.py):
* DEVICE key PC-<MACHINEID>-<YYYYMMDD>-<SIG> -> locked to ONE machine.
* SITE key SITE-<MAX>-<YYYYMMDD>-<SIG> -> one key per company, installed
on many PCs (seat count is
reported but honour-based
offline; see note below).
Every key is HMAC-SHA256 signed with SECRET, so it can't be forged without it.
HONEST LIMITATION: the secret ships inside the Commercial exe, so a determined
attacker can extract it and forge keys. This is a trust / anti-casual-sharing
measure, NOT bulletproof DRM. For hard per-seat enforcement, upgrade to online
activation (Keygen.sh / Lemon Squeezy) later.
The FREE (MIT) build does NOT bundle this module -> always runs unlicensed with
no restriction. The Commercial build bundles it and enforces a valid key.
"""
import hashlib
import hmac
import json
import os
import sys
from datetime import date
SUPPORT_URL = "https://observerly1.gumroad.com/l/ialzp"
# Change this before serious use, or set env var PC_LICENSE_SECRET.
SECRET = os.environ.get("PC_LICENSE_SECRET", "PersonalCleaner-2026-change-me")
# --------------------------------------------------------------------------- #
# Storage
# --------------------------------------------------------------------------- #
def get_data_dir() -> str:
"""Same fallback logic as the app: beside the exe, else %LOCALAPPDATA%."""
candidates = []
if getattr(sys, "frozen", False):
candidates.append(os.path.dirname(sys.executable))
local = os.environ.get("LOCALAPPDATA")
if local:
candidates.append(os.path.join(local, "PersonalCleaner"))
candidates.append(os.path.join(os.path.expanduser("~"), "PersonalCleaner"))
candidates.append(os.getcwd())
for d in candidates:
try:
os.makedirs(d, exist_ok=True)
probe = os.path.join(d, ".pc_write_test")
with open(probe, "w", encoding="utf-8") as fh:
fh.write("ok")
os.remove(probe)
return d
except OSError:
continue
return os.getcwd()
def license_file() -> str:
return os.path.join(get_data_dir(), "license.key")
def activated_file() -> str:
return os.path.join(get_data_dir(), "activated.json")
# --------------------------------------------------------------------------- #
# Machine fingerprint (unique per Windows install / machine)
# --------------------------------------------------------------------------- #
def _machine_guid() -> str:
try:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Cryptography") as k:
val, _ = winreg.QueryValueEx(k, "MachineGuid")
return str(val).strip().lower()
except Exception: # noqa: BLE001
return ""
def _volume_serial() -> str:
try:
import ctypes
from ctypes import wintypes
buf = ctypes.create_unicode_buffer(261)
fs = ctypes.create_unicode_buffer(261)
sn = wintypes.DWORD(0)
ok = ctypes.windll.kernel32.GetVolumeInformationW(
"C:\\", buf, 261, ctypes.byref(sn), None, None, fs, 261)
if ok:
return f"{sn.value:08X}"
except Exception: # noqa: BLE001
pass
return ""
def machine_id() -> str:
"""16-hex-char stable ID for this machine (machine GUID + C: serial)."""
raw = (f"{_machine_guid()}|{_volume_serial()}").strip("|")
if not raw:
raw = os.environ.get("COMPUTERNAME", "UNKNOWN")
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16].upper()
# --------------------------------------------------------------------------- #
# Signing (vendor side only — keygen.py)
# --------------------------------------------------------------------------- #
def _sign(payload: str) -> str:
return hmac.new(SECRET.encode("utf-8"), payload.encode("utf-8"),
hashlib.sha256).hexdigest()[:10].upper()
def sign_device(mid: str, expiry_iso: str) -> str:
payload = f"PC|{mid}|{expiry_iso}"
return f"PC.{mid}.{expiry_iso}.{_sign(payload)}"
def sign_site(max_machines: int, expiry_iso: str) -> str:
payload = f"SITE|{int(max_machines)}|{expiry_iso}"
return f"SITE.{int(max_machines)}.{expiry_iso}.{_sign(payload)}"
# --------------------------------------------------------------------------- #
# Validation (app side)
# --------------------------------------------------------------------------- #
def _today_iso() -> str:
return date.today().isoformat()
def _status(licensed, mode, mid, message, expiry=None,
max_machines=None, activated=None):
return {
"licensed": licensed,
"mode": mode, # no_key|invalid|bad_signature|expired|
# wrong_machine|device|site|too_many_machines
"machine_id": mid,
"message": message,
"expiry": expiry,
"max_machines": max_machines,
"activated": activated,
}
def _parse(key):
"""Return (kind, a, expiry, sig) or None if malformed."""
if not key:
return None
parts = key.strip().split(".")
if len(parts) != 4:
return None
kind, a, expiry, sig = parts
if kind not in ("PC", "SITE"):
return None
try:
date.fromisoformat(expiry)
except ValueError:
return None
return kind, a, expiry, sig
def _valid_signature(kind, a, expiry, sig) -> bool:
payload = f"{kind}|{a}|{expiry}"
return hmac.compare_digest(_sign(payload), sig)
def _load_activations() -> list:
try:
with open(activated_file(), "r", encoding="utf-8") as fh:
data = json.load(fh)
return list(data) if isinstance(data, list) else []
except (OSError, ValueError):
return []
def _save_activations(items: list) -> None:
try:
with open(activated_file(), "w", encoding="utf-8") as fh:
json.dump(items, fh, indent=2)
except OSError:
pass
def validate_key(key: str) -> dict:
"""Validate a raw key string against THIS machine. Returns a status dict."""
mid = machine_id()
parsed = _parse(key)
if parsed is None:
return _status(False, "invalid", mid, "Key format is not valid.")
kind, a, expiry, sig = parsed
if not _valid_signature(kind, a, expiry, sig):
return _status(False, "bad_signature", mid,
"Key signature is invalid (not issued by us).")
if expiry < _today_iso():
return _status(False, "expired", mid,
f"License expired on {expiry}. Renew to restore full features.")
if kind == "PC":
if a.upper() != mid:
return _status(False, "wrong_machine", mid,
"This key is locked to a different machine.")
return _status(True, "device", mid,
f"Licensed (this PC) until {expiry}.", expiry=expiry)
# SITE key — honour-based seat tracking (see module docstring).
try:
max_machines = int(a)
except ValueError:
return _status(False, "invalid", mid, "Key format is not valid.")
acts = _load_activations()
if mid in acts:
return _status(True, "site", mid,
f"Site license valid until {expiry}. This PC is activated "
f"({len(acts)} of {max_machines}).",
expiry=expiry, max_machines=max_machines, activated=len(acts))
if len(acts) < max_machines:
acts.append(mid)
_save_activations(acts)
return _status(True, "site", mid,
f"Site license valid until {expiry}. This PC is now "
f"activated ({len(acts)} of {max_machines}).",
expiry=expiry, max_machines=max_machines, activated=len(acts))
return _status(False, "too_many_machines", mid,
f"Site license allows {max_machines} PCs and this one is not "
"in the activation list.")
def load_status() -> dict:
"""Read the installed license (license.key) and validate it."""
try:
with open(license_file(), "r", encoding="utf-8") as fh:
key = fh.read().strip()
except OSError:
return _status(False, "no_key", machine_id(), "No license key installed.")
return validate_key(key)
def install_key(key: str) -> dict:
"""Validate, and if OK, save the key to license.key. Returns status dict."""
st = validate_key(key)
if st["licensed"]:
try:
with open(license_file(), "w", encoding="utf-8") as fh:
fh.write(key.strip())
except OSError:
st["message"] = "Key is valid, but the license file could not be saved."
return st
def is_licensed() -> bool:
return load_status()["licensed"]