From feecb97206437e0c75fe4a515ace4de4d453af7f Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 22 May 2026 16:45:54 -0400 Subject: [PATCH] 05/22 Implement license version --- .gitignore | 3 +- license_server/app.py | 342 -------------------------------- license_server/generate_keys.py | 73 ------- license_server/requirements.txt | 2 - 4 files changed, 2 insertions(+), 418 deletions(-) delete mode 100644 license_server/app.py delete mode 100644 license_server/generate_keys.py delete mode 100644 license_server/requirements.txt diff --git a/.gitignore b/.gitignore index 55fef4d..cc129c8 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,5 @@ Thumbs.db # ─── pytest / coverage ─────────────────────────────────────────────────────── .pytest_cache/ .coverage -htmlcov/ \ No newline at end of file +htmlcov/ +license_server/ \ No newline at end of file diff --git a/license_server/app.py b/license_server/app.py deleted file mode 100644 index 10531f9..0000000 --- a/license_server/app.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -TechDesk License Server -======================= -A simple, password-protected internal Flask app for generating and tracking -TechDesk license keys. - -Setup ------ -1. Run generate_keys.py once to produce private_key.pem and public_key.pem. -2. Set environment variables (or edit the constants below): - LICENSE_SERVER_PASSWORD — admin password (default: change-me) - SECRET_KEY — Flask secret key for sessions -3. pip install -r requirements.txt -4. python app.py - -This app is intended for localhost / internal network use only. -Do NOT expose it to the public internet. - -License key format ------------------- - TDESK-. - -Where: - payload_b64 = base64url( JSON payload bytes ) - signature_b64 = base64url( RSA-SHA256 signature of payload bytes ) - -JSON payload fields: - customer — company / customer name - email — admin contact email - tier — "community" | "business" | "enterprise" - issued_at — ISO date string (YYYY-MM-DD) - expires_at — ISO date string (YYYY-MM-DD) -""" - -import os -import json -import base64 -import sqlite3 -from datetime import date, timedelta -from functools import wraps -from flask import Flask, request, render_template_string, redirect, url_for, session, flash -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding - -app = Flask(__name__) -app.secret_key = os.environ.get('SECRET_KEY', 'license-server-secret-change-me') - -ADMIN_PASSWORD = os.environ.get('LICENSE_SERVER_PASSWORD', 'change-me') -PRIVATE_KEY_FILE = os.path.join(os.path.dirname(__file__), 'private_key.pem') -DB_FILE = os.path.join(os.path.dirname(__file__), 'licenses.db') - -TIERS = ['community', 'business', 'enterprise'] - - -# ── Database ────────────────────────────────────────────────────────────────── - -def get_db(): - conn = sqlite3.connect(DB_FILE) - conn.row_factory = sqlite3.Row - return conn - - -def init_db(): - with get_db() as conn: - conn.execute(''' - CREATE TABLE IF NOT EXISTS licenses ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer TEXT NOT NULL, - email TEXT NOT NULL, - tier TEXT NOT NULL, - issued_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - key_preview TEXT NOT NULL, - full_key TEXT NOT NULL - ) - ''') - - -# ── RSA helpers ─────────────────────────────────────────────────────────────── - -def _load_private_key(): - if not os.path.exists(PRIVATE_KEY_FILE): - raise FileNotFoundError( - f'private_key.pem not found at {PRIVATE_KEY_FILE}. ' - 'Run generate_keys.py first.' - ) - with open(PRIVATE_KEY_FILE, 'rb') as f: - return serialization.load_pem_private_key(f.read(), password=None) - - -def generate_license_key(customer, email, tier, expires_at): - payload = { - 'customer' : customer, - 'email' : email, - 'tier' : tier, - 'issued_at' : date.today().isoformat(), - 'expires_at': expires_at, - } - payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8') - private_key = _load_private_key() - signature = private_key.sign(payload_bytes, padding.PKCS1v15(), hashes.SHA256()) - - payload_b64 = base64.urlsafe_b64encode(payload_bytes).decode() - sig_b64 = base64.urlsafe_b64encode(signature).decode() - return f'TDESK-{payload_b64}.{sig_b64}' - - -# ── Auth ────────────────────────────────────────────────────────────────────── - -def login_required(f): - @wraps(f) - def decorated(*args, **kwargs): - if not session.get('authenticated'): - return redirect(url_for('login')) - return f(*args, **kwargs) - return decorated - - -# ── Templates ───────────────────────────────────────────────────────────────── - -_BASE = ''' - - - - - TechDesk License Server - - - -

🔑 TechDesk License Server

-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% for cat, msg in messages %} -
{{ msg }}
- {% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - -''' - -_LOGIN = ''' -{% extends base %} -{% block content %} -
-

Login

-
- - - -
-
-{% endblock %} -''' - -_INDEX = ''' -{% extends base %} -{% block content %} -
-

Generate New License Key

-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
-
- -{% if new_key %} -
-

✅ New License Key Generated

-

Copy this key and send it to the customer. - It will not be shown again in full.

-
{{ new_key }}
-
-{% endif %} - -
-

Issued Licenses ({{ licenses|length }})

- {% if licenses %} - - - - - - - - - {% for lic in licenses %} - - - - - - - - - - {% endfor %} - -
#CustomerEmailTierIssuedExpiresKey Preview
{{ lic.id }}{{ lic.customer }}{{ lic.email }}{{ lic.tier }}{{ lic.issued_at }}{{ lic.expires_at }}{{ lic.key_preview }}…
- {% else %} -

No licenses issued yet.

- {% endif %} -
- -

- Logout -

-{% endblock %} -''' - - -# ── Routes ──────────────────────────────────────────────────────────────────── - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - if request.form.get('password') == ADMIN_PASSWORD: - session['authenticated'] = True - return redirect(url_for('index')) - flash('Incorrect password.', 'error') - return render_template_string(_LOGIN, base=_BASE) - - -@app.route('/logout') -def logout(): - session.clear() - return redirect(url_for('login')) - - -@app.route('/') -@login_required -def index(): - with get_db() as conn: - licenses = conn.execute( - 'SELECT * FROM licenses ORDER BY id DESC' - ).fetchall() - return render_template_string(_INDEX, base=_BASE, licenses=licenses, new_key=None) - - -@app.route('/generate', methods=['POST']) -@login_required -def generate(): - customer = request.form.get('customer', '').strip() - email = request.form.get('email', '').strip() - tier = request.form.get('tier', 'business') - duration = int(request.form.get('duration', 365)) - - if not customer or not email: - flash('Customer name and email are required.', 'error') - return redirect(url_for('index')) - - if tier not in TIERS: - flash('Invalid tier.', 'error') - return redirect(url_for('index')) - - expires_at = (date.today() + timedelta(days=duration)).isoformat() - - try: - key = generate_license_key(customer, email, tier, expires_at) - except FileNotFoundError as exc: - flash(str(exc), 'error') - return redirect(url_for('index')) - - key_preview = key[:32] - - with get_db() as conn: - conn.execute( - 'INSERT INTO licenses (customer, email, tier, issued_at, expires_at, key_preview, full_key) ' - 'VALUES (?, ?, ?, ?, ?, ?, ?)', - (customer, email, tier, date.today().isoformat(), expires_at, key_preview, key), - ) - - with get_db() as conn: - licenses = conn.execute( - 'SELECT * FROM licenses ORDER BY id DESC' - ).fetchall() - - flash(f'License key generated for {customer}.', 'success') - return render_template_string(_INDEX, base=_BASE, licenses=licenses, new_key=key) - - -if __name__ == '__main__': - init_db() - print('TechDesk License Server running at http://127.0.0.1:5001') - print(f'Admin password: {ADMIN_PASSWORD}') - app.run(host='127.0.0.1', port=5001, debug=False) diff --git a/license_server/generate_keys.py b/license_server/generate_keys.py deleted file mode 100644 index 30e1ade..0000000 --- a/license_server/generate_keys.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -generate_keys.py — One-time RSA-2048 key pair generator for TechDesk licensing. - -Run this ONCE on your server to produce the key pair: - - python generate_keys.py - -Output: - private_key.pem — Keep secret. Never ship to customers. - public_key.pem — Paste the contents into license_service.py in TechDesk. - -Security notes: - - private_key.pem must NEVER be committed to git or shipped in any customer build. - - public_key.pem is safe to embed in TechDesk — it can only verify signatures, - not forge them. - - Store private_key.pem on a secure, offline or access-restricted machine. -""" - -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import serialization -import os - -KEY_SIZE = 2048 -PRIVATE_KEY_FILE = 'private_key.pem' -PUBLIC_KEY_FILE = 'public_key.pem' - - -def generate(): - if os.path.exists(PRIVATE_KEY_FILE) or os.path.exists(PUBLIC_KEY_FILE): - print('Key files already exist. Delete them manually before regenerating.') - print(f' {PRIVATE_KEY_FILE}') - print(f' {PUBLIC_KEY_FILE}') - return - - private_key = rsa.generate_private_key( - public_exponent=65537, - key_size=KEY_SIZE, - ) - - # Write private key (unencrypted PEM — restrict file permissions after writing) - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - with open(PRIVATE_KEY_FILE, 'wb') as f: - f.write(private_pem) - - # Write public key (PEM) - public_pem = private_key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - with open(PUBLIC_KEY_FILE, 'wb') as f: - f.write(public_pem) - - # Restrict private key permissions on Unix-like systems - try: - os.chmod(PRIVATE_KEY_FILE, 0o600) - except AttributeError: - pass # Windows — manage permissions manually - - print('Key pair generated successfully.') - print(f' Private key : {PRIVATE_KEY_FILE} (KEEP SECRET)') - print(f' Public key : {PUBLIC_KEY_FILE} (safe to embed in TechDesk)') - print() - print('Next steps:') - print(' 1. Copy the contents of public_key.pem into license_service.py (PUBLIC_KEY_PEM constant).') - print(' 2. Never commit or share private_key.pem.') - - -if __name__ == '__main__': - generate() diff --git a/license_server/requirements.txt b/license_server/requirements.txt deleted file mode 100644 index 9d6ce20..0000000 --- a/license_server/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flask==3.0.3 -cryptography==42.0.8