05/22 Implement license version

This commit is contained in:
2026-05-22 16:49:25 -04:00
parent 687216e332
commit feecb97206
4 changed files with 2 additions and 418 deletions
+1
View File
@@ -51,3 +51,4 @@ Thumbs.db
.pytest_cache/
.coverage
htmlcov/
license_server/
-342
View File
@@ -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-<payload_b64>.<signature_b64>
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 = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>TechDesk License Server</title>
<style>
body { font-family: system-ui, sans-serif; background: #f5f5f5; margin: 0; padding: 0; }
.header { background: #1e3a5f; color: #fff; padding: 16px 32px; }
.header h1 { margin: 0; font-size: 1.4rem; }
.container { max-width: 1000px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.1); padding: 24px; margin-bottom: 24px; }
label { display: block; margin-bottom: 4px; font-weight: 600; font-size: .9rem; color: #555; }
input, select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
font-size: .95rem; box-sizing: border-box; margin-bottom: 14px; }
.btn { background: #1e3a5f; color: #fff; border: none; padding: 10px 20px;
border-radius: 6px; cursor: pointer; font-size: .95rem; }
.btn:hover { background: #163055; }
.btn-danger { background: #dc2626; }
.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
.key-box { background: #f0f4f8; border: 1px solid #cbd5e0; border-radius: 6px;
padding: 12px; font-family: monospace; word-break: break-all;
font-size: .85rem; margin-top: 8px; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th { background: #f1f5f9; padding: 10px 12px; text-align: left; color: #475569; }
td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: .8rem; font-weight: 600; }
.badge-community { background: #e2e8f0; color: #475569; }
.badge-business { background: #dbeafe; color: #1d4ed8; }
.badge-enterprise { background: #ede9fe; color: #6d28d9; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
</style>
</head>
<body>
<div class="header"><h1>🔑 TechDesk License Server</h1></div>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }}">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</body>
</html>
'''
_LOGIN = '''
{% extends base %}
{% block content %}
<div class="card" style="max-width:400px;margin:0 auto;">
<h2 style="margin-top:0;">Login</h2>
<form method="POST">
<label>Password</label>
<input type="password" name="password" autofocus required>
<button type="submit" class="btn">Login</button>
</form>
</div>
{% endblock %}
'''
_INDEX = '''
{% extends base %}
{% block content %}
<div class="card">
<h2 style="margin-top:0;">Generate New License Key</h2>
<form method="POST" action="/generate">
<div class="form-row">
<div>
<label>Customer / Company Name</label>
<input type="text" name="customer" required placeholder="Acme Corp">
</div>
<div>
<label>Contact Email</label>
<input type="email" name="email" required placeholder="admin@acme.com">
</div>
</div>
<div class="form-row">
<div>
<label>Tier</label>
<select name="tier">
<option value="business">Business</option>
<option value="enterprise">Enterprise</option>
<option value="community">Community (free)</option>
</select>
</div>
<div>
<label>Duration</label>
<select name="duration">
<option value="365">1 year</option>
<option value="730">2 years</option>
<option value="180">6 months</option>
<option value="36500">Lifetime (100 years)</option>
</select>
</div>
</div>
<button type="submit" class="btn">Generate Key</button>
</form>
</div>
{% if new_key %}
<div class="card" style="border: 2px solid #3b82f6;">
<h3 style="margin-top:0;color:#1d4ed8;">✅ New License Key Generated</h3>
<p style="margin:0 0 8px;color:#555;">Copy this key and send it to the customer.
It will not be shown again in full.</p>
<div class="key-box">{{ new_key }}</div>
</div>
{% endif %}
<div class="card">
<h2 style="margin-top:0;">Issued Licenses ({{ licenses|length }})</h2>
{% if licenses %}
<table>
<thead>
<tr>
<th>#</th><th>Customer</th><th>Email</th><th>Tier</th>
<th>Issued</th><th>Expires</th><th>Key Preview</th>
</tr>
</thead>
<tbody>
{% for lic in licenses %}
<tr>
<td>{{ lic.id }}</td>
<td>{{ lic.customer }}</td>
<td>{{ lic.email }}</td>
<td><span class="badge badge-{{ lic.tier }}">{{ lic.tier }}</span></td>
<td>{{ lic.issued_at }}</td>
<td>{{ lic.expires_at }}</td>
<td style="font-family:monospace;font-size:.8rem;">{{ lic.key_preview }}…</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color:#888;">No licenses issued yet.</p>
{% endif %}
</div>
<p style="text-align:right;">
<a href="/logout" style="color:#dc2626;font-size:.9rem;">Logout</a>
</p>
{% 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)
-73
View File
@@ -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()
-2
View File
@@ -1,2 +0,0 @@
flask==3.0.3
cryptography==42.0.8