Calcolare l'entropia di una password in PHP
L'entropia di una password è una misura quantitativa della sua imprevedibilità, espressa in bit. Si tratta di un concetto mutuato dalla teoria dell'informazione di Claude Shannon che permette di stimare quanto sia difficile, per un attaccante, indovinare una password tramite un attacco a forza bruta. In questo articolo vediamo come calcolare l'entropia di una password in PHP, partendo dalla formula matematica di base e arrivando a una classe completa che tiene conto anche di fattori più sofisticati come pattern ripetuti, parole comuni e sequenze di tastiera.
La formula matematica di base
L'entropia H di una password viene calcolata in base alla dimensione dell'alfabeto utilizzato (lo spazio dei simboli possibili) e alla lunghezza della password stessa. La formula classica è:
H = L × log2(N)
dove L è la lunghezza della password e N è la cardinalità dell'alfabeto. Ad esempio, una password di 8 caratteri composta da sole lettere minuscole utilizza un alfabeto di 26 simboli e ha quindi un'entropia pari a 8 × log2(26) ≈ 37,6 bit. Aumentando la lunghezza o ampliando l'insieme dei caratteri possibili (lettere maiuscole, numeri, simboli), l'entropia cresce in modo significativo, rendendo la password esponenzialmente più resistente agli attacchi.
Determinare la dimensione dell'alfabeto
Il primo passo consiste nell'analizzare la password per determinare quali categorie di caratteri sono presenti. Le categorie tipiche sono lettere minuscole (26 simboli), lettere maiuscole (26 simboli), cifre numeriche (10 simboli) e simboli speciali (in genere 32 o 33). Implementiamo una funzione che, data una password in input, restituisca la dimensione complessiva dell'alfabeto utilizzato.
<?php
function getCharsetSize(string $password): int
{
$size = 0;
// Verifica la presenza di lettere minuscole
if (preg_match('/[a-z]/', $password)) {
$size += 26;
}
// Verifica la presenza di lettere maiuscole
if (preg_match('/[A-Z]/', $password)) {
$size += 26;
}
// Verifica la presenza di cifre
if (preg_match('/[0-9]/', $password)) {
$size += 10;
}
// Verifica la presenza di simboli speciali
if (preg_match('/[^a-zA-Z0-9]/', $password)) {
$size += 32;
}
return $size;
}
La funzione utilizza espressioni regolari per individuare la presenza di almeno un carattere appartenente a ciascuna categoria. Va sottolineato che la stima è leggermente conservativa: l'attaccante non sa a priori quali caratteri siano effettivamente usati, ma assumere che debba esplorare l'intero spazio di una categoria una volta rilevato un carattere di quella categoria è il modello standard adottato dai principali strumenti di valutazione, come zxcvbn di Dropbox.
Calcolare l'entropia di base
Con la dimensione dell'alfabeto e la lunghezza della password possiamo ora calcolare l'entropia in bit. PHP non dispone nativamente di una funzione log2() dedicata, ma possiamo usare log() specificando come secondo argomento la base 2.
<?php
function calculateEntropy(string $password): float
{
$length = mb_strlen($password);
if ($length === 0) {
return 0.0;
}
$charsetSize = getCharsetSize($password);
if ($charsetSize === 0) {
return 0.0;
}
// Formula di Shannon: H = L * log2(N)
return $length * log($charsetSize, 2);
}
// Esempio di utilizzo
$password = 'P@ssw0rd2024';
$entropy = calculateEntropy($password);
echo "Password: {$password}\n";
echo "Entropia: " . round($entropy, 2) . " bit\n";
Si noti l'uso di mb_strlen() al posto di strlen(): questo garantisce un conteggio corretto dei caratteri anche in presenza di codifiche multibyte come UTF-8, evitando errori quando la password contiene caratteri accentati o appartenenti ad altri alfabeti.
Classificare la robustezza della password
Un valore numerico di entropia, da solo, non è particolarmente significativo per l'utente finale. È utile mappare l'entropia su una scala qualitativa che indichi il livello di sicurezza. Le soglie comunemente accettate dalle linee guida NIST e dalla letteratura sulla sicurezza informatica sono le seguenti.
<?php
function getStrengthLabel(float $entropy): string
{
if ($entropy < 28) {
return 'Molto debole';
}
if ($entropy < 36) {
return 'Debole';
}
if ($entropy < 60) {
return 'Discreta';
}
if ($entropy < 128) {
return 'Forte';
}
return 'Molto forte';
}
Una password con entropia inferiore a 28 bit è considerata banalmente attaccabile anche da un computer modesto, mentre superare i 60 bit garantisce una resistenza adeguata alla maggior parte degli attacchi offline. Sopra i 128 bit ci si trova in territorio crittograficamente sicuro, paragonabile a quello delle chiavi simmetriche moderne.
Stimare il tempo di crack
Un'altra metrica intuitiva è il tempo necessario, in media, per indovinare la password tramite un attacco a forza bruta. Assumendo che un attaccante moderno possa testare circa 1010 tentativi al secondo (uno scenario realistico per password sottoposte ad hash veloce come MD5 o SHA-1 su GPU), possiamo derivare il tempo di crack dall'entropia.
<?php
function estimateCrackTime(float $entropy, float $guessesPerSecond = 1e10): string
{
// Numero totale di combinazioni possibili
$combinations = pow(2, $entropy);
// Tempo medio: metà dello spazio totale
$seconds = $combinations / (2 * $guessesPerSecond);
if ($seconds < 1) {
return 'Istantaneo';
}
if ($seconds < 60) {
return round($seconds, 2) . ' secondi';
}
if ($seconds < 3600) {
return round($seconds / 60, 2) . ' minuti';
}
if ($seconds < 86400) {
return round($seconds / 3600, 2) . ' ore';
}
if ($seconds < 31536000) {
return round($seconds / 86400, 2) . ' giorni';
}
$years = $seconds / 31536000;
if ($years < 1e6) {
return round($years, 2) . ' anni';
}
return sprintf('%.2e anni', $years);
}
La divisione per 2 tiene conto del fatto che, in media, un attaccante trova la password dopo aver esplorato metà dello spazio totale delle combinazioni. Quando il numero di anni diventa estremamente grande, ricorriamo alla notazione scientifica per mantenere l'output leggibile.
Penalizzare i pattern prevedibili
L'entropia calcolata con la formula di Shannon assume che ogni carattere della password sia indipendente e scelto uniformemente. Nella realtà, le password create dagli utenti contengono spesso pattern facilmente prevedibili: caratteri ripetuti, sequenze di tastiera, parole del dizionario, date. È quindi opportuno applicare delle penalizzazioni per ottenere una stima più realistica.
<?php
function detectRepeatedChars(string $password): float
{
$penalty = 0.0;
$length = mb_strlen($password);
for ($i = 1; $i < $length; $i++) {
if ($password[$i] === $password[$i - 1]) {
// Ogni ripetizione consecutiva riduce l'entropia
$penalty += 1.5;
}
}
return $penalty;
}
function detectSequentialChars(string $password): float
{
$penalty = 0.0;
$length = mb_strlen($password);
for ($i = 2; $i < $length; $i++) {
$current = ord($password[$i]);
$previous = ord($password[$i - 1]);
$beforePrevious = ord($password[$i - 2]);
// Sequenze ascendenti come "abc" o "123"
if ($current - $previous === 1 && $previous - $beforePrevious === 1) {
$penalty += 2.0;
}
// Sequenze discendenti come "cba" o "321"
if ($previous - $current === 1 && $beforePrevious - $previous === 1) {
$penalty += 2.0;
}
}
return $penalty;
}
La funzione detectRepeatedChars() individua coppie di caratteri identici consecutivi, mentre detectSequentialChars() riconosce sequenze monotone crescenti o decrescenti di lunghezza pari ad almeno tre caratteri. I valori di penalità sono indicativi e possono essere calibrati sulla base di analisi empiriche di dataset di password compromesse.
Verificare le parole comuni
Un attaccante esperto non procede mai a forza bruta cieca: utilizza dizionari di password comuni e parole frequenti. Se la password contiene una di queste parole, l'entropia effettiva crolla drasticamente. Implementiamo un controllo basato su una lista di parole comuni.
<?php
function containsCommonWord(string $password, array $commonWords): bool
{
$normalized = strtolower($password);
foreach ($commonWords as $word) {
if (mb_strlen($word) >= 4 && str_contains($normalized, $word)) {
return true;
}
}
return false;
}
// Lista minima di esempio
$commonWords = [
'password',
'admin',
'welcome',
'qwerty',
'letmein',
'master',
'dragon',
'monkey',
'football',
'iloveyou',
];
$password = 'MyPassword123';
if (containsCommonWord($password, $commonWords)) {
echo "Attenzione: la password contiene una parola comune.\n";
}
In un'applicazione di produzione, la lista delle parole comuni dovrebbe contenere migliaia di voci, attingendo a dataset pubblici come rockyou.txt o SecLists. Per password che includono parole comuni si può applicare una penalità sostanziosa, riducendo l'entropia anche del 50% o più.
Una classe completa per l'analisi
Mettiamo ora insieme tutti gli elementi visti finora in una classe coesa che fornisce un'analisi completa della password, restituendo un oggetto strutturato con entropia, livello di robustezza, tempo di crack stimato e dettagli sui pattern rilevati.
<?php
class PasswordEntropyAnalyzer
{
private array $commonWords;
private float $guessesPerSecond;
public function __construct(
array $commonWords = [],
float $guessesPerSecond = 1e10
) {
$this->commonWords = $commonWords;
$this->guessesPerSecond = $guessesPerSecond;
}
public function analyze(string $password): array
{
$length = mb_strlen($password);
if ($length === 0) {
return $this->emptyResult();
}
$charsetSize = $this->getCharsetSize($password);
$baseEntropy = $length * log($charsetSize, 2);
// Applica le penalizzazioni per ottenere l'entropia effettiva
$penalties = $this->calculatePenalties($password);
$effectiveEntropy = max(0, $baseEntropy - $penalties['total']);
return [
'length' => $length,
'charset_size' => $charsetSize,
'base_entropy' => round($baseEntropy, 2),
'effective_entropy' => round($effectiveEntropy, 2),
'strength' => $this->getStrengthLabel($effectiveEntropy),
'crack_time' => $this->estimateCrackTime($effectiveEntropy),
'penalties' => $penalties,
];
}
private function getCharsetSize(string $password): int
{
$size = 0;
if (preg_match('/[a-z]/', $password)) {
$size += 26;
}
if (preg_match('/[A-Z]/', $password)) {
$size += 26;
}
if (preg_match('/[0-9]/', $password)) {
$size += 10;
}
if (preg_match('/[^a-zA-Z0-9]/', $password)) {
$size += 32;
}
return $size;
}
private function calculatePenalties(string $password): array
{
$repeated = $this->detectRepeatedChars($password);
$sequential = $this->detectSequentialChars($password);
$commonWord = $this->containsCommonWord($password) ? 15.0 : 0.0;
$total = $repeated + $sequential + $commonWord;
return [
'repeated_chars' => $repeated,
'sequential_chars' => $sequential,
'common_word' => $commonWord,
'total' => $total,
];
}
private function detectRepeatedChars(string $password): float
{
$penalty = 0.0;
$length = mb_strlen($password);
for ($i = 1; $i < $length; $i++) {
if ($password[$i] === $password[$i - 1]) {
$penalty += 1.5;
}
}
return $penalty;
}
private function detectSequentialChars(string $password): float
{
$penalty = 0.0;
$length = mb_strlen($password);
for ($i = 2; $i < $length; $i++) {
$current = ord($password[$i]);
$previous = ord($password[$i - 1]);
$beforePrevious = ord($password[$i - 2]);
if ($current - $previous === 1 && $previous - $beforePrevious === 1) {
$penalty += 2.0;
}
if ($previous - $current === 1 && $beforePrevious - $previous === 1) {
$penalty += 2.0;
}
}
return $penalty;
}
private function containsCommonWord(string $password): bool
{
$normalized = strtolower($password);
foreach ($this->commonWords as $word) {
if (mb_strlen($word) >= 4 && str_contains($normalized, $word)) {
return true;
}
}
return false;
}
private function getStrengthLabel(float $entropy): string
{
return match (true) {
$entropy < 28 => 'Molto debole',
$entropy < 36 => 'Debole',
$entropy < 60 => 'Discreta',
$entropy < 128 => 'Forte',
default => 'Molto forte',
};
}
private function estimateCrackTime(float $entropy): string
{
$combinations = pow(2, $entropy);
$seconds = $combinations / (2 * $this->guessesPerSecond);
return match (true) {
$seconds < 1 => 'Istantaneo',
$seconds < 60 => round($seconds, 2) . ' secondi',
$seconds < 3600 => round($seconds / 60, 2) . ' minuti',
$seconds < 86400 => round($seconds / 3600, 2) . ' ore',
$seconds < 31536000 => round($seconds / 86400, 2) . ' giorni',
$seconds < 31536000 * 1e6 => round($seconds / 31536000, 2) . ' anni',
default => sprintf('%.2e anni', $seconds / 31536000),
};
}
private function emptyResult(): array
{
return [
'length' => 0,
'charset_size' => 0,
'base_entropy' => 0.0,
'effective_entropy' => 0.0,
'strength' => 'Nessuna',
'crack_time' => 'Istantaneo',
'penalties' => [
'repeated_chars' => 0.0,
'sequential_chars' => 0.0,
'common_word' => 0.0,
'total' => 0.0,
],
];
}
}
La classe è progettata per essere estensibile: il dizionario delle parole comuni e la velocità di guessing sono iniettati nel costruttore, permettendo di adattare l'analisi a contesti diversi. Per password destinate a uso amministrativo, ad esempio, si può alzare la stima della velocità di guessing per riflettere lo scenario peggiore di un attaccante con risorse hardware avanzate.
Esempio di utilizzo
Vediamo finalmente come utilizzare la classe per analizzare alcune password di esempio e confrontare i risultati.
<?php
require_once 'PasswordEntropyAnalyzer.php';
$commonWords = [
'password', 'admin', 'welcome', 'qwerty', 'letmein',
'master', 'dragon', 'monkey', 'football', 'iloveyou',
];
$analyzer = new PasswordEntropyAnalyzer($commonWords);
$samples = [
'123456',
'password',
'P@ssw0rd',
'Tr0ub4dor&3',
'correct horse battery staple',
'X9!kP2#mQ7$nR4&',
];
foreach ($samples as $sample) {
$result = $analyzer->analyze($sample);
echo "Password: {$sample}\n";
echo " Lunghezza: {$result['length']}\n";
echo " Alfabeto: {$result['charset_size']} simboli\n";
echo " Entropia base: {$result['base_entropy']} bit\n";
echo " Entropia effettiva: {$result['effective_entropy']} bit\n";
echo " Robustezza: {$result['strength']}\n";
echo " Tempo di crack: {$result['crack_time']}\n";
echo str_repeat('-', 50) . "\n";
}
Eseguendo lo script si osserva chiaramente come password apparentemente complesse, ma costruite intorno a parole comuni o pattern prevedibili, ottengano un'entropia effettiva molto inferiore alla loro entropia teorica. Al contrario, una passphrase lunga ma composta da parole casuali può raggiungere livelli di sicurezza eccellenti pur restando memorizzabile, confermando l'intuizione popolarizzata dal celebre fumetto di xkcd sulla forza relativa delle password.
Conclusioni
Calcolare l'entropia di una password in PHP non è solo un esercizio teorico, ma uno strumento pratico per migliorare la sicurezza delle applicazioni web. La formula di Shannon fornisce una base solida, ma la realtà richiede di tenere conto di pattern, dizionari e abitudini umane per ottenere una stima realistica. La classe presentata in questo articolo è un punto di partenza completo che può essere ulteriormente arricchito integrando dataset di password compromesse, riconoscimento di sequenze di tastiera (come qwerty e asdfgh), pattern di leet speak e date in formato comune. Combinando l'analisi dell'entropia con politiche di password ben progettate e con l'uso di algoritmi di hash lenti come bcrypt o argon2, è possibile costruire un sistema di autenticazione realmente robusto.