Calcolare l'entropia di una password in Java
L'entropia di una password è una misura quantitativa della sua imprevedibilità, espressa in bit. Più alto è il valore dell'entropia, maggiore è il numero di tentativi che un attaccante deve effettuare per indovinarla tramite un attacco di forza bruta. In questo articolo vedremo come calcolare l'entropia di una password in Java, partendo dai fondamenti teorici per arrivare a un'implementazione robusta che tenga conto anche delle debolezze più comuni, come pattern ripetitivi e parole presenti nei dizionari.
Fondamenti teorici dell'entropia
L'entropia di Shannon, applicata alle password, si basa su una formula molto semplice ma potente. Data una password generata da un alfabeto di N simboli e di lunghezza L, l'entropia H è data da:
H = L * log2(N)
Il valore N rappresenta la cardinalità del pool di caratteri da cui la password può essere estratta. Una password composta solo da cifre decimali ha N = 10, una composta da lettere minuscole ha N = 26, mentre una che combina maiuscole, minuscole, cifre e simboli può raggiungere facilmente N = 94 sui caratteri ASCII stampabili.
È importante sottolineare che questa formula assume che ogni carattere sia scelto in modo uniforme e indipendente. Nella realtà, gli utenti tendono a creare password con pattern prevedibili, e questo riduce drasticamente l'entropia effettiva rispetto a quella teorica.
Determinare il pool di caratteri
Il primo passo è scrivere una classe che analizzi la password e determini quali categorie di caratteri sono presenti, in modo da calcolare correttamente la dimensione del pool. Vediamo una prima implementazione:
package com.example.security;
import java.util.EnumSet;
import java.util.Set;
public class CharacterPoolAnalyzer {
public enum CharacterClass {
LOWERCASE(26),
UPPERCASE(26),
DIGITS(10),
SYMBOLS(32),
EXTENDED(128);
private final int size;
CharacterClass(int size) {
this.size = size;
}
public int size() {
return size;
}
}
public Set<CharacterClass> analyze(String password) {
// Insieme delle classi di caratteri rilevate nella password
Set<CharacterClass> classes = EnumSet.noneOf(CharacterClass.class);
if (password == null || password.isEmpty()) {
return classes;
}
for (int i = 0; i < password.length(); i++) {
char c = password.charAt(i);
if (Character.isLowerCase(c)) {
classes.add(CharacterClass.LOWERCASE);
} else if (Character.isUpperCase(c)) {
classes.add(CharacterClass.UPPERCASE);
} else if (Character.isDigit(c)) {
classes.add(CharacterClass.DIGITS);
} else if (c >= 33 && c <= 126) {
// Caratteri ASCII stampabili non alfanumerici
classes.add(CharacterClass.SYMBOLS);
} else if (c > 126) {
// Caratteri Unicode estesi
classes.add(CharacterClass.EXTENDED);
}
}
return classes;
}
public int poolSize(Set<CharacterClass> classes) {
// Somma delle dimensioni di tutte le classi presenti
return classes.stream().mapToInt(CharacterClass::size).sum();
}
}
Questa classe enumera le principali categorie di caratteri e ne stima la dimensione. Il valore di 32 simboli copre i caratteri di punteggiatura standard ASCII, mentre il valore di 128 per i caratteri estesi è una stima conservativa che può essere aumentata se la password include un range Unicode più ampio.
Implementazione del calcolo dell'entropia
Una volta determinato il pool, possiamo applicare la formula di Shannon. La conversione tra logaritmo naturale e logaritmo in base due si ottiene dividendo per log(2):
package com.example.security;
import java.util.Set;
public class EntropyCalculator {
private static final double LOG_2 = Math.log(2);
private final CharacterPoolAnalyzer analyzer;
public EntropyCalculator() {
this.analyzer = new CharacterPoolAnalyzer();
}
public double calculate(String password) {
if (password == null || password.isEmpty()) {
return 0.0;
}
Set<CharacterPoolAnalyzer.CharacterClass> classes = analyzer.analyze(password);
int poolSize = analyzer.poolSize(classes);
if (poolSize == 0) {
return 0.0;
}
// Formula di Shannon: H = L * log2(N)
return password.length() * (Math.log(poolSize) / LOG_2);
}
}
Con questa implementazione, una password come abcdef restituisce circa 28,2 bit di entropia, mentre Abcdef1! sale a circa 52,4 bit grazie alla combinazione di più classi di caratteri.
Classificazione della robustezza
I valori numerici di entropia sono utili, ma per un utente finale è più chiaro tradurli in livelli di robustezza. Una convenzione diffusa, basata sulle linee guida del NIST, considera robusta una password con almeno 60 bit di entropia, accettabile sopra i 40 bit e debole al di sotto:
package com.example.security;
public enum PasswordStrength {
VERY_WEAK,
WEAK,
REASONABLE,
STRONG,
VERY_STRONG;
public static PasswordStrength fromEntropy(double bits) {
if (bits < 28) {
return VERY_WEAK;
} else if (bits < 36) {
return WEAK;
} else if (bits < 60) {
return REASONABLE;
} else if (bits < 128) {
return STRONG;
}
return VERY_STRONG;
}
}
Penalizzazione dei pattern ripetitivi
L'approccio finora descritto sovrastima l'entropia di password con pattern ripetitivi, come aaaaaa o 123123123. Per ottenere una stima più realistica, possiamo introdurre un fattore di penalità che riduca l'entropia in presenza di sequenze ripetute o caratteri identici consecutivi:
package com.example.security;
public class PatternPenaltyEvaluator {
public double evaluate(String password) {
if (password == null || password.length() < 2) {
return 1.0;
}
double penalty = 1.0;
// Conta i caratteri identici consecutivi
int repeatCount = countConsecutiveRepeats(password);
if (repeatCount > 0) {
penalty *= Math.pow(0.85, repeatCount);
}
// Rileva sequenze numeriche o alfabetiche
int sequenceCount = countSequences(password);
if (sequenceCount > 0) {
penalty *= Math.pow(0.80, sequenceCount);
}
return penalty;
}
private int countConsecutiveRepeats(String password) {
int count = 0;
for (int i = 1; i < password.length(); i++) {
if (password.charAt(i) == password.charAt(i - 1)) {
count++;
}
}
return count;
}
private int countSequences(String password) {
int count = 0;
for (int i = 2; i < password.length(); i++) {
char a = password.charAt(i - 2);
char b = password.charAt(i - 1);
char c = password.charAt(i);
// Verifica sequenze ascendenti come "abc" o "123"
if (b - a == 1 && c - b == 1) {
count++;
}
}
return count;
}
}
Il moltiplicatore restituito è un valore compreso tra 0 e 1 che andrà applicato all'entropia calcolata, abbassandola in proporzione alla quantità di pattern rilevati.
Integrazione con un dizionario di password comuni
Un'altra debolezza tipica è l'uso di parole molto diffuse, come password, qwerty o letmein. Possiamo caricare un piccolo dizionario in memoria e applicare una penalità severa quando la password contiene una di queste stringhe:
package com.example.security;
import java.util.Set;
public class DictionaryChecker {
private final Set<String> commonPasswords;
public DictionaryChecker(Set<String> commonPasswords) {
this.commonPasswords = commonPasswords;
}
public boolean containsCommonWord(String password) {
if (password == null || password.isEmpty()) {
return false;
}
String lower = password.toLowerCase();
for (String word : commonPasswords) {
if (lower.contains(word)) {
return true;
}
}
return false;
}
}
In un'applicazione reale, il dizionario può essere caricato da un file di testo o da una risorsa nel classpath, ad esempio una versione ridotta della lista rockyou o di altri dataset pubblici di password compromesse.
Mettere insieme i componenti
A questo punto possiamo combinare tutti gli elementi in un servizio unico che restituisca un report completo. Definiamo prima una classe che rappresenti il risultato dell'analisi:
package com.example.security;
public class PasswordAnalysisResult {
private final double entropy;
private final double adjustedEntropy;
private final PasswordStrength strength;
private final boolean foundInDictionary;
public PasswordAnalysisResult(double entropy, double adjustedEntropy,
PasswordStrength strength, boolean foundInDictionary) {
this.entropy = entropy;
this.adjustedEntropy = adjustedEntropy;
this.strength = strength;
this.foundInDictionary = foundInDictionary;
}
public double getEntropy() {
return entropy;
}
public double getAdjustedEntropy() {
return adjustedEntropy;
}
public PasswordStrength getStrength() {
return strength;
}
public boolean isFoundInDictionary() {
return foundInDictionary;
}
}
Poi scriviamo il servizio che orchestra tutti i calcoli:
package com.example.security;
import java.util.Set;
public class PasswordAnalyzer {
private final EntropyCalculator entropyCalculator;
private final PatternPenaltyEvaluator penaltyEvaluator;
private final DictionaryChecker dictionaryChecker;
public PasswordAnalyzer(Set<String> commonPasswords) {
this.entropyCalculator = new EntropyCalculator();
this.penaltyEvaluator = new PatternPenaltyEvaluator();
this.dictionaryChecker = new DictionaryChecker(commonPasswords);
}
public PasswordAnalysisResult analyze(String password) {
double rawEntropy = entropyCalculator.calculate(password);
double penalty = penaltyEvaluator.evaluate(password);
boolean inDictionary = dictionaryChecker.containsCommonWord(password);
// Penalità aggiuntiva se la password compare nel dizionario
double dictionaryFactor = inDictionary ? 0.3 : 1.0;
double adjustedEntropy = rawEntropy * penalty * dictionaryFactor;
PasswordStrength strength = PasswordStrength.fromEntropy(adjustedEntropy);
return new PasswordAnalysisResult(rawEntropy, adjustedEntropy, strength, inDictionary);
}
}
Esempio di utilizzo
Vediamo ora un esempio completo che mostra come usare il servizio in una piccola applicazione di test:
package com.example.security;
import java.util.Set;
public class PasswordAnalyzerDemo {
public static void main(String[] args) {
// Dizionario semplificato per la dimostrazione
Set<String> commonPasswords = Set.of(
"password", "qwerty", "letmein", "admin", "welcome", "123456"
);
PasswordAnalyzer analyzer = new PasswordAnalyzer(commonPasswords);
String[] samples = {
"abc",
"password123",
"Tr0ub4dor&3",
"correct horse battery staple",
"9k!Lq2#pZx7@vR4"
};
for (String sample : samples) {
PasswordAnalysisResult result = analyzer.analyze(sample);
System.out.printf("Password: %s%n", sample);
System.out.printf(" Entropia grezza: %.2f bit%n", result.getEntropy());
System.out.printf(" Entropia corretta: %.2f bit%n", result.getAdjustedEntropy());
System.out.printf(" Robustezza: %s%n", result.getStrength());
System.out.printf(" Trovata in dizionario: %s%n%n", result.isFoundInDictionary());
}
}
}
Eseguendo questo programma, otterrete output in cui password ovvie come password123 ricevono punteggi nettamente più bassi rispetto a quanto suggerirebbe il calcolo grezzo, mentre passphrase lunghe e password generate casualmente raggiungono livelli di entropia adeguati.
Considerazioni finali
Il calcolo dell'entropia fornisce una stima utile della robustezza di una password, ma va interpretato con cautela. La formula di Shannon assume distribuzioni uniformi che raramente si verificano nelle password scelte da esseri umani, e per questo motivo l'integrazione di euristiche aggiuntive — come la rilevazione di pattern e il controllo su dizionari — è essenziale per ottenere stime più aderenti alla realtà. Per applicazioni di produzione, vale la pena valutare librerie consolidate come zxcvbn-java, che implementano modelli più sofisticati basati su catene di Markov e database estesi di password compromesse. L'implementazione presentata in questo articolo costituisce comunque una base solida per comprendere i principi sottostanti e per costruire soluzioni personalizzate quando le esigenze specifiche del progetto lo richiedano.