diff --git a/docker/routlin-dash/app/pages/accountcreate/action.py b/docker/routlin-dash/app/pages/accountcreate/action.py index edae1eb..02d3639 100644 --- a/docker/routlin-dash/app/pages/accountcreate/action.py +++ b/docker/routlin-dash/app/pages/accountcreate/action.py @@ -1,6 +1,7 @@ from pathlib import Path from flask import Blueprint, request, session, redirect, flash -import bcrypt, secrets, smtplib +import secrets, smtplib +import settings import time from email.message import EmailMessage import auth @@ -78,8 +79,7 @@ def form_create(): flash('This account is already set up. Please log in instead.', 'error') return redirect(f'/{_PAGE}') - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') + hashed = settings.hash_password(password) code = f'{secrets.randbelow(1000000):06d}' try: diff --git a/docker/routlin-dash/app/pages/accountlogin/action.py b/docker/routlin-dash/app/pages/accountlogin/action.py index f79ea25..8ac35b0 100644 --- a/docker/routlin-dash/app/pages/accountlogin/action.py +++ b/docker/routlin-dash/app/pages/accountlogin/action.py @@ -1,6 +1,5 @@ from pathlib import Path from flask import Blueprint, request, session, redirect, flash -import bcrypt import auth import config_utils import sanitize @@ -29,7 +28,7 @@ def form_login(): if email != settings.get_initial_manager_email() or not stored_hash: flash('Invalid email address or password.', 'error') 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') return redirect(f'/{_PAGE}') 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') 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') return redirect(f'/{_PAGE}') diff --git a/docker/routlin-dash/app/pages/clientcredentials/action.py b/docker/routlin-dash/app/pages/clientcredentials/action.py index fd3dbd8..7c1150d 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/action.py +++ b/docker/routlin-dash/app/pages/clientcredentials/action.py @@ -1,7 +1,6 @@ import sqlite3 import time -import bcrypt from cryptography.fernet import Fernet from flask import Blueprint, request, redirect, flash import auth @@ -19,12 +18,12 @@ USER_TYPE_CAPTIVE = 0 USER_TYPE_SUPPLICANT = 1 DIGEST_CYPHERTEXT_FERNET = 0 -DIGEST_HASH_BCRYPT = 2 +DIGEST_HASH_SCRYPT = 2 VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT} HASH_FOR_USER_TYPE = { - USER_TYPE_CAPTIVE: DIGEST_HASH_BCRYPT, + USER_TYPE_CAPTIVE: DIGEST_HASH_SCRYPT, USER_TYPE_SUPPLICANT: DIGEST_CYPHERTEXT_FERNET, } @@ -99,8 +98,8 @@ def _get_by_index(conn, row_index): def _hash_password(plaintext, digest_type): if digest_type == DIGEST_CYPHERTEXT_FERNET: return encrypt_password(plaintext) - if digest_type == DIGEST_HASH_BCRYPT: - return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode() + if digest_type == DIGEST_HASH_SCRYPT: + return settings.hash_password(plaintext) raise ValueError(f"Unknown digest_type: {digest_type}") diff --git a/docker/routlin-dash/app/pages/preferences/action.py b/docker/routlin-dash/app/pages/preferences/action.py index f204a17..8be7185 100644 --- a/docker/routlin-dash/app/pages/preferences/action.py +++ b/docker/routlin-dash/app/pages/preferences/action.py @@ -1,6 +1,5 @@ from pathlib import Path from flask import Blueprint, request, session, redirect, flash -import bcrypt import auth import config_utils import sanitize @@ -131,11 +130,11 @@ def changepassword_save(): flash('New password must be at least 8 characters.', 'error') 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(): 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') return redirect(f'/{_PAGE}') try: @@ -156,7 +155,7 @@ def changepassword_save(): flash('Account not found. Please log in again.', 'error') 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') return redirect(f'/{_PAGE}') diff --git a/docker/routlin-dash/app/settings.py b/docker/routlin-dash/app/settings.py index 404a110..aa7e6e0 100644 --- a/docker/routlin-dash/app/settings.py +++ b/docker/routlin-dash/app/settings.py @@ -100,6 +100,28 @@ def get_credentials_key(): 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(): """Return SMTP settings from app_config.json, falling back to env vars.""" cfg = _load_app_config() diff --git a/docker/routlin-dash/requirements.txt b/docker/routlin-dash/requirements.txt index b124ba9..0a694a1 100644 --- a/docker/routlin-dash/requirements.txt +++ b/docker/routlin-dash/requirements.txt @@ -1,5 +1,4 @@ flask -bcrypt cryptography manuf mac-vendor-lookup diff --git a/docker/routlin-portal/app/page.py b/docker/routlin-portal/app/page.py index ea52e53..0616ad7 100644 --- a/docker/routlin-portal/app/page.py +++ b/docker/routlin-portal/app/page.py @@ -1,14 +1,29 @@ +import hashlib +import hmac import ipaddress import sqlite3 import time -import bcrypt from flask import Blueprint, request, redirect 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 -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__) @@ -85,11 +100,8 @@ def _verify_credential(username, password, vlan_name): return False if row['expires_seconds'] > 0 and (row['date_set'] + row['expires_seconds']) < now: return False - if row['digest_type'] == DIGEST_HASH_BCRYPT: - try: - return bcrypt.checkpw(password.encode(), row['password'].encode()) - except Exception: - return False + if row['digest_type'] == DIGEST_HASH_SCRYPT: + return _verify_scrypt(password, row['password']) return False diff --git a/docker/routlin-portal/requirements.txt b/docker/routlin-portal/requirements.txt index bb2793b..7e10602 100644 --- a/docker/routlin-portal/requirements.txt +++ b/docker/routlin-portal/requirements.txt @@ -1,2 +1 @@ flask -bcrypt diff --git a/routlin/install.py b/routlin/install.py index 059dffc..fff6af3 100644 --- a/routlin/install.py +++ b/routlin/install.py @@ -373,7 +373,7 @@ def setup_docker_compose(reuse_config=False): print() while True: 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: ") if pw != pw2: 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.") continue break - import bcrypt as _bcrypt - pw_hash = _bcrypt.hashpw(pw.encode('utf-8'), _bcrypt.gensalt()).decode('utf-8') + import hashlib as _hashlib, os as _os + _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 = { "initial_manager_email": manager_email, "credentials_key": credentials_key,