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	"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}