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