Spaces:
Build error
Build error
| """ | |
| upif.core.licensing | |
| ~~~~~~~~~~~~~~~~~~~ | |
| Manages Commercial Licensing and Feature Unlocking. | |
| Integrates with Gumroad API for key verification and uses local AES encryption | |
| to cache license state offline. | |
| :copyright: (c) 2025 Yash Dhone. | |
| :license: Proprietary, see LICENSE for details. | |
| """ | |
| import os | |
| import json | |
| import logging | |
| import requests | |
| import base64 | |
| from typing import Optional, Dict | |
| from cryptography.fernet import Fernet | |
| from cryptography.hazmat.primitives import hashes | |
| from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
| logger = logging.getLogger("upif.licensing") | |
| class LicenseManager: | |
| """ | |
| License Manager. | |
| Security Note: | |
| In a real-world scenario, the '_INTERNAL_KEY' and salt should be: | |
| 1. Obfuscated via Cython compilation (which we do in build.py). | |
| 2. Ideally fetched from a separate secure enclave or injected at build time. | |
| """ | |
| # Placeholder: Developer should replace this | |
| PRODUCT_PERMALINK = "xokvjp" | |
| # Obfuscated internal key material (Static for Demo) | |
| _INTERNAL_SALT = b'upif_secure_salt_2025' | |
| _INTERNAL_KEY = b'static_key_for_demo_purposes_only' | |
| def __init__(self, storage_path: str = ".upif_license.enc"): | |
| self.storage_path = storage_path | |
| self._cipher = self._get_cipher() | |
| self.license_data: Optional[Dict] = None | |
| def _get_cipher(self) -> Fernet: | |
| """Derives a strong AES-256 key from the internal secret using PBKDF2.""" | |
| kdf = PBKDF2HMAC( | |
| algorithm=hashes.SHA256(), | |
| length=32, | |
| salt=self._INTERNAL_SALT, | |
| iterations=100000, | |
| ) | |
| key = base64.urlsafe_b64encode(kdf.derive(self._INTERNAL_KEY)) | |
| return Fernet(key) | |
| def activate(self, license_key: str, product_permalink: Optional[str] = None) -> bool: | |
| """ | |
| Activates the license online via Gumroad API. | |
| Args: | |
| license_key (str): The user's key (e.g. from email). | |
| product_permalink (str, optional): Override product ID. | |
| Returns: | |
| bool: True if activation successful. | |
| """ | |
| permalink = product_permalink or self.PRODUCT_PERMALINK | |
| url = "https://api.gumroad.com/v2/licenses/verify" | |
| try: | |
| logger.info(f"Contacting License Server...") | |
| # Timeout is critical to avoid hanging app startup | |
| response = requests.post(url, data={ | |
| "product_permalink": permalink, | |
| "license_key": license_key | |
| }, timeout=10) | |
| data = response.json() | |
| if data.get("success") is True: | |
| # Valid License: Cache minimal data locally | |
| self.license_data = { | |
| "key": license_key, | |
| "uses": data.get("uses"), | |
| "email": data.get("purchase", {}).get("email"), | |
| "timestamp": data.get("purchase", {}).get("created_at"), | |
| "tier": "PRO" # Logic to distinguish tiers could go here | |
| } | |
| self._save_local() | |
| logger.info("License Activated. PRO Features Unlocked.") | |
| return True | |
| else: | |
| logger.error(f"License Activation Failed: {data.get('message')}") | |
| return False | |
| except Exception as e: | |
| logger.error(f"License Verification Network Error: {e}") | |
| return False | |
| def validate_offline(self) -> bool: | |
| """ | |
| Checks for a valid, tamper-evident local license file. | |
| Useful for air-gapped or offline startups. | |
| """ | |
| if self.license_data: | |
| return True | |
| return self._load_local() | |
| def _save_local(self) -> None: | |
| """Encrypts and persists the license state.""" | |
| if not self.license_data: | |
| return | |
| try: | |
| raw_json = json.dumps(self.license_data).encode('utf-8') | |
| encrypted = self._cipher.encrypt(raw_json) | |
| with open(self.storage_path, "wb") as f: | |
| f.write(encrypted) | |
| except Exception as e: | |
| logger.error(f"Failed to persist license: {e}") | |
| def _load_local(self) -> bool: | |
| """Decrypts and validates the local license file.""" | |
| if not os.path.exists(self.storage_path): | |
| return False | |
| try: | |
| with open(self.storage_path, "rb") as f: | |
| encrypted = f.read() | |
| decrypted = self._cipher.decrypt(encrypted) | |
| self.license_data = json.loads(decrypted.decode('utf-8')) | |
| return True | |
| except Exception: | |
| # Decryption failure implies file tampering or wrong key | |
| logger.warning("Local license file corrupted or tampered.") | |
| return False | |
| def get_tier(self) -> str: | |
| """Returns 'PRO' or 'BASELINE'.""" | |
| if self.validate_offline(): | |
| return self.license_data.get("tier", "BASELINE") | |
| return "BASELINE" | |