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:
@@ -1,5 +1,14 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.3.6 (2026-02-21)
|
||||
|
||||
**Human-friendly retrieval passwords**
|
||||
|
||||
- Retrieval passwords now use Hungarian word passphrases (e.g. `áldás-plazmid-palánta-süvítve-pócgém`) instead of 64-char hex strings.
|
||||
- Embedded 29K+ curated Hungarian word list (`hungarian.txt`) via go:embed; 5-word passphrases give ~74 bits of entropy.
|
||||
- New `configgen.RandomPassphrase(wordCount)` function; all 3 retrieval password generation sites updated.
|
||||
- API keys remain as hex (machine-to-machine, never typed by humans).
|
||||
|
||||
## v0.3.5 (2026-02-21)
|
||||
|
||||
**Recovery Endpoint & Customer Standing**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,7 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Generate credentials
|
||||
retrievalPassword, err := configgen.RandomHex(32)
|
||||
retrievalPassword, err := configgen.RandomPassphrase(5)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -493,7 +493,7 @@ func (s *Server) handleConfigPreview(w http.ResponseWriter, r *http.Request, cus
|
||||
|
||||
// handleConfigRegenPassword regenerates the retrieval password.
|
||||
func (s *Server) handleConfigRegenPassword(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
newPassword, err := configgen.RandomHex(32)
|
||||
newPassword, err := configgen.RandomPassphrase(5)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -633,7 +633,7 @@ func (s *Server) handleCreateConfigFromReport(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// Generate credentials
|
||||
retrievalPassword, _ := configgen.RandomHex(32)
|
||||
retrievalPassword, _ := configgen.RandomPassphrase(5)
|
||||
apiKey, _ := configgen.RandomHex(32)
|
||||
|
||||
cfg := &store.CustomerConfig{
|
||||
|
||||
Reference in New Issue
Block a user