""" LeTissier licence verification — Python. Used by Yewee (facetrack). Requires `cryptography` (already a common dependency). Copy this module into the app and set PUBLIC_KEY_HEX to your own key. """ from __future__ import annotations import base64 import hashlib import json import time import urllib.error import urllib.request from dataclasses import dataclass from typing import Any, Optional from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey # The studio's licence signing key. Public: safe to ship in a binary. PUBLIC_KEY_HEX = "REPLACE_WITH_YOUR_PUBLIC_KEY_HEX" ACTIVE = "active" UPDATE_REQUIRED = "update_required" CHECK_IN_REQUIRED = "check_in_required" EXPIRED = "expired" WRONG_MACHINE = "wrong_machine" INVALID = "invalid" @dataclass class Verdict: status: str claims: Optional[dict[str, Any]] = None check_in_in: Optional[int] = None def _b64url_decode(value: str) -> bytes: padding = "=" * (-len(value) % 4) return base64.urlsafe_b64decode(value + padding) def machine_hash(fingerprint: str) -> str: """Must match the server exactly: sha256 of the trimmed fingerprint, first 32 hex chars.""" return hashlib.sha256(fingerprint.strip().encode("utf-8")).hexdigest()[:32] def verify(token: str, public_key_hex: str = PUBLIC_KEY_HEX) -> Optional[dict[str, Any]]: """Verify the signature and parse the claims. No clock or machine checks.""" parts = token.split(".") if len(parts) != 2: return None try: key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex)) key.verify(_b64url_decode(parts[1]), parts[0].encode("ascii")) claims = json.loads(_b64url_decode(parts[0])) except (InvalidSignature, ValueError, json.JSONDecodeError): return None return claims if claims.get("v") == 1 else None def check( token: str, fingerprint: str, build_date: int, now: Optional[int] = None, public_key_hex: str = PUBLIC_KEY_HEX, ) -> Verdict: """ The whole decision, offline. build_date is when THIS build was released (unix seconds), baked in at build time. An older build stays entitled forever; a newer one asks for a renewal. That is what makes "a year of updates, yours to keep" work with no server involved. """ claims = verify(token, public_key_hex) if claims is None: return Verdict(INVALID) if claims.get("machine") != machine_hash(fingerprint): return Verdict(WRONG_MACHINE, claims) moment = int(time.time()) if now is None else now check_in_in = int(claims["exp"]) - moment if check_in_in <= 0: # A trial's lease is its lifetime, so a lapsed trial is simply over. status = EXPIRED if claims.get("edition") == "trial" else CHECK_IN_REQUIRED return Verdict(status, claims, check_in_in) if build_date > int(claims["maintUntil"]): return Verdict(UPDATE_REQUIRED, claims, check_in_in) return Verdict(ACTIVE, claims, check_in_in) class LicenceClient: """Thin activation client for the hosted licence service.""" def __init__(self, fingerprint: str, base_url: str = "https://letissier.ie") -> None: self.fingerprint = fingerprint self.base_url = base_url.rstrip("/") def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: request = urllib.request.Request( f"{self.base_url}{path}", data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(request, timeout=15) as response: return json.loads(response.read()) except urllib.error.HTTPError as error: body = json.loads(error.read() or b"{}") raise RuntimeError(body.get("message", f"Request failed ({error.code})")) from error def activate(self, key: str, label: Optional[str] = None) -> dict[str, Any]: return self._post("/api/licence/activate", {"key": key, "machine": self.fingerprint, "label": label}) def heartbeat(self, key: str) -> dict[str, Any]: return self._post("/api/licence/heartbeat", {"key": key, "machine": self.fingerprint}) def deactivate(self, key: str) -> dict[str, Any]: return self._post("/api/licence/deactivate", {"key": key, "machine": self.fingerprint}) def start_trial(self, product: str, email: str, name: Optional[str] = None) -> dict[str, Any]: return self._post( "/api/licence/trial", {"product": product, "email": email, "name": name, "machine": self.fingerprint}, )