Files
xp-probackup/license_manager.py

158 lines
5.3 KiB
Python

import hashlib
import uuid
import subprocess
import platform
import os
import base64
import sys
# Simple Secret Salt - In a real app, obfuscate this or use asymmetric keys (RSA)
# For this level, a symmetric secret is "good enough" for basic protection.
SECRET_SALT = "ProBackup_Secret_Salt_2026_!@#"
# Flag to hide console window on Windows
CREATE_NO_WINDOW = 0x08000000
class LicenseManager:
@staticmethod
def _get_base_dir():
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def _get_license_path():
return os.path.join(LicenseManager._get_base_dir(), "license.dat")
@staticmethod
def save_license(key):
try:
with open(LicenseManager._get_license_path(), "w") as f:
f.write(key.strip())
return True
except:
return False
@staticmethod
def load_license():
path = LicenseManager._get_license_path()
if not os.path.exists(path):
return None
try:
with open(path, "r") as f:
return f.read().strip()
except:
return None
@staticmethod
def get_motherboard_id():
"""Gets the motherboard UUID using WMIC (Windows only)."""
try:
result = subprocess.run(
['wmic', 'csproduct', 'get', 'uuid'],
capture_output=True, text=True, timeout=5,
creationflags=CREATE_NO_WINDOW
)
lines = result.stdout.strip().split('\n')
for line in lines:
line = line.strip()
if line and line != 'UUID':
return line
except:
pass
return None
@staticmethod
def get_processor_id():
"""Gets the processor ID using WMIC (Windows only)."""
try:
result = subprocess.run(
['wmic', 'cpu', 'get', 'processorid'],
capture_output=True, text=True, timeout=5,
creationflags=CREATE_NO_WINDOW
)
lines = result.stdout.strip().split('\n')
for line in lines:
line = line.strip()
if line and line != 'ProcessorId':
return line
except:
pass
# Fallback to platform.processor()
return platform.processor()
@staticmethod
def _hash_to_hwid(raw_id):
"""Converts raw ID string to formatted HWID."""
hwid_hash = hashlib.sha256(raw_id.encode()).hexdigest().upper()
return "{}-{}-{}-{}".format(hwid_hash[:4], hwid_hash[4:8], hwid_hash[8:12], hwid_hash[12:16])
@staticmethod
def get_hardware_id():
"""
Returns hardware ID based on combination of Motherboard UUID + Processor ID.
This is for DISPLAY purposes to customer.
"""
mb_id = LicenseManager.get_motherboard_id() or "UNKNOWN_MB"
proc_id = LicenseManager.get_processor_id() or "UNKNOWN_CPU"
# Combine both IDs for display
combined = "COMBO-{}-{}".format(mb_id, proc_id)
return LicenseManager._hash_to_hwid(combined)
@staticmethod
def get_mb_hwid():
"""Returns HWID based on motherboard only."""
mb_id = LicenseManager.get_motherboard_id() or "UNKNOWN_MB"
return LicenseManager._hash_to_hwid("MB-{}".format(mb_id))
@staticmethod
def get_cpu_hwid():
"""Returns HWID based on processor only."""
proc_id = LicenseManager.get_processor_id() or "UNKNOWN_CPU"
return LicenseManager._hash_to_hwid("CPU-{}".format(proc_id))
@staticmethod
def generate_signature(hwid):
"""Generates the expected signature for a given HWID."""
data = "{}|{}".format(hwid, SECRET_SALT)
return hashlib.sha256(data.encode()).hexdigest().upper()
@staticmethod
def validate_license(key, hwid=None):
"""
Validates license with flexible hardware check.
Returns tuple: (is_valid, status)
- status: 'valid', 'mb_changed', 'cpu_changed', 'both_changed', 'invalid'
"""
if not key:
return False, 'invalid'
key = key.strip().upper()
# Check against combined HWID (exact match - preferred)
combined_hwid = LicenseManager.get_hardware_id()
expected_combined = LicenseManager.generate_signature(combined_hwid)[:32]
if key == expected_combined:
return True, 'valid'
# Check against motherboard only
mb_hwid = LicenseManager.get_mb_hwid()
expected_mb = LicenseManager.generate_signature(mb_hwid)[:32]
mb_match = (key == expected_mb)
# Check against processor only
cpu_hwid = LicenseManager.get_cpu_hwid()
expected_cpu = LicenseManager.generate_signature(cpu_hwid)[:32]
cpu_match = (key == expected_cpu)
# If either matches, license is valid (single hardware change allowed)
if mb_match:
return True, 'cpu_changed' # MB matches, CPU must have changed
if cpu_match:
return True, 'mb_changed' # CPU matches, MB must have changed
# Neither matches - could be both changed or completely invalid key
return False, 'both_changed'