Development

This commit is contained in:
Matthew Grotke 2026-06-13 10:02:51 -04:00
parent 8a8e947fcf
commit 450c0081f7
9 changed files with 59 additions and 28 deletions

View file

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, session, redirect, flash from flask import Blueprint, request, session, redirect, flash
import bcrypt, secrets, smtplib import secrets, smtplib
import settings
import time import time
from email.message import EmailMessage from email.message import EmailMessage
import auth import auth
@ -78,8 +79,7 @@ def form_create():
flash('This account is already set up. Please log in instead.', 'error') flash('This account is already set up. Please log in instead.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
salt = bcrypt.gensalt() hashed = settings.hash_password(password)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
code = f'{secrets.randbelow(1000000):06d}' code = f'{secrets.randbelow(1000000):06d}'
try: try:

View file

@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, session, redirect, flash from flask import Blueprint, request, session, redirect, flash
import bcrypt
import auth import auth
import config_utils import config_utils
import sanitize import sanitize
@ -29,7 +28,7 @@ def form_login():
if email != settings.get_initial_manager_email() or not stored_hash: if email != settings.get_initial_manager_email() or not stored_hash:
flash('Invalid email address or password.', 'error') flash('Invalid email address or password.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash.encode('utf-8')): if not settings.verify_password(password, stored_hash):
flash('Invalid email address or password.', 'error') flash('Invalid email address or password.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
session.clear() session.clear()
@ -76,7 +75,7 @@ def form_login():
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error') flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
if not bcrypt.checkpw(password.encode('utf-8'), account['hashed_password'].encode('utf-8')): if not settings.verify_password(password, account['hashed_password']):
flash('Invalid email address or password.', 'error') flash('Invalid email address or password.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')

View file

@ -1,7 +1,6 @@
import sqlite3 import sqlite3
import time import time
import bcrypt
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from flask import Blueprint, request, redirect, flash from flask import Blueprint, request, redirect, flash
import auth import auth
@ -19,12 +18,12 @@ USER_TYPE_CAPTIVE = 0
USER_TYPE_SUPPLICANT = 1 USER_TYPE_SUPPLICANT = 1
DIGEST_CYPHERTEXT_FERNET = 0 DIGEST_CYPHERTEXT_FERNET = 0
DIGEST_HASH_BCRYPT = 2 DIGEST_HASH_SCRYPT = 2
VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT} VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT}
HASH_FOR_USER_TYPE = { HASH_FOR_USER_TYPE = {
USER_TYPE_CAPTIVE: DIGEST_HASH_BCRYPT, USER_TYPE_CAPTIVE: DIGEST_HASH_SCRYPT,
USER_TYPE_SUPPLICANT: DIGEST_CYPHERTEXT_FERNET, USER_TYPE_SUPPLICANT: DIGEST_CYPHERTEXT_FERNET,
} }
@ -99,8 +98,8 @@ def _get_by_index(conn, row_index):
def _hash_password(plaintext, digest_type): def _hash_password(plaintext, digest_type):
if digest_type == DIGEST_CYPHERTEXT_FERNET: if digest_type == DIGEST_CYPHERTEXT_FERNET:
return encrypt_password(plaintext) return encrypt_password(plaintext)
if digest_type == DIGEST_HASH_BCRYPT: if digest_type == DIGEST_HASH_SCRYPT:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode() return settings.hash_password(plaintext)
raise ValueError(f"Unknown digest_type: {digest_type}") raise ValueError(f"Unknown digest_type: {digest_type}")

View file

@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, session, redirect, flash from flask import Blueprint, request, session, redirect, flash
import bcrypt
import auth import auth
import config_utils import config_utils
import sanitize import sanitize
@ -131,11 +130,11 @@ def changepassword_save():
flash('New password must be at least 8 characters.', 'error') flash('New password must be at least 8 characters.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
hashed = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') hashed = settings.hash_password(new_password)
if settings.is_single_user(): if settings.is_single_user():
stored_hash = settings.get_initial_manager_password_hash() stored_hash = settings.get_initial_manager_password_hash()
if not stored_hash or not bcrypt.checkpw(current_password.encode('utf-8'), stored_hash.encode('utf-8')): if not stored_hash or not settings.verify_password(current_password, stored_hash):
flash('Current password is incorrect.', 'error') flash('Current password is incorrect.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
try: try:
@ -156,7 +155,7 @@ def changepassword_save():
flash('Account not found. Please log in again.', 'error') flash('Account not found. Please log in again.', 'error')
return redirect('/accountlogin') return redirect('/accountlogin')
if not bcrypt.checkpw(current_password.encode('utf-8'), account['hashed_password'].encode('utf-8')): if not settings.verify_password(current_password, account['hashed_password']):
flash('Current password is incorrect.', 'error') flash('Current password is incorrect.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')

View file

@ -100,6 +100,28 @@ def get_credentials_key():
return base64.urlsafe_b64encode(raw) return base64.urlsafe_b64encode(raw)
def hash_password(plaintext):
import hashlib, os as _os
salt = _os.urandom(16)
h = hashlib.scrypt(plaintext.encode('utf-8'), salt=salt, n=16384, r=8, p=1, dklen=32)
return f'scrypt:16384:8:1:{salt.hex()}:{h.hex()}'
def verify_password(plaintext, stored):
import hashlib, hmac
try:
tag, n, r, p, salt_hex, hash_hex = stored.split(':')
if tag != 'scrypt':
return False
salt = bytes.fromhex(salt_hex)
expected = bytes.fromhex(hash_hex)
h = hashlib.scrypt(plaintext.encode('utf-8'), salt=salt,
n=int(n), r=int(r), p=int(p), dklen=len(expected))
return hmac.compare_digest(h, expected)
except Exception:
return False
def get_smtp_config(): def get_smtp_config():
"""Return SMTP settings from app_config.json, falling back to env vars.""" """Return SMTP settings from app_config.json, falling back to env vars."""
cfg = _load_app_config() cfg = _load_app_config()

View file

@ -1,5 +1,4 @@
flask flask
bcrypt
cryptography cryptography
manuf manuf
mac-vendor-lookup mac-vendor-lookup

View file

@ -1,14 +1,29 @@
import hashlib
import hmac
import ipaddress import ipaddress
import sqlite3 import sqlite3
import time import time
import bcrypt
from flask import Blueprint, request, redirect from flask import Blueprint, request, redirect
import config_utils import config_utils
CREDENTIALS_DB = f'{config_utils.CONFIGS_DIR}/.client-credentials' CREDENTIALS_DB = f'{config_utils.CONFIGS_DIR}/.client-credentials'
USER_TYPE_CAPTIVE = 0 USER_TYPE_CAPTIVE = 0
DIGEST_HASH_BCRYPT = 2 DIGEST_HASH_SCRYPT = 2
def _verify_scrypt(plaintext, stored):
try:
tag, n, r, p, salt_hex, hash_hex = stored.split(':')
if tag != 'scrypt':
return False
salt = bytes.fromhex(salt_hex)
expected = bytes.fromhex(hash_hex)
h = hashlib.scrypt(plaintext.encode('utf-8'), salt=salt,
n=int(n), r=int(r), p=int(p), dklen=len(expected))
return hmac.compare_digest(h, expected)
except Exception:
return False
bp = Blueprint('portal', __name__) bp = Blueprint('portal', __name__)
@ -85,11 +100,8 @@ def _verify_credential(username, password, vlan_name):
return False return False
if row['expires_seconds'] > 0 and (row['date_set'] + row['expires_seconds']) < now: if row['expires_seconds'] > 0 and (row['date_set'] + row['expires_seconds']) < now:
return False return False
if row['digest_type'] == DIGEST_HASH_BCRYPT: if row['digest_type'] == DIGEST_HASH_SCRYPT:
try: return _verify_scrypt(password, row['password'])
return bcrypt.checkpw(password.encode(), row['password'].encode())
except Exception:
return False
return False return False

View file

@ -1,2 +1 @@
flask flask
bcrypt

View file

@ -373,7 +373,7 @@ def setup_docker_compose(reuse_config=False):
print() print()
while True: while True:
import getpass as _gp import getpass as _gp
pw = _gp.getpass(f" Password for {manager_email}: ") pw = _gp.getpass(f" Create password for {manager_email}: ")
pw2 = _gp.getpass(f" Confirm password: ") pw2 = _gp.getpass(f" Confirm password: ")
if pw != pw2: if pw != pw2:
print(" Passwords do not match. Try again.") print(" Passwords do not match. Try again.")
@ -382,8 +382,10 @@ def setup_docker_compose(reuse_config=False):
print(" Password must be at least 8 characters.") print(" Password must be at least 8 characters.")
continue continue
break break
import bcrypt as _bcrypt import hashlib as _hashlib, os as _os
pw_hash = _bcrypt.hashpw(pw.encode('utf-8'), _bcrypt.gensalt()).decode('utf-8') _salt = _os.urandom(16)
_h = _hashlib.scrypt(pw.encode('utf-8'), salt=_salt, n=16384, r=8, p=1, dklen=32)
pw_hash = f'scrypt:16384:8:1:{_salt.hex()}:{_h.hex()}'
app_config = { app_config = {
"initial_manager_email": manager_email, "initial_manager_email": manager_email,
"credentials_key": credentials_key, "credentials_key": credentials_key,