Powrót do strony głównej

Generowanie haseł Python: secrets zamiast random

Artykuł wyjaśnia, dlaczego moduł random jest nieodpowiedni do generowania haseł z powodu determinizmu Mersenne Twister. Porównanie z secrets, analiza entropii i nieudana próba ataku na seed. Zalecenia dotyczące bezpiecznego kodu.

Dlaczego random jest niebezpieczny dla haseł w Python: secrets w akcji
Advertisement 728x90

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ą:

Google AdInline article slot
  • 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:

Google AdInline article slot
  • 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):

Google AdInline article slot

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

Advertisement 728x90

Czytaj dalej