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.