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}