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 sqlite3
import time import time
from pathlib import Path
import bcrypt import bcrypt
from cryptography.fernet import Fernet
from flask import Blueprint, request, redirect, flash from flask import Blueprint, request, redirect, flash
import auth import auth
import config_utils import config_utils
@ -20,19 +19,39 @@ USER_TYPE_CAPTIVE = 0
USER_TYPE_SUPPLICANT = 1 USER_TYPE_SUPPLICANT = 1
HASH_CLEARTEXT = 0 HASH_CLEARTEXT = 0
HASH_NT = 1
HASH_BCRYPT = 2 HASH_BCRYPT = 2
VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT} VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT}
VALID_HASH_TYPES = {HASH_CLEARTEXT, HASH_NT, HASH_BCRYPT}
# Compatible hash types per user type HASH_FOR_USER_TYPE = {
COMPATIBLE_HASHES = { USER_TYPE_CAPTIVE: HASH_BCRYPT,
USER_TYPE_CAPTIVE: {HASH_CLEARTEXT, HASH_BCRYPT}, USER_TYPE_SUPPLICANT: HASH_CLEARTEXT,
USER_TYPE_SUPPLICANT: {HASH_CLEARTEXT, HASH_NT},
} }
# ===================================================================
# 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 # DB helpers
# =================================================================== # ===================================================================
@ -78,12 +97,7 @@ def _get_by_index(conn, row_index):
def _hash_password(plaintext, hash_type): def _hash_password(plaintext, hash_type):
if hash_type == HASH_CLEARTEXT: if hash_type == HASH_CLEARTEXT:
return plaintext return encrypt_password(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.")
if hash_type == HASH_BCRYPT: if hash_type == HASH_BCRYPT:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode() return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode()
raise ValueError(f"Unknown hash_type: {hash_type}") raise ValueError(f"Unknown hash_type: {hash_type}")
@ -144,20 +158,15 @@ def addedit():
try: try:
user_type = int(request.form.get('user_type', '')) user_type = int(request.form.get('user_type', ''))
hash_type = int(request.form.get('hash_type', ''))
except (ValueError, TypeError): except (ValueError, TypeError):
flash('Invalid user type or hash type.', 'error') flash('Invalid user type.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
if user_type not in VALID_USER_TYPES: if user_type not in VALID_USER_TYPES:
flash('Invalid user type.', 'error') flash('Invalid user type.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
if hash_type not in VALID_HASH_TYPES:
flash('Invalid hash type.', 'error') hash_type = HASH_FOR_USER_TYPE[user_type]
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}')
vlan = sanitize.name(request.form.get('vlan', '')) vlan = sanitize.name(request.form.get('vlan', ''))
if not vlan: if not vlan:

View file

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

View file

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

View file

@ -7,3 +7,16 @@ def is_production():
def is_pro(): def is_pro():
return bool(os.environ.get('LICENSE', '').strip()) 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 - WEB_APP_DISPLAY_NAME=Routlin Dashboard
- INITIAL_MANAGER_EMAIL=mgrotke@gmail.com - INITIAL_MANAGER_EMAIL=mgrotke@gmail.com
- SECRET_KEY=ey8hSQCCYE5kQXV8nOg1CB44LSd3AoUet2ZBc3aZlFrwBbazE7aHcxXWyuT97eAObet5jmOL0CjMg0rB1hE4d2SBVYHPfl8De55EiFv307r1QP3Mf5XgOSSCxD3TuD - SECRET_KEY=ey8hSQCCYE5kQXV8nOg1CB44LSd3AoUet2ZBc3aZlFrwBbazE7aHcxXWyuT97eAObet5jmOL0CjMg0rB1hE4d2SBVYHPfl8De55EiFv307r1QP3Mf5XgOSSCxD3TuD
- CREDENTIALS_KEY=TwnRAoORr7OaMVeS3q4JJP3NYvBDlyPB8qgl2ovAlm2OGsNf0qsnv0a67MXgaozKWf5Gc1CM0Z1m0xdTQeiw4R0RKK0fmLKMKfttOp2sfKg9lDsMZavJWzn5VS8dyD
- SMTP_HOST=smtp.gmail.com - SMTP_HOST=smtp.gmail.com
- SMTP_PORT=587 - SMTP_PORT=587
- SMTP_USER=grotek.industries@gmail.com - SMTP_USER=grotek.industries@gmail.com