// // Author : Dennis Eriksen // File : makepass.go // Created : 2023-09-05 // Licence : BSD-3-Clause // // Copyright (c) 2018-2023 Dennis Eriksen package main import ( "bufio" "flag" "fmt" "golang.org/x/term" "math/rand" "os" "strconv" "strings" "time" ) // Basic constant definitions const ( max = 255 // Maximum length of passwords rangeMax = 42 // Maximum range for password length rangeMin = 8 // Minimum range for password length passWords = 8 // Number of words in passphrases lower = "abcdefghijklmnopqrstuvwxyz" upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" digit = "0123456789" other = "!$%&#/()=?+-_,.;:<>[]{}|@*" // Special characters for special passwords defaultFile = "/usr/share/dict/words" // Default path to the file of words used for passphrases ) // Variables for different types of passwords var ( alpha []string = strings.Split(lower+upper, "") // Array of alphabets (lowercase and uppercase) alnum []string = append(alpha, strings.Split(digit, "")...) // Array of alphanumeric characters every []string = append(alnum, strings.Split(other, "")...) // Array of all characters (alphabet, digit and special) normal []string = append(alnum, "-", "_") // Characters for normal passwords special []string = every // Characters for special passwords wordlist string = defaultFile // Path to a dictionary to use for passphrases colWidth int = 1 colNum int = 1 length int = 0 // Length of passwords number int = 10 printBool bool = false printLen int = 0 help bool = false ) func main() { // define error-vars var errl error // err for length var errn error // err for number // Handle environment if len(os.Getenv("MAKEPASS_LENGTH")) > 0 { length, errl = strconv.Atoi(os.Getenv("MAKEPASS_LENGTH")) } if len(os.Getenv("MAKEPASS_NUMBER")) > 0 { number, errn = strconv.Atoi(os.Getenv("MAKEPASS_NUMBER")) } // Get wordlist from env if len(os.Getenv("MAKEPASS_WORDLIST")) > 0 { wordlist = os.Getenv("MAKEPASS_WORDLIST") } // // Flag handling // flag.BoolVar(&help, "h", help, "print helptext") flag.IntVar(&length, "l", length, "length of passwords to output\nmust be a number between 0 and "+strconv.Itoa(max)) flag.IntVar(&number, "n", number, "number of passwords to output\nmust be a number between 1 and "+strconv.Itoa(max)) flag.BoolVar(&printBool, "p", printBool, "print length of each password") flag.Parse() if help { printHelp() } // Handle cmd-line args that are not flags if len(flag.Args()) == 1 { length, errl = strconv.Atoi(flag.Arg(0)) } // // Error handling // // We take max one argument if len(flag.Args()) > 1 { fmt.Println("only one argument") os.Exit(1) } // If there is an error in conversion, or if the length is not within limits, exit the program with an error message if errl != nil || (length > max || length < 0) { fmt.Printf("length must be a number between 0 and %d.\n", max) os.Exit(1) } if errn != nil || (number < 1 || max < number) { fmt.Printf("number-argument must be between 1 and %d.\n", max) os.Exit(1) } // // move on // // initialise the random seed // TODO: Get seed from /dev/random, like we do in the perl- and zsh-versions? rand.Seed(time.Now().UnixNano()) // get screen width termWidth, _, err := term.GetSize(0) if err != nil { termWidth = 0 } // col width of printLen if printBool { // convert length to string and count length of string, and add one for space // In the other versions we add the space when calculating colNum, but go doesn't support ternary operators. This way we don't need an extra if-statement. Just subtract the space when printing the length in printColumns(). if length <= 100 { printLen = 2 + 1 } else { printLen = 3 + 1 } } else { printLen = 0 } // column width colWidth = rangeMax + 2 if length > 0 { colWidth = length + 2 } // number of colums colNum = termWidth / (colWidth + printLen) if colNum == 0 { colNum = 1 } // // print passwords // // Generate and print normal and special passwords printColumns("Normal passwords:", number, normal) fmt.Println("") printColumns("Passwords with special characters:", number/3*2+1, special) // Generate and print passphrases if wordlist exists if _, err := os.Stat(wordlist); err == nil { // Read wordlist from file file, err := os.Open(wordlist) if err != nil { fmt.Printf("failed to open file: %s", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) var words []string for scanner.Scan() { words = append(words, scanner.Text()) } file.Close() // Print it fmt.Println("") fmt.Println("Passphrases:") for i := 0; i < number/2; i++ { fmt.Println(passphrase(words)) } } } // Function to generate and print passwords func printColumns(title string, num int, chars []string) { var strings []string for i := 0; i < num; i++ { strings = append(strings, randstring(chars)) } // Print title fmt.Println(title) // Print passwords in neat columns for i, str := range strings { if printBool { fmt.Printf("%0[1]*[2]d ", printLen-1, len(str)) } fmt.Printf("%-[1]*[2]s", colWidth, str) if (i+1)%colNum == 0 || (i+1 == num && (i+1)%colNum > 0) { // Add newlines fmt.Println("") } } } // Function to generate a random string of given length from given characters func randstring(chars []string) string { l := length if length == 0 { // Random length if not specified l = rand.Intn(rangeMax-rangeMin+1) + rangeMin } var str strings.Builder // Add random characters to str for i := 1; i <= l; i++ { if i == 1 { // don't want passwords to start or end with special characters str.WriteString(alnum[rand.Intn(len(alpha))]) } else if i == l { str.WriteString(alnum[rand.Intn(len(alnum))]) } else { str.WriteString(chars[rand.Intn(len(chars))]) } } return str.String() } // Function to generate passphrases func passphrase(words []string) string { var str strings.Builder for i := 0; i < passWords; i++ { str.WriteString(words[rand.Intn(len(words))]) // Write a random word to the passphrase if i != passWords-1 { str.WriteString("-") // Add hyphen between words } } return str.String() } // Help-function func printHelp() { fmt.Println(`NAME makepass - create several random passwords SYNOPSIS makepass [OPTIONS] [NUM] If a NUM is provided, passwords will be NUM characters long. By default ` + "`makepass`" + ` will output passwords from the three following classes: - Normal passwords - random strings with letters (both lower and upper case), numbers, and dashes and underscores. - Passwords with special characters - random strings generated from lower and upper case letters, numbers, and the following characters: !#$%&/()=?+-_,.;:<>[]{}|@* - Passphrases - if we find a dictionary, a series of eight random words from the dictionary, separated by dashes. The number of words can not be changed, but you do not have to use all of them. Use as mane as you want. The first character will always be alphabetic, and the last will always be alphanumeric. DESCRIPTION makepass has the following options: -h output this help-text -l length of passwords. See MAKEPASS_LENGTH below -n number of passwords. See MAKEPASS_NUMBER below -p print length of number ENVIRONMENT makepass examines the following environmental variables. MAKEPASS_LENGTH Specifies the length of passwords. Valid values are 0-255. If 0, a random value between 8 and 42 will be used for each password. -l overrides this environmental variable, and the argument NUM overrides that again. So ` + "MAKEPASS_LENGTH=10 makepass -l 12 14" + ` will give passwords that are 14 characters long, even though both -l and MAKEPASS_LENGTH also specifies a length. MAKEPASS_NUMBER The number of passwords to generate. This formula is used to determine how many passwords from each group should be generated: - (n) normal passwords - (n / 3 * 2 + 1) special passwords - (n / 2) passphrases Where n is 10 by default. Valid values for n are 1-255. Floating-poing math is not used, so results may vary. MAKEPASS_PRINTLEN If 1, print length of all passwords. If 0, don\'t. MAKEPASS_NORMAL String of characters from which to generate "normal" passwords. Defaults to: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_ MAKEPASS_SPECIAL String of characters from which to generate passwords with special characters. Defaults to the same characters as in MAKEPASS_NORMAL, plus these: !#$%&/()=?+-_,.;:<>[]{}|@* MAKEPASS_WORDLIST Specifies the dictionary we find words for passphrases in. If this is unset or empty, we try "/usr/share/dict/words". If that file does not exist, no passphrases will be provided. NOTES This scripts makes use of $RANDOM - a builtin in zsh which produces a pseudo-random integer between 0 and 32767, newly generated each time the parameter is referenced. We initially seed the random number generator with a random 32bit integer generated from /dev/random. This should provide enough randomnes to generate sufficiently secure passwords. AUTHOR Dennis Eriksen `) os.Exit(0) }