main.go

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