Generare una passphrase con Go

Generare una passphrase con Go

Una passphrase è una sequenza di parole comuni, scelta casualmente da un dizionario, che viene usata come credenziale di autenticazione al posto di una password tradizionale. A differenza di una password composta da caratteri casuali, una passphrase è più lunga ma più facile da memorizzare, e offre un livello di entropia comparabile o superiore. In questo articolo vedremo come implementare un generatore di passphrase in Go, partendo dai fondamenti crittografici fino a ottenere uno strumento completo da riga di comando.

Entropia e sicurezza delle passphrase

Prima di scrivere codice, è utile capire il concetto di entropia applicato alle passphrase. L'entropia si misura in bit e indica quanto è difficile indovinare una passphrase per un attaccante che conosce il metodo di generazione ma non il risultato. La formula è la seguente:

entropia = log2(N^L)

dove N è la dimensione del dizionario e L il numero di parole. Con un dizionario da 7776 parole (il classico Diceware) e 6 parole, si ottengono circa 77 bit di entropia, considerati sufficienti per la maggior parte degli scopi pratici. Go offre il pacchetto crypto/rand per la generazione crittograficamente sicura di numeri casuali, che useremo come base.

Struttura del progetto

Organizziamo il progetto nel seguente modo:

passphrase-generator/
├── go.mod
├── main.go
├── generator/
│   ├── generator.go
│   └── generator_test.go
└── wordlist/
    └── wordlist.go

Inizializziamo il modulo:

go mod init passphrase-generator

Il wordlist integrato

Per semplicità incorporiamo una wordlist ridotta direttamente nel codice. In produzione si userebbe la lista Diceware completa o la EFF Large Wordlist. Creiamo il file wordlist/wordlist.go:

package wordlist

// DefaultWords contiene un campione di parole comuni in inglese
// usato come dizionario di base per la generazione della passphrase.
var DefaultWords = []string{
    "able", "about", "above", "absent", "absorb", "abstract", "abuse",
    "access", "accident", "account", "accuse", "achieve", "acid",
    "acoustic", "acquire", "across", "action", "actor", "adapt",
    "address", "adjust", "admit", "adult", "advance", "advice",
    "affair", "afford", "afraid", "again", "agent", "agree",
    "ahead", "aim", "airport", "alarm", "album", "alert",
    "alien", "alley", "allow", "almost", "alone", "alpha",
    "already", "alter", "always", "amateur", "amazing", "among",
    "amount", "amused", "analyst", "anchor", "ancient", "anger",
    "angle", "angry", "animal", "ankle", "announce", "annual",
    "another", "answer", "antenna", "antique", "anxiety", "apart",
    "appear", "apple", "april", "arch", "arctic", "arena",
    "argue", "armor", "army", "around", "arrange", "arrest",
    "arrive", "arrow", "asset", "assist", "assume", "athlete",
    "atom", "attack", "attend", "attitude", "attract", "auction",
    "audit", "august", "author", "autumn", "average", "avoid",
    "awake", "aware", "away", "awful", "bacon", "badge",
    "balance", "bamboo", "banana", "banner", "barely", "barrel",
    "basic", "basket", "battle", "beach", "beauty", "become",
    "before", "begin", "behave", "belief", "below", "bench",
    "benefit", "between", "beyond", "bicycle", "bitter", "black",
    "blade", "blame", "blanket", "blast", "bleak", "bless",
    "blind", "block", "blood", "blossom", "blouse", "blue",
    "board", "boost", "border", "bounce", "brave", "bread",
    "breeze", "bridge", "brief", "bright", "bring", "brisk",
    "broken", "bronze", "brown", "brush", "budget", "build",
    "burden", "burst", "butter", "buyer", "cabin", "cable",
    "camera", "cancel", "captain", "carbon", "carpet", "carry",
    "castle", "casual", "cause", "caution", "ceiling", "cement",
    "center", "certain", "change", "chaos", "chapter", "charge",
    "charity", "chase", "cheap", "cheese", "chef", "cherry",
    "chest", "chief", "choose", "chronic", "church", "circle",
    "citizen", "claim", "clamp", "clarity", "classic", "clean",
    "clever", "client", "climb", "clinic", "clock", "close",
    "cloud", "cluster", "coach", "coast", "coffee", "collect",
    "color", "column", "commit", "common", "company", "complex",
    "concert", "conduct", "confirm", "connect", "control", "copper",
    "corner", "correct", "couple", "courage", "cover", "craft",
    "crane", "crash", "create", "credit", "crime", "cross",
    "crowd", "cruel", "crush", "crystal", "culture", "curious",
    "curve", "cycle", "damage", "dance", "danger", "daring",
    "debate", "decide", "decline", "degree", "delay", "deliver",
    "demand", "depend", "design", "detail", "detect", "develop",
    "direct", "discover", "display", "divide", "domain", "double",
    "dragon", "drama", "dream", "drift", "drink", "drive",
    "drum", "during", "eager", "early", "earth", "effort",
    "eight", "either", "emerge", "empty", "enable", "endless",
    "energy", "engine", "enjoy", "enough", "enter", "episode",
    "equal", "escape", "estate", "eternal", "ethics", "evolve",
    "exact", "example", "excess", "expect", "extend", "extra",
    "fabric", "facade", "factor", "faint", "false", "fancy",
    "fatal", "fault", "feature", "field", "figure", "filter",
    "finger", "finish", "first", "fiscal", "flame", "flight",
    "floor", "flower", "fluid", "focus", "forest", "forge",
    "found", "frame", "fraud", "fresh", "front", "frozen",
    "function", "future", "galaxy", "garden", "gather", "gauge",
    "ghost", "giant", "glass", "glide", "global", "glove",
    "grace", "grade", "grain", "grant", "graph", "grass",
    "great", "green", "grief", "group", "guard", "guide",
    "guilt", "habit", "happy", "harvest", "heart", "heavy",
    "height", "hidden", "history", "honest", "honor", "horse",
    "hotel", "human", "humid", "hundred", "hybrid", "ideal",
    "image", "impact", "import", "index", "inner", "input",
    "inside", "invest", "invite", "island", "issue", "jungle",
    "junior", "kernel", "layer", "leader", "learn", "letter",
    "level", "light", "limit", "listen", "local", "logic",
    "login", "loose", "lucky", "lunar", "magic", "major",
    "manage", "manual", "market", "master", "matter", "mental",
    "method", "middle", "minute", "mirror", "model", "money",
    "monitor", "moral", "motion", "mountain", "mutual", "naive",
    "narrow", "nature", "network", "noble", "noise", "north",
    "novel", "nuclear", "object", "obtain", "ocean", "office",
    "open", "option", "order", "output", "palace", "panel",
    "paper", "parent", "pattern", "pause", "peace", "period",
    "phrase", "pilot", "place", "planet", "plant", "plasma",
    "point", "policy", "portal", "power", "pretty", "price",
    "prime", "private", "problem", "process", "profit", "promise",
    "provide", "public", "puzzle", "quantum", "quarter", "quick",
    "quiet", "radar", "random", "rapid", "reach", "reason",
    "record", "reform", "reject", "release", "remote", "render",
    "report", "request", "resist", "reveal", "review", "rhythm",
    "river", "rocket", "rotate", "route", "royal", "rural",
    "safety", "sample", "school", "screen", "secret", "secure",
    "select", "senior", "series", "server", "settle", "seven",
    "shadow", "shape", "share", "sharp", "shield", "shift",
    "short", "simple", "single", "sister", "skill", "sleep",
    "slope", "small", "smart", "smoke", "solar", "solid",
    "solve", "south", "space", "spark", "speed", "spirit",
    "stable", "stage", "state", "static", "steel", "stone",
    "store", "storm", "story", "stream", "street", "strict",
    "strong", "style", "sugar", "summit", "super", "surface",
    "switch", "symbol", "talent", "target", "theory", "three",
    "title", "token", "topic", "track", "trade", "trail",
    "train", "travel", "treat", "trend", "trial", "trigger",
    "trust", "truth", "tunnel", "ultra", "uncle", "unique",
    "update", "urban", "usage", "valid", "value", "vector",
    "vendor", "version", "viable", "visual", "vital", "voice",
    "volume", "wallet", "water", "wealth", "weekly", "welcome",
    "whole", "width", "world", "worth", "yield", "young",
    "zone",
}

Il generatore

Creiamo ora il cuore della libreria in generator/generator.go. Il package espone un tipo Generator configurabile tramite opzioni funzionali, un pattern idiomatico in Go per costruttori flessibili.

package generator

import (
    "crypto/rand"
    "fmt"
    "math/big"
    "strings"

    "passphrase-generator/wordlist"
)

// SeparatorType definisce il tipo di separatore tra le parole della passphrase.
type SeparatorType string

const (
    // SeparatorHyphen usa il trattino come separatore
    SeparatorHyphen SeparatorType = "-"
    // SeparatorSpace usa lo spazio come separatore
    SeparatorSpace SeparatorType = " "
    // SeparatorDot usa il punto come separatore
    SeparatorDot SeparatorType = "."
    // SeparatorUnderscore usa il trattino basso come separatore
    SeparatorUnderscore SeparatorType = "_"
)

// Config contiene la configurazione del generatore.
type Config struct {
    // WordCount è il numero di parole nella passphrase
    WordCount int
    // Separator è il carattere usato per separare le parole
    Separator SeparatorType
    // Capitalize indica se capitalizzare la prima lettera di ogni parola
    Capitalize bool
    // AppendNumber indica se aggiungere un numero casuale in fondo
    AppendNumber bool
    // Words è il dizionario personalizzato; se nil, viene usato quello di default
    Words []string
}

// defaultConfig restituisce la configurazione predefinita del generatore.
func defaultConfig() Config {
    return Config{
        WordCount:    4,
        Separator:    SeparatorHyphen,
        Capitalize:   false,
        AppendNumber: false,
        Words:        wordlist.DefaultWords,
    }
}

// Option è una funzione che modifica la configurazione del generatore.
type Option func(*Config)

// WithWordCount imposta il numero di parole della passphrase.
func WithWordCount(n int) Option {
    return func(c *Config) {
        // Il numero minimo di parole accettabile è due
        if n < 2 {
            n = 2
        }
        c.WordCount = n
    }
}

// WithSeparator imposta il separatore tra le parole.
func WithSeparator(sep SeparatorType) Option {
    return func(c *Config) {
        c.Separator = sep
    }
}

// WithCapitalize abilita la capitalizzazione della prima lettera di ogni parola.
func WithCapitalize(v bool) Option {
    return func(c *Config) {
        c.Capitalize = v
    }
}

// WithAppendNumber abilita l'aggiunta di un numero casuale a due cifre in fondo.
func WithAppendNumber(v bool) Option {
    return func(c *Config) {
        c.AppendNumber = v
    }
}

// WithCustomWords imposta un dizionario personalizzato.
func WithCustomWords(words []string) Option {
    return func(c *Config) {
        // Il dizionario deve contenere almeno due parole
        if len(words) < 2 {
            return
        }
        c.Words = words
    }
}

// Generator è il generatore di passphrase.
type Generator struct {
    config Config
}

// New crea un nuovo Generator con le opzioni fornite.
func New(opts ...Option) *Generator {
    cfg := defaultConfig()
    for _, opt := range opts {
        opt(&cfg)
    }
    return &Generator{config: cfg}
}

// secureRandomInt restituisce un intero casuale crittograficamente sicuro
// nell'intervallo [0, max).
func secureRandomInt(max int) (int, error) {
    // big.Int è necessario per l'interfaccia di crypto/rand
    n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
    if err != nil {
        return 0, fmt.Errorf("errore nella generazione del numero casuale: %w", err)
    }
    return int(n.Int64()), nil
}

// pickWord seleziona una parola casuale dal dizionario.
func (g *Generator) pickWord() (string, error) {
    idx, err := secureRandomInt(len(g.config.Words))
    if err != nil {
        return "", err
    }
    word := g.config.Words[idx]
    // Capitalizza se richiesto dalla configurazione
    if g.config.Capitalize && len(word) > 0 {
        word = strings.ToUpper(word[:1]) + word[1:]
    }
    return word, nil
}

// Generate genera e restituisce una passphrase.
func (g *Generator) Generate() (string, error) {
    parts := make([]string, g.config.WordCount)

    for i := 0; i < g.config.WordCount; i++ {
        word, err := g.pickWord()
        if err != nil {
            return "", fmt.Errorf("errore alla parola %d: %w", i+1, err)
        }
        parts[i] = word
    }

    result := strings.Join(parts, string(g.config.Separator))

    // Aggiunge un numero casuale a due cifre se richiesto
    if g.config.AppendNumber {
        num, err := secureRandomInt(100)
        if err != nil {
            return "", fmt.Errorf("errore nel numero finale: %w", err)
        }
        result = fmt.Sprintf("%s%s%02d", result, string(g.config.Separator), num)
    }

    return result, nil
}

// Entropy calcola l'entropia in bit della passphrase generata dalla configurazione corrente.
func (g *Generator) Entropy() float64 {
    // log2(N^L) = L * log2(N)
    n := float64(len(g.config.Words))
    l := float64(g.config.WordCount)
    // log2(x) = ln(x) / ln(2)
    logN := 0.0
    // Calcolo del logaritmo in base 2 tramite la costante math.Log2E
    // evitando l'import di math per semplicità: usiamo la relazione diretta
    x := n
    for x > 1 {
        x /= 2
        logN++
    }
    // Approssimazione intera: restituiamo come float per uso futuro
    return l * logN
}

Vale la pena soffermarsi su secureRandomInt. La funzione rand.Int del package crypto/rand accetta un *big.Int come limite superiore e legge da rand.Reader, che su Linux è backed da /dev/urandom (o dal syscall getrandom). Questo garantisce che ogni indice estratto sia imprevedibile da un punto di vista crittografico, a differenza di math/rand che produce sequenze pseudocasuali deterministiche.

Calcolo dell'entropia preciso

Il metodo Entropy nella versione precedente usa un'approssimazione intera. Aggiungiamo un'implementazione precisa sfruttando il package math. Aggiorniamo generator.go aggiungendo l'import e il metodo corretto:

import (
    "crypto/rand"
    "fmt"
    "math"
    "math/big"
    "strings"

    "passphrase-generator/wordlist"
)

// EntropyBits calcola l'entropia in bit con precisione in virgola mobile.
func (g *Generator) EntropyBits() float64 {
    n := float64(len(g.config.Words))
    l := float64(g.config.WordCount)
    // Entropia = L * log2(N)
    return l * math.Log2(n)
}

I test

Un generatore crittografico merita una suite di test solida. Creiamo generator/generator_test.go:

package generator_test

import (
    "strings"
    "testing"

    "passphrase-generator/generator"
    "passphrase-generator/wordlist"
)

// TestGenerateLength verifica che la passphrase contenga il numero corretto di parole.
func TestGenerateLength(t *testing.T) {
    counts := []int{2, 3, 4, 5, 6, 8}

    for _, count := range counts {
        g := generator.New(generator.WithWordCount(count))
        phrase, err := g.Generate()
        if err != nil {
            t.Fatalf("wordCount=%d: errore inatteso: %v", count, err)
        }

        // Separiamo la passphrase e contiamo le parti
        parts := strings.Split(phrase, "-")
        if len(parts) != count {
            t.Errorf("wordCount=%d: atteso %d parti, ottenuto %d (phrase: %q)",
                count, count, len(parts), phrase)
        }
    }
}

// TestGenerateSeparator verifica che il separatore configurato venga usato.
func TestGenerateSeparator(t *testing.T) {
    separators := []struct {
        sep      generator.SeparatorType
        expected string
    }{
        {generator.SeparatorHyphen, "-"},
        {generator.SeparatorDot, "."},
        {generator.SeparatorUnderscore, "_"},
    }

    for _, tc := range separators {
        g := generator.New(
            generator.WithWordCount(3),
            generator.WithSeparator(tc.sep),
        )
        phrase, err := g.Generate()
        if err != nil {
            t.Fatalf("separator=%q: errore inatteso: %v", tc.expected, err)
        }
        if !strings.Contains(phrase, tc.expected) {
            t.Errorf("separator=%q: non trovato in %q", tc.expected, phrase)
        }
    }
}

// TestGenerateCapitalize verifica che la capitalizzazione venga applicata correttamente.
func TestGenerateCapitalize(t *testing.T) {
    g := generator.New(
        generator.WithWordCount(4),
        generator.WithCapitalize(true),
    )
    phrase, err := g.Generate()
    if err != nil {
        t.Fatalf("errore inatteso: %v", err)
    }

    parts := strings.Split(phrase, "-")
    for _, part := range parts {
        if len(part) == 0 {
            continue
        }
        // La prima lettera di ogni parola deve essere maiuscola
        first := string(part[0])
        if first != strings.ToUpper(first) {
            t.Errorf("parola non capitalizzata: %q in %q", part, phrase)
        }
    }
}

// TestGenerateAppendNumber verifica che il numero finale sia presente e valido.
func TestGenerateAppendNumber(t *testing.T) {
    g := generator.New(
        generator.WithWordCount(3),
        generator.WithAppendNumber(true),
    )
    phrase, err := g.Generate()
    if err != nil {
        t.Fatalf("errore inatteso: %v", err)
    }

    parts := strings.Split(phrase, "-")
    // Con 3 parole e numero finale ci aspettiamo 4 parti
    if len(parts) != 4 {
        t.Errorf("atteso 4 parti con numero, ottenuto %d in %q", len(parts), phrase)
    }

    // L'ultima parte deve essere un numero a due cifre
    last := parts[len(parts)-1]
    if len(last) != 2 {
        t.Errorf("numero finale non a due cifre: %q", last)
    }
    for _, ch := range last {
        if ch < '0' || ch > '9' {
            t.Errorf("carattere non numerico nel suffisso: %q", last)
        }
    }
}

// TestGenerateUniqueness verifica che due passphrase consecutive siano diverse.
func TestGenerateUniqueness(t *testing.T) {
    g := generator.New(generator.WithWordCount(4))
    seen := make(map[string]bool)
    // Generiamo 20 passphrase; la probabilità di collisione è trascurabile
    for i := 0; i < 20; i++ {
        phrase, err := g.Generate()
        if err != nil {
            t.Fatalf("iterazione %d: errore inatteso: %v", i, err)
        }
        if seen[phrase] {
            t.Errorf("passphrase duplicata generata: %q", phrase)
        }
        seen[phrase] = true
    }
}

// TestGenerateCustomWords verifica che il dizionario personalizzato venga usato.
func TestGenerateCustomWords(t *testing.T) {
    custom := []string{"alpha", "beta", "gamma", "delta", "epsilon"}
    g := generator.New(
        generator.WithWordCount(3),
        generator.WithCustomWords(custom),
    )
    phrase, err := g.Generate()
    if err != nil {
        t.Fatalf("errore inatteso: %v", err)
    }

    parts := strings.Split(phrase, "-")
    for _, part := range parts {
        found := false
        for _, w := range custom {
            if part == w {
                found = true
                break
            }
        }
        if !found {
            t.Errorf("parola %q non appartiene al dizionario personalizzato", part)
        }
    }
}

// TestEntropyBits verifica che il calcolo dell'entropia sia ragionevole.
func TestEntropyBits(t *testing.T) {
    g := generator.New(
        generator.WithWordCount(4),
        generator.WithCustomWords(wordlist.DefaultWords),
    )
    entropy := g.EntropyBits()
    // Con 4 parole e un dizionario ampio ci aspettiamo almeno 30 bit
    if entropy < 30 {
        t.Errorf("entropia troppo bassa: %.2f bit", entropy)
    }
    t.Logf("entropia calcolata: %.2f bit", entropy)
}

Per eseguire i test:

go test ./generator/... -v

Il programma principale

Creiamo main.go, che espone il generatore come strumento da riga di comando usando il package flag della standard library:

package main

import (
    "flag"
    "fmt"
    "os"

    "passphrase-generator/generator"
)

// cliFlags raccoglie tutti i flag della riga di comando.
type cliFlags struct {
    wordCount    int
    separator    string
    capitalize   bool
    appendNumber bool
    count        int
    showEntropy  bool
}

// parseSeparator converte la stringa del flag nel tipo SeparatorType corrispondente.
func parseSeparator(s string) generator.SeparatorType {
    switch s {
    case "space":
        return generator.SeparatorSpace
    case "dot":
        return generator.SeparatorDot
    case "underscore":
        return generator.SeparatorUnderscore
    default:
        // Il trattino è il separatore predefinito
        return generator.SeparatorHyphen
    }
}

func main() {
    // Definizione dei flag con valori predefiniti e descrizioni
    flags := cliFlags{}
    flag.IntVar(&flags.wordCount, "words", 4, "numero di parole nella passphrase")
    flag.StringVar(&flags.separator, "sep", "hyphen", "separatore: hyphen, space, dot, underscore")
    flag.BoolVar(&flags.capitalize, "capitalize", false, "capitalizza la prima lettera di ogni parola")
    flag.BoolVar(&flags.appendNumber, "number", false, "aggiunge un numero a due cifre in fondo")
    flag.IntVar(&flags.count, "count", 1, "numero di passphrase da generare")
    flag.BoolVar(&flags.showEntropy, "entropy", false, "mostra l'entropia stimata in bit")
    flag.Parse()

    // Validazione del numero di passphrase richieste
    if flags.count < 1 {
        fmt.Fprintln(os.Stderr, "errore: -count deve essere almeno 1")
        os.Exit(1)
    }

    // Costruzione del generatore con le opzioni dalla riga di comando
    gen := generator.New(
        generator.WithWordCount(flags.wordCount),
        generator.WithSeparator(parseSeparator(flags.separator)),
        generator.WithCapitalize(flags.capitalize),
        generator.WithAppendNumber(flags.appendNumber),
    )

    // Stampa dell'entropia se richiesto
    if flags.showEntropy {
        fmt.Printf("entropia stimata: %.2f bit\n\n", gen.EntropyBits())
    }

    // Generazione e stampa delle passphrase
    for i := 0; i < flags.count; i++ {
        phrase, err := gen.Generate()
        if err != nil {
            fmt.Fprintf(os.Stderr, "errore durante la generazione: %v\n", err)
            os.Exit(1)
        }
        fmt.Println(phrase)
    }
}

Compilazione e utilizzo

Compiliamo il programma:

go build -o passphrase-generator ./...

Esempi di utilizzo da riga di comando:

# Passphrase con 4 parole separate da trattino (default)
./passphrase-generator

# 5 parole, separatore punto, con capitalizzazione
./passphrase-generator -words 5 -sep dot -capitalize

# 6 parole con numero finale, mostrando l'entropia
./passphrase-generator -words 6 -number -entropy

# Generare 10 passphrase in una sola chiamata
./passphrase-generator -words 4 -count 10

# Passphrase con separatore spazio
./passphrase-generator -words 5 -sep space -capitalize

Output di esempio:

entropia stimata: 54.21 bit

Market-Frozen-Spiral-Ocean-Judge-River-42

Aggiungere il supporto per wordlist esterne

Per rendere il generatore più flessibile, possiamo aggiungere il supporto per il caricamento di wordlist da file di testo esterni, una parola per riga. Estendiamo wordlist/wordlist.go:

package wordlist

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

// LoadFromFile carica una wordlist da un file di testo,
// aspettandosi una parola per riga. Ignora le righe vuote.
func LoadFromFile(path string) ([]string, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("impossibile aprire il file wordlist: %w", err)
    }
    defer file.Close()

    var words []string
    scanner := bufio.NewScanner(file)

    for scanner.Scan() {
        // Rimuoviamo spazi e ritorni a capo dal bordo
        word := strings.TrimSpace(scanner.Text())
        if word == "" {
            // Saltiamo le righe vuote
            continue
        }
        words = append(words, word)
    }

    if err := scanner.Err(); err != nil {
        return nil, fmt.Errorf("errore nella lettura del file wordlist: %w", err)
    }

    if len(words) < 2 {
        return nil, fmt.Errorf("wordlist troppo corta: trovate %d parole, minimo 2", len(words))
    }

    return words, nil
}

E aggiorniamo main.go per accettare il flag -wordlist:

// Aggiunta alla struttura cliFlags
wordlistPath string

// Aggiunta nella sezione flag.Parse
flag.StringVar(&flags.wordlistPath, "wordlist", "", "percorso a un file wordlist esterno (una parola per riga)")

// Costruzione condizionale del generatore
var opts []generator.Option
opts = append(opts,
    generator.WithWordCount(flags.wordCount),
    generator.WithSeparator(parseSeparator(flags.separator)),
    generator.WithCapitalize(flags.capitalize),
    generator.WithAppendNumber(flags.appendNumber),
)

if flags.wordlistPath != "" {
    // Carichiamo il dizionario dal file specificato
    words, err := wordlist.LoadFromFile(flags.wordlistPath)
    if err != nil {
        fmt.Fprintf(os.Stderr, "errore nella wordlist: %v\n", err)
        os.Exit(1)
    }
    opts = append(opts, generator.WithCustomWords(words))
}

gen := generator.New(opts...)

Considerazioni sulla sicurezza

Alcune note importanti per chi intende usare questo codice in contesti reali. Il package crypto/rand è la scelta corretta per qualsiasi applicazione crittografica: non usare mai math/rand, che produce sequenze deterministiche e prevedibili. La dimensione del dizionario influisce direttamente sull'entropia: la EFF Large Wordlist contiene 7776 parole, che con sei parole producono 77,5 bit di entropia. Con il dizionario ridotto di questo articolo (circa 500 parole) si ottengono circa 52 bit con sei parole, accettabili per molti scopi ma non per contesti ad alta sicurezza. Il numero casuale aggiunto con -number contribuisce circa 6,6 bit aggiuntivi (log2(100)).

La passphrase generata non dovrebbe mai essere trasmessa in chiaro né memorizzata come hash non salato. In un sistema di autenticazione reale va combinata con bcrypt, scrypt o Argon2id prima di essere persistita.

Conclusioni

Abbiamo costruito un generatore di passphrase completo in Go, applicando il pattern delle opzioni funzionali per una API flessibile, crypto/rand per la casualità crittografica, e una suite di test che copre i casi principali. Il risultato è un binario autonomo, senza dipendenze esterne, che produce passphrase memorabili con un'entropia configurabile. La struttura del codice si presta facilmente a estensioni: si potrebbe aggiungere l'output in formato JSON per l'integrazione con altri strumenti, oppure esporre il generatore come libreria importabile da altri moduli Go.