hub: use Hungarian word passphrases for retrieval passwords

Replace 64-char hex retrieval passwords with 5-word Hungarian
passphrases (e.g. áldás-plazmid-palánta-süvítve-pócgém) for
better UX in disaster recovery scenarios. Embed 29K+ word list
via go:embed. API keys remain hex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 14:31:39 +01:00
parent 165c59e84b
commit 1f05f9f866
5 changed files with 29744 additions and 3 deletions
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
package configgen
import (
"crypto/rand"
_ "embed"
"math/big"
"strings"
)
//go:embed hungarian.txt
var hungarianWords string
// wordList is populated from the embedded hungarian.txt at init time.
var wordList []string
func init() {
seen := make(map[string]struct{}, 30000)
for _, line := range strings.Split(hungarianWords, "\n") {
w := strings.TrimSpace(line)
if w == "" {
continue
}
if _, dup := seen[w]; dup {
continue
}
seen[w] = struct{}{}
wordList = append(wordList, w)
}
}
// RandomPassphrase generates a human-friendly passphrase from Hungarian words.
// Format: "szó-szó-szó-szó-szó" (words separated by dashes).
// With a ~29K word list, 4 words gives ~59 bits of entropy, 5 words ~74 bits.
// Easy to read, type, and dictate by Hungarian-speaking customers.
func RandomPassphrase(wordCount int) (string, error) {
words := make([]string, wordCount)
max := big.NewInt(int64(len(wordList)))
for i := range words {
idx, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
words[i] = wordList[idx.Int64()]
}
return strings.Join(words, "-"), nil
}
+52
View File
@@ -0,0 +1,52 @@
package configgen
import (
"fmt"
"strings"
"testing"
)
func TestWordListNoDuplicates(t *testing.T) {
seen := make(map[string]bool, len(wordList))
for i, w := range wordList {
if seen[w] {
t.Errorf("duplicate word %q at index %d", w, i)
}
seen[w] = true
}
t.Logf("Total unique words: %d", len(seen))
}
func TestWordListSize(t *testing.T) {
if len(wordList) < 10000 {
t.Errorf("word list too small: %d (want >= 10000)", len(wordList))
}
t.Logf("Word list size: %d", len(wordList))
}
func TestRandomPassphrase(t *testing.T) {
p, err := RandomPassphrase(5)
if err != nil {
t.Fatal(err)
}
parts := strings.Split(p, "-")
if len(parts) != 5 {
t.Errorf("expected 5 words, got %d: %s", len(parts), p)
}
for _, word := range parts {
if word == "" {
t.Error("empty word in passphrase")
}
}
t.Logf("Sample passphrase: %s", p)
}
func TestRandomPassphraseSamples(t *testing.T) {
for i := 0; i < 5; i++ {
p, err := RandomPassphrase(5)
if err != nil {
t.Fatal(err)
}
fmt.Printf(" Sample %d: %s\n", i+1, p)
}
}