M-Bus Gateway
← Tilbage til blog
· 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.

Se JWT auth guide eller FastAPI multi-tenant guide.