Kryptograficzne bezpieczeństwo generowania haseł w Pythonie: random vs secrets
Entropia określa jakość hasła nie jako właściwość ciągu znaków, ale jako cechę procesu generowania. Wzór H = L × log₂(N) zakłada niezależny i równoprawdopodobny wybór każdego znaku z alfabetu o rozmiarze N przy długości L. Dla 8 małych liter (N=26): ~37,6 bitów. Dla pełnego ASCII (N=94): ~79 bitów.
Błędne przekonanie: zewnętrzna losowość hasła gwarantuje odporność. Dwa hasła, takie jak f9A$kL2pQzX1, mogą mieć tę samą formalną entropię, ale różne rzeczywiste bezpieczeństwo w zależności od generatora.
Moduł random: deterministyczny PRNG
Moduł random implementuje Mersenne Twister (MT19937) z wewnętrznym stanem ~19937 bitów. Każda wartość jest determinowana przez poprzednią. Ziarno (seed) domyślnie pobierane jest z os.urandom(), ale właściwości PRNG pozostają:
- Determinizm: jedno ziarno — jedna sekwencja.
- Odtwarzalność: stan można zrekonstruować z ~624 surowych 32-bitowych wyjść.
- Brak odporności kryptograficznej: nie jest odporny na ataki na stan.
Przykład typowego kodu:
import random
import string
alphabet = string.ascii_letters + string.digits
def generate_password(length=12):
return ''.join(random.choice(alphabet) for _ in range(length))
Hasło — to wycinek sekwencji PRNG. Jeśli stan jest znany, sekwencja jest przewidywalna.
Moduł secrets: bezpośredni dostęp do entropii systemowej
Secrets używa os.urandom() bezpośrednio dla każdego wywołania:
- secrets.randbelow(n) → os.urandom().
- random.SystemRandom() → os.urandom().
Brak ustalonego stanu, brak determinizmu. Każdy bajt czerpany jest ze źródeł sprzętowych: przerwania, szum sieciowy, ruchy.
Różnica jest kluczowa: random tworzy iluzję odporności, secrets ją zapewnia.
Praktyczny atak: odzyskiwanie ziarna i stanu
Eksperyment z 5 kolejnymi hasłami z GigaChat (długość 12, Python 3.9.18, czas 24.03.2026 11:11):
Hipoteza: ziarno = znacznik czasu serwera ±1 godzina.
from datetime import datetime, timedelta
base_time = datetime(2026, 3, 24, 11, 11, 0)
window_seconds = 3600
for offset in range(-window_seconds, window_seconds):
t = base_time + timedelta(seconds=offset)
seed = int(t.timestamp())
# Inicjalizacja random.seed(seed) i sprawdzenie
Wynik: brak dopasowań. os.urandom() dla ziarna uniemożliwia brute-force w czasie.
Rzeczywisty atak na MT19937 wymaga 624 surowych wartości. Hasła — to zniekształcone wyjście (choice → znak), tylko 60 wyborów za 5 haseł. Odtworzenie jest niemożliwe.
Granice ataku:
- Wymaga surowych wyjść PRNG.
- Przewidywalne ziarno (czas, PID).
- Wielokrotne użycie tego samego generatora.
- Znany algorytm.
Co jest ważne
- Entropia — to właściwość procesu, nie wyniku: sprawdzaj generator.
- random (Mersenne Twister) jest podatny na odzyskanie stanu przy wystarczających danych.
- secrets/os.urandom — jedyny wybór dla haseł, kluczy, tokenów.
- Iluzja losowości od random jest niebezpieczna w produkcji.
- Ataki na PRNG są realne tylko przy błędach w ziarnie lub wyciekach stanów.
Rekomendacje dla programistów middle/senior
Wdrażaj w kodzie:
import secrets
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
def secure_password(length=16):
return ''.join(secrets.choice(alphabet) for _ in range(length))
Unikaj:
- random do uwierzytelniania.
- Stałych ziaren.
- Ponownego użycia generatora.
Testuj: generuj 10⁶ haseł, sprawdzaj równomierny rozkład (chi²).
— Editorial Team
Brak komentarzy.