Generare una passphrase con Java
Una passphrase è una sequenza di parole casuali, separate da un carattere delimitatore, utilizzata come alternativa sicura a una password tradizionale. A differenza di una password composta da caratteri pseudo-casuali difficili da memorizzare, una passphrase è leggibile e facile da ricordare, mantenendo al tempo stesso un elevato livello di entropia. In questo articolo vedremo come implementare un generatore di passphrase in Java, partendo dalla gestione del dizionario di parole fino alla costruzione della stringa finale.
Concetti fondamentali
La sicurezza di una passphrase dipende da due fattori principali: la dimensione del dizionario da cui vengono estratte le parole e il numero di parole che compongono la passphrase. Se il dizionario contiene N parole e la passphrase è composta da k parole, l'entropia in bit si calcola come:
entropia = k * log2(N)
Ad esempio, con un dizionario di 2048 parole e una passphrase di 6 parole si ottengono circa 66 bit di entropia, considerata sufficiente per la maggior parte degli scenari di sicurezza odierni.
Un aspetto critico è la fonte di casualità. Java mette a disposizione due classi principali: java.util.Random, che utilizza un generatore pseudo-casuale non adatto a contesti crittografici, e java.security.SecureRandom, che si appoggia a sorgenti di entropia del sistema operativo. Per un generatore di passphrase destinato a uso reale è obbligatorio utilizzare SecureRandom.
Struttura del progetto
Organizzeremo il codice in tre classi principali:
- WordlistLoader: carica il dizionario da file o da risorsa embedded.
- PassphraseGenerator: seleziona le parole casuali e assembla la passphrase.
- Main: punto di ingresso dell'applicazione con configurazione via riga di comando.
Per semplicità utilizzeremo un progetto Maven standard senza dipendenze esterne, in modo che il codice sia eseguibile su qualsiasi JDK 17 o superiore.
Il dizionario di parole
Un buon dizionario per passphrase deve contenere parole comuni, riconoscibili e preferibilmente senza ambiguità ortografiche. Lo standard de facto è la wordlist EFF (Electronic Frontier Foundation), che contiene 7776 parole in inglese progettate specificamente per questo scopo. Per i nostri esempi useremo un file di testo semplice, una parola per riga, collocato in src/main/resources/wordlist.txt.
La classe WordlistLoader legge il file dalla classpath e restituisce una lista immutabile di stringhe:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class WordlistLoader {
// Percorso della wordlist nella classpath
private static final String DEFAULT_RESOURCE = "/wordlist.txt";
/**
* Carica la wordlist dalla classpath e restituisce
* una lista immutabile di parole.
*/
public static List<String> load() throws IOException {
return load(DEFAULT_RESOURCE);
}
/**
* Carica la wordlist dal percorso di risorsa indicato.
*/
public static List<String> load(String resourcePath) throws IOException {
List<String> words = new ArrayList<>();
// Apriamo la risorsa dal classpath
try (InputStream inputStream = WordlistLoader.class.getResourceAsStream(resourcePath)) {
if (inputStream == null) {
throw new IOException("Risorsa non trovata: " + resourcePath);
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8)
);
String line;
while ((line = reader.readLine()) != null) {
// Ignoriamo righe vuote e commenti
String trimmed = line.strip();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
words.add(trimmed.toLowerCase());
}
}
}
if (words.isEmpty()) {
throw new IllegalStateException("La wordlist è vuota.");
}
// Rendiamo la lista immutabile per sicurezza
return Collections.unmodifiableList(words);
}
}
La scelta di Collections.unmodifiableList impedisce modifiche accidentali alla wordlist una volta caricata. Il metodo strip(), disponibile da Java 11, gestisce correttamente gli spazi Unicode oltre agli spazi ASCII tradizionali.
Il generatore di passphrase
La classe centrale è PassphraseGenerator. Riceve in costruzione la lista di parole e un'istanza di SecureRandom, e offre un metodo generate che accetta il numero di parole desiderate e il separatore:
import java.security.SecureRandom;
import java.util.List;
import java.util.StringJoiner;
public class PassphraseGenerator {
private final List<String> wordlist;
private final SecureRandom secureRandom;
/**
* Costruisce il generatore con la wordlist fornita.
* Crea internamente un'istanza di SecureRandom.
*/
public PassphraseGenerator(List<String> wordlist) {
this(wordlist, new SecureRandom());
}
/**
* Costruisce il generatore con wordlist e sorgente
* di casualità esplicita (utile nei test).
*/
public PassphraseGenerator(List<String> wordlist, SecureRandom secureRandom) {
if (wordlist == null || wordlist.isEmpty()) {
throw new IllegalArgumentException("La wordlist non può essere nulla o vuota.");
}
this.wordlist = wordlist;
this.secureRandom = secureRandom;
}
/**
* Genera una passphrase composta dal numero di parole indicato,
* separate dal separatore specificato.
*/
public String generate(int wordCount, String separator) {
if (wordCount < 1) {
throw new IllegalArgumentException("Il numero di parole deve essere almeno 1.");
}
if (separator == null) {
throw new IllegalArgumentException("Il separatore non può essere null.");
}
StringJoiner joiner = new StringJoiner(separator);
for (int i = 0; i < wordCount; i++) {
// Selezioniamo un indice casuale crittograficamente sicuro
int index = secureRandom.nextInt(wordlist.size());
joiner.add(wordlist.get(index));
}
return joiner.toString();
}
/**
* Genera una passphrase con separatore predefinito (trattino).
*/
public String generate(int wordCount) {
return generate(wordCount, "-");
}
/**
* Calcola l'entropia in bit della configurazione corrente.
*/
public double calculateEntropy(int wordCount) {
// log2(N) * k dove N è la dimensione della wordlist
return wordCount * (Math.log(wordlist.size()) / Math.log(2));
}
}
L'utilizzo di StringJoiner è preferibile alla concatenazione manuale con StringBuilder perché gestisce automaticamente il separatore senza aggiungerne uno in coda, producendo codice più leggibile e meno soggetto a errori di off-by-one.
Validazione dell'entropia
Prima di restituire la passphrase all'utente è buona pratica verificare che l'entropia minima sia rispettata. Definiamo una soglia di 60 bit come minimo accettabile e aggiungiamo un metodo di validazione:
import java.security.SecureRandom;
import java.util.List;
import java.util.StringJoiner;
public class PassphraseGenerator {
// Soglia minima di entropia considerata sicura
private static final double MINIMUM_ENTROPY_BITS = 60.0;
private final List<String> wordlist;
private final SecureRandom secureRandom;
public PassphraseGenerator(List<String> wordlist) {
this(wordlist, new SecureRandom());
}
public PassphraseGenerator(List<String> wordlist, SecureRandom secureRandom) {
if (wordlist == null || wordlist.isEmpty()) {
throw new IllegalArgumentException("La wordlist non può essere nulla o vuota.");
}
this.wordlist = wordlist;
this.secureRandom = secureRandom;
}
public String generate(int wordCount, String separator) {
if (wordCount < 1) {
throw new IllegalArgumentException("Il numero di parole deve essere almeno 1.");
}
// Avvisiamo se l'entropia è inferiore alla soglia minima
double entropy = calculateEntropy(wordCount);
if (entropy < MINIMUM_ENTROPY_BITS) {
throw new IllegalArgumentException(
String.format(
"Entropia insufficiente: %.1f bit (minimo %.1f bit). " +
"Aumentare il numero di parole o la dimensione della wordlist.",
entropy, MINIMUM_ENTROPY_BITS
)
);
}
StringJoiner joiner = new StringJoiner(separator);
for (int i = 0; i < wordCount; i++) {
int index = secureRandom.nextInt(wordlist.size());
joiner.add(wordlist.get(index));
}
return joiner.toString();
}
public String generate(int wordCount) {
return generate(wordCount, "-");
}
public double calculateEntropy(int wordCount) {
return wordCount * (Math.log(wordlist.size()) / Math.log(2));
}
}
Il punto di ingresso dell'applicazione
La classe Main gestisce gli argomenti da riga di comando e coordina il flusso di esecuzione. Accetta due parametri opzionali: il numero di parole (default 6) e il separatore (default -):
import java.io.IOException;
import java.util.List;
public class Main {
// Numero di parole predefinito se non specificato
private static final int DEFAULT_WORD_COUNT = 6;
// Separatore predefinito tra le parole
private static final String DEFAULT_SEPARATOR = "-";
public static void main(String[] args) {
int wordCount = DEFAULT_WORD_COUNT;
String separator = DEFAULT_SEPARATOR;
// Leggiamo il numero di parole dal primo argomento
if (args.length >= 1) {
try {
wordCount = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
System.err.println("Errore: il primo argomento deve essere un intero positivo.");
System.exit(1);
}
}
// Leggiamo il separatore dal secondo argomento
if (args.length >= 2) {
separator = args[1];
}
try {
// Carichiamo il dizionario dalla classpath
List<String> wordlist = WordlistLoader.load();
PassphraseGenerator generator = new PassphraseGenerator(wordlist);
String passphrase = generator.generate(wordCount, separator);
double entropy = generator.calculateEntropy(wordCount);
System.out.println("Passphrase: " + passphrase);
System.out.printf("Entropia: %.1f bit%n", entropy);
System.out.printf("Parole: %d / Dizionario: %d parole%n",
wordCount, wordlist.size());
} catch (IOException e) {
System.err.println("Errore nel caricamento della wordlist: " + e.getMessage());
System.exit(1);
} catch (IllegalArgumentException e) {
System.err.println("Parametri non validi: " + e.getMessage());
System.exit(1);
}
}
}
Generare un dizionario di parole italiane
Se si desidera una passphrase in italiano, è possibile costruire una wordlist personalizzata a partire da un corpus lessicale oppure da un file di testo esistente. Il seguente metodo legge un file di testo, estrae i token alfabetici e seleziona le parole con lunghezza compresa tra 4 e 8 caratteri, scrivendo il risultato in un nuovo file:
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class WordlistBuilder {
// Lunghezza minima e massima delle parole selezionate
private static final int MIN_WORD_LENGTH = 4;
private static final int MAX_WORD_LENGTH = 8;
/**
* Costruisce una wordlist leggendo un corpus testuale.
* Estrae parole uniche di lunghezza compresa tra MIN e MAX.
*/
public static void buildFromCorpus(Path inputPath, Path outputPath) throws IOException {
String content = Files.readString(inputPath, StandardCharsets.UTF_8);
// Suddividiamo il testo in token non alfabetici
String[] tokens = content.split("[^a-zA-Zàèéìîïòôùûüäöüçñ]+");
// Filtriamo per lunghezza e normalizziamo in minuscolo
Set<String> uniqueWords = Arrays.stream(tokens)
.map(String::toLowerCase)
.filter(w -> w.length() >= MIN_WORD_LENGTH && w.length() <= MAX_WORD_LENGTH)
.collect(Collectors.toCollection(LinkedHashSet::new));
// Scriviamo le parole nel file di output, una per riga
try (BufferedWriter writer = Files.newBufferedWriter(outputPath, StandardCharsets.UTF_8)) {
for (String word : uniqueWords) {
writer.write(word);
writer.newLine();
}
}
System.out.printf("Wordlist costruita: %d parole uniche salvate in %s%n",
uniqueWords.size(), outputPath);
}
}
L'utilizzo di LinkedHashSet garantisce l'unicità delle parole preservando l'ordine di inserimento, il che facilita la verifica manuale del contenuto del file generato.
Test con JUnit 5
Una suite di test essenziale verifica il comportamento del generatore nei casi normali e nei casi limite. Utilizziamo un SecureRandom con seed fisso per rendere i test deterministici:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.security.SecureRandom;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class PassphraseGeneratorTest {
// Wordlist minimale per i test
private static final List<String> TEST_WORDLIST = List.of(
"apple", "banana", "cherry", "date", "elder",
"fig", "grape", "honeydew", "ivy", "jasmine",
"kiwi", "lemon", "mango", "nectarine", "orange",
"papaya", "quince", "raspberry", "strawberry", "tangerine",
"ugli", "vanilla", "watermelon", "ximenia", "yuzu", "zucchini"
);
private PassphraseGenerator generator;
@BeforeEach
void setUp() {
// Seed fisso per rendere i test riproducibili
SecureRandom deterministicRandom = new SecureRandom(new byte[]{42});
generator = new PassphraseGenerator(TEST_WORDLIST, deterministicRandom);
}
@Test
void generateReturnsCorrectWordCount() {
// Verifichiamo che la passphrase contenga esattamente 4 parole
String passphrase = generator.generate(4, "-");
String[] parts = passphrase.split("-");
assertEquals(4, parts.length);
}
@Test
void generateUsesSpecifiedSeparator() {
// Verifichiamo che il separatore sia quello indicato
String passphrase = generator.generate(3, ".");
assertTrue(passphrase.contains("."));
assertFalse(passphrase.contains("-"));
}
@Test
void generateContainsOnlyWordlistWords() {
// Ogni parola della passphrase deve appartenere alla wordlist
String passphrase = generator.generate(4, " ");
String[] parts = passphrase.split(" ");
for (String part : parts) {
assertTrue(TEST_WORDLIST.contains(part),
"La parola '" + part + "' non appartiene alla wordlist.");
}
}
@Test
void calculateEntropyIsCorrect() {
// Con 26 parole e 4 token: entropia = 4 * log2(26) ≈ 18.8 bit
double entropy = generator.calculateEntropy(4);
assertEquals(4 * (Math.log(26) / Math.log(2)), entropy, 0.001);
}
@Test
void generateThrowsOnZeroWordCount() {
// Deve sollevare eccezione con 0 parole
assertThrows(IllegalArgumentException.class, () -> generator.generate(0));
}
@Test
void constructorThrowsOnEmptyWordlist() {
// Deve sollevare eccezione con wordlist vuota
assertThrows(IllegalArgumentException.class,
() -> new PassphraseGenerator(List.of()));
}
}
Esempio di output
Con una wordlist EFF da 7776 parole e 6 parole richieste, un'esecuzione tipica produce:
Passphrase: clutter-uproot-unmasked-dwindling-afoot-granite
Entropia: 77.5 bit
Parole: 6 / Dizionario: 7776 parole
Con 5 parole e separatore spazio:
Passphrase: mossy upbeat revival slander jukebox
Entropia: 64.6 bit
Parole: 5 / Dizionario: 7776 parole
Considerazioni sulla sicurezza
Alcuni aspetti meritano attenzione quando si distribuisce un generatore di passphrase in un contesto reale. In primo luogo, la passphrase non deve essere mai memorizzata in chiaro: se l'applicazione gestisce credenziali, il risultato deve essere passato immediatamente alla funzione di hashing (Argon2, bcrypt o scrypt) senza lasciare tracce in log o variabili di istanza a lunga vita.
In secondo luogo, String in Java è immutabile e interned, il che significa che il suo contenuto rimane in memoria fino al prossimo ciclo del garbage collector senza possibilità di azzeramento esplicito. Per applicazioni ad alta sensibilità è preferibile lavorare con array di char o byte, che possono essere azzerati con Arrays.fill non appena la passphrase non è più necessaria.
In terzo luogo, la scelta del dizionario influisce sulla resistenza agli attacchi a dizionario stessi. Una wordlist pubblica e nota come quella EFF è paradossalmente più sicura di una lista oscura e privata, perché l'entropia è calcolabile con precisione e non dipende dall'ipotesi che l'attaccante non conosca la lista.
Conclusioni
Abbiamo costruito un generatore di passphrase completo in Java puro, senza dipendenze esterne, articolato in tre classi con responsabilità ben separate. Il cuore del sistema è l'utilizzo di SecureRandom per la selezione delle parole, che garantisce una distribuzione crittograficamente uniforme. La validazione dell'entropia protegge da configurazioni deboli, mentre la struttura modulare facilita l'estensione: è sufficiente sostituire la wordlist o il separatore per adattare il generatore a qualsiasi requisito di localizzazione o policy aziendale.
Un ulteriore sviluppo potrebbe prevedere l'integrazione con una API REST, la gestione di dizionari multipli in lingue diverse o l'aggiunta di opzioni per inserire cifre e simboli tra le parole, aumentando l'entropia senza sacrificare eccessivamente la memorizzabilità della passphrase.