1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package main
6
7import (
8 "crypto/rand"
9 "fmt"
10 "math/big"
11 "os"
12 "strings"
13 "time"
14
15 flag "github.com/spf13/pflag"
16)
17
18var (
19 version = ""
20 consonants = "bcdfghjklmnpqrstvwxyz"
21 vowels = "aeiou"
22 numbers = "0123456789"
23 separators = "-. "
24 newBase60Chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"
25 flagNB60 = flag.BoolP("nb60", "n", false, "Generate a 20-character password using the NewBase60 charset")
26 flagVersion = flag.BoolP("version", "v", false, "Print eow's version and exit")
27)
28
29func main() {
30 flag.Parse()
31 if *flagVersion {
32 fmt.Println("eow", version)
33 os.Exit(0)
34 }
35 if *flagNB60 {
36 fmt.Println(generateNB60())
37 os.Exit(0)
38 }
39 fmt.Println(generateFriendly())
40 os.Exit(0)
41}
42
43func generateNB60() string {
44 var sb strings.Builder
45 sb.Grow(20)
46
47 for i := 0; i < 20; i++ {
48 sb.WriteByte(newBase60Chars[randomInt(len(newBase60Chars))])
49 }
50
51 return sb.String()
52}
53
54func generateFriendly() string {
55 sets := make([]string, 3)
56 for i := 0; i < 3; i++ {
57 sets[i] = genFriendlySet()
58 }
59
60 result := strings.Join(sets, string(separators[randomInt(len(separators))]))
61
62 letterPos := randomInt(len(result))
63
64 // Generate a different position until a non-separator is found
65 for strings.Contains(separators, string(result[letterPos])) {
66 letterPos = randomInt(len(result))
67 }
68 existingLetter := result[letterPos]
69 var newLetter string
70 if strings.Contains(consonants, string(existingLetter)) {
71 newLetter = string(consonants[randomInt(len(consonants))])
72 } else if strings.Contains(vowels, string(existingLetter)) {
73 newLetter = string(vowels[randomInt(len(vowels))])
74 }
75 result = result[:letterPos] + strings.ToUpper(newLetter) + result[letterPos+1:]
76
77 // Generate a different position until a non-separator and non-uppercase-letter
78 // is found
79 numberPos := randomInt(len(result))
80 for strings.Contains(separators, string(result[numberPos])) || numberPos == letterPos {
81 numberPos = randomInt(len(result))
82 }
83 result = result[:numberPos] + string(numbers[randomInt(len(numbers))]) + result[numberPos+1:]
84
85 return result
86}
87
88func genFriendlySet() string {
89 set := make([]byte, 6)
90 set[0] = consonants[randomInt(len(consonants))]
91 set[1] = vowels[randomInt(len(vowels))]
92 set[2] = consonants[randomInt(len(consonants))]
93 set[3] = consonants[randomInt(len(consonants))]
94 set[4] = vowels[randomInt(len(vowels))]
95 set[5] = consonants[randomInt(len(consonants))]
96 return string(set)
97}
98
99func randomInt(max int) int {
100 n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
101
102 retries := 20
103
104 for err != nil && retries > 0 {
105 if retries < 20 {
106 time.Sleep(time.Duration(100) * time.Millisecond)
107 }
108 n, err = rand.Int(rand.Reader, big.NewInt(int64(max)))
109 retries--
110 }
111
112 if retries == 0 {
113 fmt.Println("Failed to generate random number:", err)
114 os.Exit(1)
115 }
116
117 return int(n.Int64())
118}