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
14 flag "github.com/spf13/pflag"
15)
16
17var (
18 version = ""
19 consonants = "bcdfghjklmnpqrstvwxyz"
20 vowels = "aeiou"
21 numbers = "0123456789"
22 separators = "-. "
23 newBase60Chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"
24 flagNB60 = flag.BoolP("nb60", "n", false, "Generate a 20-character password using the NewBase60 charset")
25 flagFriendly = flag.BoolP("friendly", "f", true, "Generate a 20-character password using an Apple-style friendly algorithm")
26)
27
28func main() {
29 flag.Parse()
30 if *flagNB60 {
31 fmt.Println(generateNB60())
32 os.Exit(0)
33 }
34 fmt.Println(generateFriendly())
35 os.Exit(0)
36}
37
38func generateNB60() string {
39 var sb strings.Builder
40 sb.Grow(20)
41
42 for i := 0; i < 20; i++ {
43 sb.WriteByte(newBase60Chars[randomInt(len(newBase60Chars))])
44 }
45
46 return sb.String()
47}
48
49func generateFriendly() string {
50 sets := make([]string, 3)
51 for i := 0; i < 3; i++ {
52 sets[i] = genFriendlySet()
53 }
54
55 result := strings.Join(sets, string(separators[randomInt(len(separators))]))
56
57 letterPos := randomInt(len(result))
58
59 // Generate a different position until a non-separator is found
60 for strings.Contains(separators, string(result[letterPos])) {
61 letterPos = randomInt(len(result))
62 }
63 existingLetter := result[letterPos]
64 var newLetter string
65 if strings.Contains(consonants, string(existingLetter)) {
66 newLetter = string(consonants[randomInt(len(consonants))])
67 } else if strings.Contains(vowels, string(existingLetter)) {
68 newLetter = string(vowels[randomInt(len(vowels))])
69 }
70 result = result[:letterPos] + strings.ToUpper(newLetter) + result[letterPos+1:]
71
72 // Generate a different position until a non-separator and non-uppercase-letter
73 // is found
74 numberPos := randomInt(len(result))
75 for strings.Contains(separators, string(result[numberPos])) || numberPos == letterPos {
76 numberPos = randomInt(len(result))
77 }
78 result = result[:numberPos] + string(numbers[randomInt(len(numbers))]) + result[numberPos+1:]
79
80 return result
81}
82
83func genFriendlySet() string {
84 set := make([]byte, 6)
85 set[0] = consonants[randomInt(len(consonants))]
86 set[1] = vowels[randomInt(len(vowels))]
87 set[2] = consonants[randomInt(len(consonants))]
88 set[3] = consonants[randomInt(len(consonants))]
89 set[4] = vowels[randomInt(len(vowels))]
90 set[5] = consonants[randomInt(len(consonants))]
91 return string(set)
92}
93
94func randomInt(max int) int {
95 n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
96 if err != nil {
97 fmt.Println("Error: could not generate a random number:", err)
98 os.Exit(1)
99 }
100 return int(n.Int64())
101}