Génération de mots de passe cryptographiquement sûrs en Python : random vs secrets
L'entropie définit la qualité d'un mot de passe non pas comme une propriété de la chaîne elle-même, mais comme une caractéristique du processus de génération. La formule H = L × log₂(N) suppose un choix indépendant et équiprobable de chaque caractère dans un alphabet de taille N sur une longueur L. Pour 8 lettres minuscules (N=26) : ~37,6 bits. Pour l'ASCII complet (N=94) : ~79 bits.
Une idée reçue courante : l'apparente aléatoire d'un mot de passe garantit sa robustesse. Deux mots de passe comme f9A$kL2pQzX1 peuvent avoir la même entropie formelle mais une sécurité réelle très différente selon le générateur utilisé.
Le module random : un PRNG déterministe
Le module random implémente le Mersenne Twister (MT19937) avec un état interne d'environ 19937 bits. Chaque valeur est déterministe par rapport à la précédente. La graine par défaut est tirée de os.urandom(), mais les propriétés inhérentes du PRNG restent :
- Déterminisme : une graine produit une séquence prévisible.
- Récupérabilité : l'état peut être reconstruit à partir d'environ 624 sorties brutes de 32 bits.
- Absence de sécurité cryptographique : non résistant aux attaques de récupération d'état.
Exemple de code typique :
import random
import string
alphabet = string.ascii_letters + string.digits
def generate_password(length=12):
return ''.join(random.choice(alphabet) for _ in range(length))
Le mot de passe est une tranche de la séquence du PRNG. Si l'état est connu, la séquence est prévisible.
Le module secrets : accès direct à l'entropie système
Secrets utilise os.urandom() directement pour chaque appel :
- secrets.randbelow(n) → os.urandom().
- random.SystemRandom() → os.urandom().
Il n'y a pas d'état fixe, pas de déterminisme. Chaque octet est tiré de sources matérielles : interruptions, bruit réseau, mouvements de souris.
La différence est cruciale : random crée une illusion de sécurité, secrets la fournit.
Attaque pratique : récupération de la graine et de l'état
Expérience avec 5 mots de passe consécutifs de GigaChat (longueur 12, Python 3.9.18, date 24.03.2026 11:11) :
Hypothèse : graine = horodatage serveur ±1 heure.
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())
# Initialiser random.seed(seed) et vérifier
Résultat : aucune correspondance. L'utilisation de os.urandom() pour la graine défait les attaques par force brute basées sur le temps.
Une attaque réelle sur MT19937 nécessite 624 valeurs brutes. Les mots de passe sont une sortie déformée (choix → caractère), totalisant seulement 60 choix pour 5 mots de passe. La récupération est impossible.
Limites de l'attaque :
- Nécessite des sorties brutes du PRNG.
- Graine prévisible (temps, PID).
- Utilisation répétée du même générateur.
- Algorithme connu.
Points clés
- L'entropie est une propriété du processus, pas du résultat : vérifiez toujours votre générateur.
- random (Mersenne Twister) est vulnérable à la récupération d'état avec suffisamment de données.
- secrets/os.urandom est le seul choix pour les mots de passe, clés et jetons.
- L'illusion d'aléatoire de random est dangereuse en production.
- Les attaques sur PRNG ne sont réalistes qu'avec des erreurs de graine ou des fuites d'état.
Recommandations pour développeurs intermédiaires/seniors
Implémentez dans votre code :
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))
À éviter :
- Utiliser random pour l'authentification.
- Les graines fixes.
- La réutilisation de générateurs.
Test : générez 10⁶ mots de passe, vérifiez la distribution uniforme (chi²).
— Editorial Team
Aucun commentaire pour le moment.