Development
This commit is contained in:
parent
5071f06624
commit
a3bab5ff1f
5 changed files with 53 additions and 50 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue