· bcrypt· password· hashing· sikkerhed· Python· FastAPI· argon2· kryptografi· autentificering
bcrypt password hashing i Python — implementering og sikkerhed
bcrypt til password hashing i Python: work factor valg, timing attack, hash-upgrade ved login, argon2id-migration, integration med FastAPI og fejlmønstre.
Af M-Bus Gateway
M-Bus Gateway platformen bruger bcrypt til password hashing med work factor 12. Her er implementeringen og de sikkerhedsmæssige overvejelser.
Hvorfor bcrypt frem for SHA-256?
SHA-256 (ALDRIG til passwords):
→ Designet til at være HURTIG (kryptografisk hashing)
→ Kan hashe 10+ millioner passwords/sekund på moderne GPU
→ Et bruteforce-angreb på 8-char password: minutter
bcrypt (korrekt til passwords):
→ Designet til at være LANGSOM (adaptive work factor)
→ Work factor 12 = ~0.3 sekunder pr. hash
→ GPU-resistent (sekventiel algoritme)
→ Samme GPU: 1.000 passwords/sekund = 1000× sværere
Alternativ: argon2id (endnu bedre, anbefales til nye systemer):
→ Memory-hard (kræver RAM → GPU-angreb endnu sværere)
→ OWASP anbefaling 2024
→ Python: `pip install argon2-cffi`
bcrypt implementering
# server/src/auth/password.py
import bcrypt
import secrets
import time
# Work factor: Vælg højeste der holder responstid < 1 sek:
WORK_FACTOR = 12
def hash_password(password: str) -> str:
"""
Hash password med bcrypt.
Returnerer string (inkluderer salt og work factor).
"""
password_bytes = password.encode("utf-8")
# bcrypt.gensalt() genererer cryptographically random salt:
salt = bcrypt.gensalt(rounds=WORK_FACTOR)
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verificer password. Constant-time comparison (ingen timing leak).
bcrypt.checkpw er allerede constant-time.
"""
try:
return bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
)
except Exception:
# Ugyldig hash-format → False (ikke exception)
return False
def needs_rehash(hashed_password: str, current_rounds: int = WORK_FACTOR) -> bool:
"""
Tjek om hash er lavet med lavere work factor end nuværende.
Bruges til hash-upgrade ved login.
"""
try:
# bcrypt-hashen indeholder rounds: $2b$12$...
rounds = bcrypt.checkpw.__module__ # Hacky men virker
# Brug bcrypt.rounds() i nyere versioner:
stored_rounds = int(hashed_password.split("$")[2])
return stored_rounds < current_rounds
except Exception:
return True # Kan ikke parse → rehash for en sikkerheds skyld
FastAPI login med timing-sikker verificering
# server/src/auth/router.py
import time
import bcrypt
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/auth", tags=["auth"])
FAKE_HASH = "$2b$12$fakehashfakehashfakehashfakehashfakehashfakehash"
@router.post("/login")
async def login(
request: LoginRequest,
session: AsyncSession = Depends(get_session),
):
user = await get_user_by_email(request.email, session)
if not user:
# KRITISK: Verificer mod fake hash for at undgå timing attack.
# Uden dette: Angriber kan måle responstid og se om email eksisterer.
bcrypt.checkpw(request.password.encode(), FAKE_HASH.encode())
raise HTTPException(401, "Ugyldige legitimationsoplysninger")
if user.deleted_at:
bcrypt.checkpw(request.password.encode(), FAKE_HASH.encode())
raise HTTPException(401, "Ugyldige legitimationsoplysninger")
# Verificer password:
password_valid = bcrypt.checkpw(
request.password.encode("utf-8"),
user.password_hash.encode("utf-8"),
)
if not password_valid:
raise HTTPException(401, "Ugyldige legitimationsoplysninger")
# Hash-upgrade: Rehash ved login hvis work factor er steget:
if needs_rehash(user.password_hash):
user.password_hash = hash_password(request.password)
session.add(user)
await session.commit()
# Generer tokens:
access_token = create_access_token(user_id=str(user.id), role=user.role, ...)
return {"access_token": access_token}
Password-validering
# server/src/auth/validation.py
import re
class PasswordPolicy:
"""
BEK-kompatible krav (ikke lovpligtige, men good practice).
Tilpas til eget risikoniveau.
"""
MIN_LENGTH = 10
REQUIRE_UPPER = True
REQUIRE_DIGIT = True
REQUIRE_SPECIAL = True
@classmethod
def validate(cls, password: str) -> list[str]:
"""Returnerer liste af fejl. Tom liste = OK."""
errors = []
if len(password) < cls.MIN_LENGTH:
errors.append(f"Adgangskode skal være mindst {cls.MIN_LENGTH} tegn")
if cls.REQUIRE_UPPER and not re.search(r"[A-Z]", password):
errors.append("Adgangskode skal indeholde mindst ét stort bogstav")
if cls.REQUIRE_DIGIT and not re.search(r"\d", password):
errors.append("Adgangskode skal indeholde mindst ét tal")
if cls.REQUIRE_SPECIAL and not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
errors.append("Adgangskode skal indeholde mindst ét specialtegn")
return errors
Work factor valg: Benchmark
# Kør dette på produktionsserveren for at vælge work factor:
import bcrypt
import time
for rounds in range(10, 15):
start = time.time()
bcrypt.hashpw(b"test-password", bcrypt.gensalt(rounds=rounds))
elapsed = time.time() - start
print(f"rounds={rounds}: {elapsed:.3f}s")
# Typisk output (Hetzner CX32 — 4 vCPU):
# rounds=10: 0.038s
# rounds=11: 0.074s
# rounds=12: 0.149s ← Valgt (under 200 ms)
# rounds=13: 0.298s
# rounds=14: 0.596s
# Regel: Vælg højeste rounds der holder < 200-300 ms
# Husk: API-timeout skal matche (FastAPI default er 60s)
Migration til argon2id (fremtid)
# Gradvis migration via hash-upgrade-mønster:
# Eksisterende bcrypt-hashes bevares
# Nye passwords hashet med argon2id
# bcrypt-hashes opgraderes til argon2id ved næste login
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
def hash_password_v2(password: str) -> str:
return ph.hash(password)
def verify_password_v2(password: str, hashed: str) -> bool:
try:
return ph.verify(hashed, password)
except VerifyMismatchError:
return False
def is_bcrypt_hash(hashed: str) -> bool:
return hashed.startswith("$2b$") or hashed.startswith("$2a$")
Konklusion
bcrypt med work factor 12 er tilstrækkelig for de fleste SaaS-platforme i 2026. Det vigtigste anti-mønster at undgå: timing attack ved bruger-opslag (løst med fake hash ved ukendt email). Hash-upgrade ved login er gratis sikkerhedsforbedring der gradvis moderniserer alle hash-parametre i produktion.