Generare una passphrase con Python
Una passphrase è una sequenza di parole casuali, separata da un carattere delimitatore, utilizzata come credenziale di autenticazione al posto di una password tradizionale. Rispetto a una password composta da caratteri arbitrari, una passphrase offre due vantaggi fondamentali: è più facile da memorizzare per un essere umano e, se costruita correttamente, è computazionalmente difficile da violare tramite attacchi a forza bruta o a dizionario.
Il modello di riferimento più noto è Diceware, proposto da Arnold Reinhold nel 1995, che associa ogni possibile combinazione di cinque lanci di dado a una parola tratta da un dizionario standardizzato. Python mette a disposizione gli strumenti necessari per replicare e migliorare questo approccio in modo programmatico, sfruttando un generatore di numeri casuali crittograficamente sicuro.
Entropia e sicurezza
Prima di scrivere codice, è utile capire il concetto di entropia applicato alle passphrase. L'entropia misura, in bit, la quantità di informazione casuale contenuta in una credenziale. Più alta è l'entropia, più resistente è la passphrase agli attacchi.
La formula è la seguente:
H = log2(N^L)
= L * log2(N)
dove N è la dimensione del vocabolario e L è il numero di parole. Con un vocabolario di 7776 parole (6^5, l'insieme Diceware completo) e una passphrase di sei parole si ottiene:
H = 6 * log2(7776) ≈ 6 * 12.92 ≈ 77.5 bit
Settantasette bit di entropia sono considerati sufficienti per la maggior parte degli scenari di sicurezza odierni. Per applicazioni ad alto rischio si raccomanda di portare la passphrase ad almeno 128 bit, aumentando il numero di parole o la dimensione del vocabolario.
Il modulo secrets
Python 3.6 ha introdotto il modulo secrets, progettato esplicitamente per la generazione di valori crittograficamente sicuri. A differenza del modulo random, che utilizza un generatore pseudocasuale deterministico (Mersenne Twister), secrets si appoggia alla sorgente di entropia del sistema operativo (/dev/urandom su Linux e macOS, CryptGenRandom su Windows). Questo lo rende adatto alla generazione di token, password e passphrase.
La funzione chiave per i nostri scopi è secrets.choice(sequence), che restituisce un elemento estratto in modo sicuro da una sequenza.
import secrets
# Lista di parole di esempio
word_list = ["apple", "bridge", "cloud", "dragon", "echo"]
# Estrazione crittograficamente sicura
word = secrets.choice(word_list)
print(word)
Costruire un vocabolario
La qualità della passphrase dipende in larga misura dalla qualità del vocabolario. Esistono diverse strategie per costruirlo.
Caricare il wordlist EFF
La Electronic Frontier Foundation ha pubblicato un wordlist largo composto da 7776 parole inglesi comuni, facili da pronunciare e da ricordare. Il file è in formato testo con una coppia per riga: il codice a dado e la parola corrispondente.
import secrets
from pathlib import Path
def load_wordlist(file_path: str) -> list[str]:
"""Carica il wordlist EFF da un file di testo."""
words = []
path = Path(file_path)
with path.open(encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# Ogni riga ha il formato: "11111\tword"
parts = line.split("\t")
if len(parts) == 2:
words.append(parts[1].strip())
return words
# Utilizzo
wordlist = load_wordlist("eff_large_wordlist.txt")
print(f"Parole caricate: {len(wordlist)}")
Usare il dizionario di sistema
Su sistemi Unix il file /usr/share/dict/words contiene decine di migliaia di parole. È una soluzione rapida per i test, ma il file include forme flesse, abbreviazioni e termini tecnici che possono rendere la passphrase difficile da memorizzare.
from pathlib import Path
def load_system_dictionary(min_length: int = 4, max_length: int = 8) -> list[str]:
"""Carica e filtra il dizionario di sistema per lunghezza delle parole."""
dict_path = Path("/usr/share/dict/words")
words = [
line.strip().lower()
for line in dict_path.read_text(encoding="utf-8").splitlines()
# Mantieni solo parole alfabetiche entro i limiti di lunghezza
if line.strip().isalpha()
and min_length <= len(line.strip()) <= max_length
]
return words
Costruire un vocabolario da una stringa in memoria
Per esempi autonomi e test unitari è comodo definire il vocabolario direttamente nel codice.
SAMPLE_WORDLIST: list[str] = [
"correct", "horse", "battery", "staple", "apple", "bridge",
"cloud", "dragon", "echo", "forest", "granite", "harbor",
"island", "jungle", "kernel", "lantern", "marble", "nectar",
"orbit", "pebble", "quartz", "river", "stone", "timber",
"umbrella", "valley", "walnut", "xenon", "yellow", "zenith",
]
Generare la passphrase
Con un vocabolario disponibile, la logica di generazione è diretta: si estraggono num_words parole in modo indipendente e si uniscono con un separatore.
import secrets
import math
def generate_passphrase(
wordlist: list[str],
num_words: int = 6,
separator: str = "-",
) -> str:
"""
Genera una passphrase crittograficamente sicura.
Args:
wordlist: Lista di parole candidate.
num_words: Numero di parole nella passphrase.
separator: Carattere di separazione tra le parole.
Returns:
La passphrase come stringa.
"""
if len(wordlist) < 2:
raise ValueError("Il vocabolario deve contenere almeno due parole.")
if num_words < 1:
raise ValueError("Il numero di parole deve essere almeno 1.")
# Estrazione sicura di num_words parole
chosen_words = [secrets.choice(wordlist) for _ in range(num_words)]
return separator.join(chosen_words)
def compute_entropy(wordlist_size: int, num_words: int) -> float:
"""Calcola l'entropia in bit della passphrase."""
return num_words * math.log2(wordlist_size)
# Esempio di utilizzo
passphrase = generate_passphrase(SAMPLE_WORDLIST, num_words=6)
entropy = compute_entropy(len(SAMPLE_WORDLIST), 6)
print(f"Passphrase: {passphrase}")
print(f"Entropia: {entropy:.2f} bit")
Classe PassphraseGenerator
Per un'applicazione reale conviene incapsulare la logica in una classe, separando la configurazione dalla generazione e aggiungendo funzionalità come la verifica dell'entropia minima.
import secrets
import math
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class PassphraseConfig:
"""Configurazione del generatore di passphrase."""
num_words: int = 6
separator: str = "-"
min_entropy_bits: float = 70.0
capitalize: bool = False
class PassphraseGenerator:
"""Generatore di passphrase crittograficamente sicure."""
def __init__(self, wordlist: list[str], config: PassphraseConfig | None = None):
if len(wordlist) < 2:
raise ValueError("Il vocabolario è troppo piccolo.")
# Rimozione dei duplicati preservando l'ordine
self._wordlist: list[str] = list(dict.fromkeys(wordlist))
self._config: PassphraseConfig = config or PassphraseConfig()
@classmethod
def from_file(
cls,
file_path: str | Path,
config: PassphraseConfig | None = None,
) -> "PassphraseGenerator":
"""Costruisce il generatore caricando le parole da un file di testo."""
path = Path(file_path)
words = [
line.strip().lower()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip().isalpha()
]
return cls(words, config)
@property
def vocabulary_size(self) -> int:
"""Dimensione effettiva del vocabolario."""
return len(self._wordlist)
def entropy(self, num_words: int | None = None) -> float:
"""Entropia in bit per un dato numero di parole."""
n = num_words if num_words is not None else self._config.num_words
return n * math.log2(self.vocabulary_size)
def generate(self, num_words: int | None = None) -> str:
"""
Genera una passphrase.
Args:
num_words: Sovrascrive il valore di configurazione se fornito.
Returns:
La passphrase generata.
Raises:
ValueError: Se l'entropia risultante è inferiore al minimo configurato.
"""
n = num_words if num_words is not None else self._config.num_words
computed_entropy = self.entropy(n)
if computed_entropy < self._config.min_entropy_bits:
raise ValueError(
f"Entropia insufficiente: {computed_entropy:.2f} bit "
f"(minimo richiesto: {self._config.min_entropy_bits:.2f} bit). "
f"Aumenta il numero di parole o la dimensione del vocabolario."
)
words = [secrets.choice(self._wordlist) for _ in range(n)]
if self._config.capitalize:
# Prima lettera maiuscola per ogni parola
words = [w.capitalize() for w in words]
return self._config.separator.join(words)
def generate_batch(self, count: int, num_words: int | None = None) -> list[str]:
"""Genera un insieme di passphrase distinte."""
passphrases = set()
# Limite di sicurezza per evitare loop infiniti
max_attempts = count * 10
attempts = 0
while len(passphrases) < count and attempts < max_attempts:
passphrases.add(self.generate(num_words))
attempts += 1
return list(passphrases)
Aggiungere un numero o un simbolo
Alcuni sistemi richiedono che la password contenga almeno un carattere numerico o un simbolo speciale. È possibile soddisfare questo requisito inserendo un elemento aggiuntivo in una posizione casuale della sequenza di parole, senza degradare la leggibilità della passphrase.
import secrets
import string
def insert_random_element(
words: list[str],
element_type: str = "digit",
) -> list[str]:
"""
Inserisce un carattere casuale (cifra o simbolo) nella lista di parole.
Args:
words: Lista di parole della passphrase.
element_type: "digit" per una cifra, "symbol" per un simbolo.
Returns:
Lista di parole con l'elemento inserito.
"""
if element_type == "digit":
# Cifra casuale tra 0 e 9
extra = secrets.choice(string.digits)
elif element_type == "symbol":
# Simbolo da un sottoinsieme sicuro (esclude caratteri ambigui)
safe_symbols = "!@#$%^&*"
extra = secrets.choice(safe_symbols)
else:
raise ValueError(f"Tipo non supportato: {element_type}")
# Posizione casuale nella lista
position = secrets.randbelow(len(words) + 1)
result = words.copy()
result.insert(position, extra)
return result
# Esempio
base_words = ["correct", "horse", "battery", "staple"]
words_with_digit = insert_random_element(base_words, "digit")
passphrase = "-".join(words_with_digit)
print(passphrase) # es: "correct-horse-7-battery-staple"
Verifica interattiva dell'entropia
È utile fornire all'utente un feedback visivo sulla forza della passphrase. La funzione seguente classifica l'entropia in livelli e restituisce una valutazione leggibile.
import math
def rate_passphrase_strength(wordlist_size: int, num_words: int) -> dict:
"""
Valuta la forza di una passphrase in base all'entropia.
Returns:
Dizionario con entropia, livello e descrizione.
"""
entropy_bits = num_words * math.log2(wordlist_size)
if entropy_bits < 40:
level = "debole"
description = "Adatta solo per test. Non usare in produzione."
elif entropy_bits < 60:
level = "accettabile"
description = "Sufficiente per account a basso rischio."
elif entropy_bits < 80:
level = "buona"
description = "Adatta per la maggior parte degli account personali."
elif entropy_bits < 100:
level = "ottima"
description = "Adatta per account ad alto valore."
else:
level = "eccellente"
description = "Adatta per applicazioni crittografiche critiche."
return {
"entropy_bits": round(entropy_bits, 2),
"level": level,
"description": description,
}
# Esempio
rating = rate_passphrase_strength(7776, 6)
print(f"Entropia: {rating['entropy_bits']} bit")
print(f"Livello: {rating['level']}")
print(f"Nota: {rating['description']}")
Interfaccia a riga di comando
Combinando tutti i componenti precedenti si può costruire uno script eseguibile da terminale, con argomenti configurabili tramite argparse.
#!/usr/bin/env python3
"""
Generatore di passphrase crittograficamente sicure.
Utilizzo: python passphrase_generator.py --words 6 --count 3 --separator " "
"""
import argparse
import math
import secrets
import string
import sys
from pathlib import Path
DEFAULT_WORDLIST: list[str] = [
"abandon", "ability", "able", "about", "above", "absent",
"absorb", "abstract", "absurd", "abuse", "access", "accident",
"account", "accuse", "achieve", "acid", "acoustic", "acquire",
"across", "action", "actor", "actual", "adapt", "address",
"adjust", "admit", "adult", "advance", "advice", "aerobic",
"afford", "afraid", "again", "agent", "agree", "ahead",
"album", "alcohol", "alert", "alien", "align", "alive",
"alley", "allow", "almost", "alone", "alpha", "already",
"alter", "always", "amateur", "amazing", "among", "amount",
"amused", "analyst", "anchor", "ancient", "anger", "angle",
"animal", "ankle", "answer", "antenna", "antique", "anxiety",
"apart", "apology", "appear", "apple", "approve", "april",
"arcade", "arctic", "arena", "argue", "around", "arrest",
"arrive", "arrow", "artist", "aspect", "assault", "asset",
"assist", "assume", "asthma", "athlete", "atom", "attack",
"attend", "attitude", "attract", "auction", "audit", "autumn",
"average", "avocado", "avoid", "awake", "aware", "awesome",
]
def load_words_from_file(path: Path) -> list[str]:
"""Carica le parole da un file, una per riga."""
return [
line.strip().lower()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip().isalpha()
]
def generate_passphrase(
wordlist: list[str],
num_words: int,
separator: str,
add_digit: bool = False,
capitalize: bool = False,
) -> str:
"""Genera una singola passphrase con le opzioni specificate."""
words = [secrets.choice(wordlist) for _ in range(num_words)]
if capitalize:
words = [w.capitalize() for w in words]
if add_digit:
# Inserisce una cifra in posizione casuale
digit = secrets.choice(string.digits)
pos = secrets.randbelow(len(words) + 1)
words.insert(pos, digit)
return separator.join(words)
def build_argument_parser() -> argparse.ArgumentParser:
"""Costruisce il parser degli argomenti da riga di comando."""
parser = argparse.ArgumentParser(
description="Genera passphrase crittograficamente sicure.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--words", "-w",
type=int,
default=6,
help="Numero di parole per passphrase (default: 6).",
)
parser.add_argument(
"--count", "-c",
type=int,
default=1,
help="Numero di passphrase da generare (default: 1).",
)
parser.add_argument(
"--separator", "-s",
type=str,
default="-",
help="Separatore tra le parole (default: '-').",
)
parser.add_argument(
"--wordlist", "-f",
type=Path,
default=None,
help="Percorso al file wordlist (default: vocabolario interno).",
)
parser.add_argument(
"--digit",
action="store_true",
help="Aggiunge una cifra casuale nella passphrase.",
)
parser.add_argument(
"--capitalize",
action="store_true",
help="Porta ogni parola in maiuscolo iniziale.",
)
parser.add_argument(
"--entropy",
action="store_true",
help="Mostra l'entropia stimata della passphrase.",
)
return parser
def main() -> None:
"""Punto di ingresso principale dello script."""
parser = build_argument_parser()
args = parser.parse_args()
# Caricamento del vocabolario
if args.wordlist:
if not args.wordlist.exists():
print(f"Errore: file non trovato: {args.wordlist}", file=sys.stderr)
sys.exit(1)
wordlist = load_words_from_file(args.wordlist)
else:
wordlist = DEFAULT_WORDLIST
if len(wordlist) < 2:
print("Errore: vocabolario insufficiente.", file=sys.stderr)
sys.exit(1)
# Calcolo dell'entropia prima della generazione
entropy_bits = args.words * math.log2(len(wordlist))
if args.entropy:
print(f"Vocabolario: {len(wordlist)} parole")
print(f"Entropia: {entropy_bits:.2f} bit")
print()
# Generazione delle passphrase
for _ in range(args.count):
phrase = generate_passphrase(
wordlist=wordlist,
num_words=args.words,
separator=args.separator,
add_digit=args.digit,
capitalize=args.capitalize,
)
print(phrase)
if __name__ == "__main__":
main()
Alcuni esempi di utilizzo da terminale:
# Genera una passphrase di 6 parole con separatore trattino
python passphrase_generator.py
# Genera 5 passphrase di 8 parole con spazio come separatore
python passphrase_generator.py --words 8 --count 5 --separator " "
# Usa un wordlist esterno e mostra l'entropia
python passphrase_generator.py --wordlist eff_large_wordlist.txt --entropy
# Aggiunge una cifra e usa maiuscole iniziali
python passphrase_generator.py --digit --capitalize
Test unitari con unittest
La correttezza del generatore si verifica con una suite di test che controlla le invarianti fondamentali: lunghezza dell'output, uso esclusivo delle parole del vocabolario, corretta gestione degli errori.
import unittest
import math
class TestPassphraseGenerator(unittest.TestCase):
"""Test unitari per la classe PassphraseGenerator."""
def setUp(self):
"""Prepara un'istanza del generatore prima di ogni test."""
self.wordlist = [
"alpha", "bravo", "charlie", "delta", "echo",
"foxtrot", "golf", "hotel", "india", "juliet",
"kilo", "lima", "mike", "november", "oscar",
"papa", "quebec", "romeo", "sierra", "tango",
]
self.config = PassphraseConfig(
num_words=4,
separator="-",
min_entropy_bits=0.0, # Nessun limite per i test
capitalize=False,
)
self.generator = PassphraseGenerator(self.wordlist, self.config)
def test_word_count(self):
"""La passphrase deve contenere esattamente num_words parole."""
passphrase = self.generator.generate()
parts = passphrase.split("-")
self.assertEqual(len(parts), 4)
def test_all_words_from_vocabulary(self):
"""Ogni parola della passphrase deve appartenere al vocabolario."""
passphrase = self.generator.generate()
for word in passphrase.split("-"):
self.assertIn(word, self.wordlist)
def test_custom_separator(self):
"""Il separatore configurato deve essere usato correttamente."""
config = PassphraseConfig(num_words=3, separator=".", min_entropy_bits=0.0)
generator = PassphraseGenerator(self.wordlist, config)
passphrase = generator.generate()
self.assertIn(".", passphrase)
self.assertEqual(len(passphrase.split(".")), 3)
def test_capitalize_option(self):
"""Con capitalize=True ogni parola inizia con la maiuscola."""
config = PassphraseConfig(
num_words=4, capitalize=True, min_entropy_bits=0.0
)
generator = PassphraseGenerator(self.wordlist, config)
passphrase = generator.generate()
for word in passphrase.split("-"):
self.assertTrue(word[0].isupper(), f"La parola '{word}' non inizia con maiuscola.")
def test_entropy_calculation(self):
"""L'entropia calcolata deve corrispondere alla formula attesa."""
expected = 4 * math.log2(len(self.wordlist))
self.assertAlmostEqual(self.generator.entropy(), expected, places=5)
def test_minimum_entropy_enforcement(self):
"""Deve sollevare ValueError se l'entropia scende sotto il minimo."""
config = PassphraseConfig(num_words=1, min_entropy_bits=100.0)
generator = PassphraseGenerator(self.wordlist, config)
with self.assertRaises(ValueError):
generator.generate()
def test_empty_vocabulary_raises(self):
"""Un vocabolario con meno di due parole deve sollevare ValueError."""
with self.assertRaises(ValueError):
PassphraseGenerator(["solo"])
def test_batch_generation(self):
"""La generazione batch deve restituire il numero di passphrase richiesto."""
batch = self.generator.generate_batch(5)
self.assertEqual(len(batch), 5)
def test_batch_uniqueness(self):
"""Le passphrase nel batch devono essere uniche."""
batch = self.generator.generate_batch(5)
self.assertEqual(len(batch), len(set(batch)))
if __name__ == "__main__":
unittest.main()
Considerazioni sulla sicurezza
Alcune note importanti per un utilizzo corretto del generatore:
Non usare random al posto di secrets. Il modulo random non è adatto a scopi crittografici. La funzione random.seed() accetta un seme deterministico: un attaccante che conosce il seme (ad esempio l'ora di sistema al momento della generazione) può riprodurre esattamente la sequenza di parole estratte.
Evitare vocabolari troppo piccoli. Un vocabolario di cento parole fornisce solo log2(100) ≈ 6.6 bit per parola. Con sei parole si ottengono appena 39.8 bit di entropia, insufficienti contro un attacco offline moderno. Il vocabolario EFF da 7776 parole rimane la scelta consigliata.
Non trasmettere la passphrase in chiaro. Dopo la generazione, la passphrase deve essere consegnata all'utente attraverso un canale cifrato (HTTPS, TLS) e non deve essere registrata in log o database senza un adeguato hashing (bcrypt, Argon2, scrypt).
Non riutilizzare la passphrase. Ogni servizio dovrebbe ricevere una passphrase distinta. Un gestore di password (ad esempio KeePass, Bitwarden) è lo strumento adatto per conservarle in modo sicuro.
Conclusione
Python offre tutti gli strumenti necessari per costruire un generatore di passphrase sicuro e flessibile. Il modulo secrets garantisce la casualità crittografica, mentre la struttura a classe permette di adattare facilmente il generatore a requisiti diversi: numero di parole, separatori, vocabolari personalizzati, requisiti di entropia minima. Con un vocabolario di dimensioni adeguate e almeno sei parole, si ottiene una passphrase che combina sicurezza computazionale e memorabilità, risultando superiore alle password tradizionali sia dal punto di vista della sicurezza sia da quello dell'usabilità.