Files
NetBirdMSP-Appliance/app/utils/security.py
twothatit 3d28f13054 Add TOTP-based Multi-Factor Authentication (MFA) for local users
Global MFA toggle in Security settings, QR code setup on first login,
6-digit TOTP verification on subsequent logins. Azure AD users exempt.
Admins can reset user MFA. TOTP secrets encrypted at rest with Fernet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:14:06 +01:00

132 lines
3.8 KiB
Python

"""Security utilities — password hashing (bcrypt), token encryption (Fernet), TOTP."""
import os
import secrets
import pyotp
from cryptography.fernet import Fernet
from passlib.context import CryptContext
# ---------------------------------------------------------------------------
# Password hashing (bcrypt)
# ---------------------------------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
"""Hash a plaintext password with bcrypt.
Args:
plain: The plaintext password.
Returns:
Bcrypt hash string.
"""
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
"""Verify a plaintext password against a bcrypt hash.
Args:
plain: The plaintext password to check.
hashed: The stored bcrypt hash.
Returns:
True if the password matches.
"""
return pwd_context.verify(plain, hashed)
# ---------------------------------------------------------------------------
# Fernet encryption for secrets (NPM token, relay secrets, etc.)
# ---------------------------------------------------------------------------
def _get_fernet() -> Fernet:
"""Derive a Fernet key from the application SECRET_KEY.
The SECRET_KEY from the environment is used as the basis. We pad/truncate
it to produce a valid 32-byte URL-safe-base64 key that Fernet requires.
"""
import base64
import hashlib
secret = os.environ.get("SECRET_KEY", "change-me-in-production")
# Derive a stable 32-byte key via SHA-256
key_bytes = hashlib.sha256(secret.encode()).digest()
fernet_key = base64.urlsafe_b64encode(key_bytes)
return Fernet(fernet_key)
def encrypt_value(plaintext: str) -> str:
"""Encrypt a string value with Fernet.
Args:
plaintext: Value to encrypt.
Returns:
Encrypted string (base64-encoded Fernet token).
"""
f = _get_fernet()
return f.encrypt(plaintext.encode()).decode()
def decrypt_value(ciphertext: str) -> str:
"""Decrypt a Fernet-encrypted string.
Args:
ciphertext: Encrypted value.
Returns:
Original plaintext string.
"""
f = _get_fernet()
return f.decrypt(ciphertext.encode()).decode()
def generate_relay_secret() -> str:
"""Generate a cryptographically secure relay secret.
Returns:
A 32-character hex string.
"""
return secrets.token_hex(16)
# ---------------------------------------------------------------------------
# TOTP (Time-based One-Time Password)
# ---------------------------------------------------------------------------
def generate_totp_secret() -> str:
"""Generate a new random TOTP secret (base32-encoded)."""
return pyotp.random_base32()
def verify_totp(secret: str, code: str) -> bool:
"""Verify a 6-digit TOTP code against a secret.
Allows a window of +/- 1 interval (30s) to account for clock drift.
"""
totp = pyotp.TOTP(secret)
return totp.verify(code, valid_window=1)
def generate_totp_uri(secret: str, username: str, issuer: str = "NetBird MSP") -> str:
"""Generate an otpauth:// URI for QR code generation."""
totp = pyotp.TOTP(secret)
return totp.provisioning_uri(name=username, issuer_name=issuer)
# ---------------------------------------------------------------------------
# Misc key generation
# ---------------------------------------------------------------------------
def generate_datastore_encryption_key() -> str:
"""Generate a base64-encoded 32-byte key for NetBird DataStoreEncryptionKey.
NetBird management (Go) expects standard base64 decoding to exactly 32 bytes.
Returns:
A standard base64-encoded string representing 32 random bytes.
"""
import base64
return base64.b64encode(secrets.token_bytes(32)).decode()