feat: Introduce initial application structure, SQL database tools, and license management.
This commit is contained in:
144
license_manager.py
Normal file
144
license_manager.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import hashlib
|
||||
import uuid
|
||||
import subprocess
|
||||
import platform
|
||||
import os
|
||||
import base64
|
||||
|
||||
# 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_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'
|
||||
|
||||
@staticmethod
|
||||
def save_license(key):
|
||||
try:
|
||||
with open("license.dat", "w") as f:
|
||||
f.write(key.strip())
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_license():
|
||||
if not os.path.exists("license.dat"):
|
||||
return None
|
||||
try:
|
||||
with open("license.dat", "r") as f:
|
||||
return f.read().strip()
|
||||
except:
|
||||
return None
|
||||
Reference in New Issue
Block a user