Development

This commit is contained in:
Matthew Grotke 2026-06-07 00:50:49 -04:00
parent 5071f06624
commit a3bab5ff1f
5 changed files with 53 additions and 50 deletions

View file

@ -1,9 +1,8 @@
import hashlib
import sqlite3
import time
from pathlib import Path
import bcrypt
from cryptography.fernet import Fernet
from flask import Blueprint, request, redirect, flash
import auth
import config_utils
@ -20,19 +19,39 @@ USER_TYPE_CAPTIVE = 0
USER_TYPE_SUPPLICANT = 1
HASH_CLEARTEXT = 0
HASH_NT = 1
HASH_BCRYPT = 2
VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT}
VALID_HASH_TYPES = {HASH_CLEARTEXT, HASH_NT, HASH_BCRYPT}
VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT}
# Compatible hash types per user type
COMPATIBLE_HASHES = {
USER_TYPE_CAPTIVE: {HASH_CLEARTEXT, HASH_BCRYPT},
USER_TYPE_SUPPLICANT: {HASH_CLEARTEXT, HASH_NT},
HASH_FOR_USER_TYPE = {
USER_TYPE_CAPTIVE: HASH_BCRYPT,
USER_TYPE_SUPPLICANT: HASH_CLEARTEXT,
}
# ===================================================================
# Encryption helpers (cleartext passwords only)
# ===================================================================
_credentials_key = settings.get_credentials_key()
_FERNET = Fernet(_credentials_key) if _credentials_key else None
def encrypt_password(plaintext):
if _FERNET is None:
return plaintext
return _FERNET.encrypt(plaintext.encode()).decode()
def decrypt_password(stored):
if _FERNET is None:
return stored
try:
return _FERNET.decrypt(stored.encode()).decode()
except Exception:
return stored
# ===================================================================
# DB helpers
# ===================================================================
@ -78,12 +97,7 @@ def _get_by_index(conn, row_index):
def _hash_password(plaintext, hash_type):
if hash_type == HASH_CLEARTEXT:
return plaintext
if hash_type == HASH_NT:
try:
return hashlib.new('md4', plaintext.encode('utf-16-le')).hexdigest()
except ValueError:
raise ValueError("NT-Password requires MD4 support. It may be disabled on this system's OpenSSL build.")
return encrypt_password(plaintext)
if hash_type == HASH_BCRYPT:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode()
raise ValueError(f"Unknown hash_type: {hash_type}")
@ -144,20 +158,15 @@ def addedit():
try:
user_type = int(request.form.get('user_type', ''))
hash_type = int(request.form.get('hash_type', ''))
except (ValueError, TypeError):
flash('Invalid user type or hash type.', 'error')
flash('Invalid user type.', 'error')
return redirect(f'/{_PAGE}')
if user_type not in VALID_USER_TYPES:
flash('Invalid user type.', 'error')
return redirect(f'/{_PAGE}')
if hash_type not in VALID_HASH_TYPES:
flash('Invalid hash type.', 'error')
return redirect(f'/{_PAGE}')
if hash_type not in COMPATIBLE_HASHES[user_type]:
flash('Selected hash type is not compatible with the selected user type.', 'error')
return redirect(f'/{_PAGE}')
hash_type = HASH_FOR_USER_TYPE[user_type]
vlan = sanitize.name(request.form.get('vlan', ''))
if not vlan:

View file

@ -37,11 +37,6 @@
"field": "user_type_label",
"class": "col-narrow"
},
{
"label": "Hash",
"field": "hash_type_label",
"class": "col-narrow"
},
{
"label": "VLAN",
"field": "vlan",
@ -126,26 +121,13 @@
"type": "hr"
},
{
"type": "field_row",
"cols": 2,
"items": [
{
"type": "field",
"label": "User Type",
"name": "user_type",
"input_type": "select",
"options": [
{"value": "0", "label": "Captive Portal"},
{"value": "1", "label": "802.1X Supplicant"}
]
},
{
"type": "field",
"label": "Hash Type",
"name": "hash_type",
"input_type": "select",
"options": []
}
"type": "field",
"label": "User Type",
"name": "user_type",
"input_type": "select",
"options": [
{"value": "0", "label": "Captive Portal"},
{"value": "1", "label": "802.1X Supplicant"}
]
},
{

View file

@ -10,7 +10,6 @@ import settings
PRO_LICENSE = settings.is_pro()
USER_TYPE_LABELS = {0: 'Captive Portal', 1: '802.1X'}
HASH_TYPE_LABELS = {0: 'Cleartext', 1: 'NT-Password', 2: 'Bcrypt'}
def _load_credentials():
@ -88,7 +87,6 @@ def collect_tokens(cfg):
r = dict(row)
r.pop('password', None)
r['user_type_label'] = USER_TYPE_LABELS.get(r.get('user_type'), str(r.get('user_type', '')))
r['hash_type_label'] = HASH_TYPE_LABELS.get(r.get('hash_type'), str(r.get('hash_type', '')))
r['expires_label'] = _format_expiry(r.get('date_set', 0), r.get('valid_for'))
display_rows.append(r)

View file

@ -7,3 +7,16 @@ def is_production():
def is_pro():
return bool(os.environ.get('LICENSE', '').strip())
def get_credentials_key():
"""Return a Fernet-compatible key derived from the CREDENTIALS_KEY environment variable,
or None if not set. SHA-256 hashes the raw string to produce 32 bytes, which are then
URL-safe base64-encoded as required by Fernet."""
import base64
import hashlib
key_str = os.environ.get('CREDENTIALS_KEY', '')
if not key_str:
return None
raw = hashlib.sha256(key_str.encode()).digest()
return base64.urlsafe_b64encode(raw)

View file

@ -19,6 +19,7 @@ services:
- WEB_APP_DISPLAY_NAME=Routlin Dashboard
- INITIAL_MANAGER_EMAIL=mgrotke@gmail.com
- SECRET_KEY=ey8hSQCCYE5kQXV8nOg1CB44LSd3AoUet2ZBc3aZlFrwBbazE7aHcxXWyuT97eAObet5jmOL0CjMg0rB1hE4d2SBVYHPfl8De55EiFv307r1QP3Mf5XgOSSCxD3TuD
- CREDENTIALS_KEY=TwnRAoORr7OaMVeS3q4JJP3NYvBDlyPB8qgl2ovAlm2OGsNf0qsnv0a67MXgaozKWf5Gc1CM0Z1m0xdTQeiw4R0RKK0fmLKMKfttOp2sfKg9lDsMZavJWzn5VS8dyD
- SMTP_HOST=smtp.gmail.com
- SMTP_PORT=587
- SMTP_USER=grotek.industries@gmail.com