From a3bab5ff1ff2cf190321d2a651a47544b2fdc414 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sun, 7 Jun 2026 00:50:49 -0400 Subject: [PATCH] Development --- .../app/pages/clientcredentials/action.py | 55 +++++++++++-------- .../app/pages/clientcredentials/content.json | 32 +++-------- .../app/pages/clientcredentials/view.py | 2 - docker/routlin-dash/app/settings.py | 13 +++++ docker/routlin-dash/docker-compose.yml | 1 + 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/docker/routlin-dash/app/pages/clientcredentials/action.py b/docker/routlin-dash/app/pages/clientcredentials/action.py index 59e83f8..d02e680 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/action.py +++ b/docker/routlin-dash/app/pages/clientcredentials/action.py @@ -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: diff --git a/docker/routlin-dash/app/pages/clientcredentials/content.json b/docker/routlin-dash/app/pages/clientcredentials/content.json index 1f04020..43ecd87 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/content.json +++ b/docker/routlin-dash/app/pages/clientcredentials/content.json @@ -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"} ] }, { diff --git a/docker/routlin-dash/app/pages/clientcredentials/view.py b/docker/routlin-dash/app/pages/clientcredentials/view.py index 0806b45..5b611c4 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/view.py +++ b/docker/routlin-dash/app/pages/clientcredentials/view.py @@ -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) diff --git a/docker/routlin-dash/app/settings.py b/docker/routlin-dash/app/settings.py index 1fd6bc2..0688863 100644 --- a/docker/routlin-dash/app/settings.py +++ b/docker/routlin-dash/app/settings.py @@ -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) diff --git a/docker/routlin-dash/docker-compose.yml b/docker/routlin-dash/docker-compose.yml index 88d29f1..f8a88c4 100644 --- a/docker/routlin-dash/docker-compose.yml +++ b/docker/routlin-dash/docker-compose.yml @@ -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