diff options
author | Dennis Eriksen <d@ennis.no> | 2023-11-10 09:54:17 +0100 |
---|---|---|
committer | Dennis Eriksen <d@ennis.no> | 2023-11-10 09:54:17 +0100 |
commit | cf84fad0955ff07dd6fe3ec30a17dc134e23c356 (patch) | |
tree | 6f85e6017e7a5feacd88270cb942e4e2e007ffcb /src | |
parent | Adding a python-verion (diff) | |
download | makepass-cf84fad0955ff07dd6fe3ec30a17dc134e23c356.tar.gz |
trying out new project structure
Diffstat (limited to 'src')
-rw-r--r-- | src/go/go.mod | 7 | ||||
-rw-r--r-- | src/go/go.sum | 4 | ||||
-rw-r--r-- | src/go/main.go | 331 | ||||
-rw-r--r-- | src/rust/Cargo.lock | 363 | ||||
-rw-r--r-- | src/rust/Cargo.toml | 14 | ||||
-rw-r--r-- | src/rust/src/main.rs | 393 |
6 files changed, 1112 insertions, 0 deletions
diff --git a/src/go/go.mod b/src/go/go.mod new file mode 100644 index 0000000..108f34b --- /dev/null +++ b/src/go/go.mod @@ -0,0 +1,7 @@ +module makepass.go + +go 1.20 + +require golang.org/x/term v0.12.0 + +require golang.org/x/sys v0.12.0 // indirect diff --git a/src/go/go.sum b/src/go/go.sum new file mode 100644 index 0000000..8c01403 --- /dev/null +++ b/src/go/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= diff --git a/src/go/main.go b/src/go/main.go new file mode 100644 index 0000000..a5241a3 --- /dev/null +++ b/src/go/main.go @@ -0,0 +1,331 @@ +// +// Author : Dennis Eriksen <d@ennis.no> +// File : makepass.go +// Created : 2023-09-05 +// Licence : BSD-3-Clause +// +// Copyright (c) 2018-2023 Dennis Eriksen <d@ennis.no> + +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 <https://dnns.no>`) + os.Exit(0) + +} diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock new file mode 100644 index 0000000..50747ce --- /dev/null +++ b/src/rust/Cargo.lock @@ -0,0 +1,363 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "once_cell", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.13", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "makepass" +version = "0.1.0" +dependencies = [ + "clap", + "rand", + "terminal_size", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.7", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.23", + "windows-sys", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..181b7a7 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "makepass" +authors = ["Dennis Eriksen <d@ennis.no>"] +version = "0.1.0" +edition = "2021" +rust-version = "1.68" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +rand = "0.8.4" +clap = { version = "4.3.24", features = ["cargo"] } +terminal_size = "0.2.6" diff --git a/src/rust/src/main.rs b/src/rust/src/main.rs new file mode 100644 index 0000000..083a99f --- /dev/null +++ b/src/rust/src/main.rs @@ -0,0 +1,393 @@ +// +// Author : Dennis Eriksen <d@ennis.no> +// File : main.rs +// Bin : makepass.rs +// Created : 2023-09-05 +// Licence : BSD-3-Clause +// +// Copyright (c) 2018-2023 Dennis Eriksen <d@ennis.no> + +// +// Imports +// +use clap::{Arg, ArgAction, Command, crate_authors, value_parser}; +use rand::{seq::SliceRandom, Rng}; +use std::env; +use std::fs; +use std::io; +use std::io::prelude::*; +use std::process::exit; +use terminal_size::{terminal_size, Width}; + +// +// Constants +// +const MAX: u8 = 255; +const RANGE_MAX: u8 = 42; +const RANGE_MIN: u8 = 8; +const PASS_WORDS: u8 = 8; + +// Character sets +const LOWER: &str = "abcdefghijklmnopqrstuvwxyz"; +const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const DIGIT: &str = "0123456789"; +const OTHER: &str = "!$%&#/()=?+-_,.;:<>[]{}|@*"; +const EXTRA: &str = "-_"; + +// Defaults +const DEFAULT_LENGTH: u8 = 0; +const DEFAULT_NUMBER: u8 = 10; +const DEFAULT_WORDLIST: &str = "/usr/share/dict/words"; + +// Structure used to pass common data to the print-function +struct CommonPrintData<'a> { + length: u8, + alpha: &'a [char], + alnum: &'a [char], + printbool: bool, + printlen: u8, + col_width: u8, + col_num: u16, +} + +// +// Main function. This is where the magic happens +// +fn main() { + // Construct arrays of the character sets + let lower = LOWER.chars().collect::<Vec<_>>(); + let upper = UPPER.chars().collect::<Vec<_>>(); + let digit = DIGIT.chars().collect::<Vec<_>>(); + let other = OTHER.chars().collect::<Vec<_>>(); + let extra = EXTRA.chars().collect::<Vec<_>>(); + let alpha = [&lower[..], &upper[..]].concat(); + let alnum = [&alpha[..], &digit[..]].concat(); + let every = [&alnum[..], &other[..]].concat(); + + // These are the sets we actually use + let mut normal = [&alnum[..], &extra[..]].concat(); + let mut special = every; + + // Set the defaults + let mut length = DEFAULT_LENGTH; + let mut number = DEFAULT_NUMBER; + let mut wordlist = DEFAULT_WORDLIST.to_string(); + + // + // env + // Deal with env before cli-args, because cli-args take precedence. + // + let env = ( + env::var("MAKEPASS_LENGTH").unwrap_or_default(), + env::var("MAKEPASS_NUMBER").unwrap_or_default(), + env::var("MAKEPASS_WORDLIST").unwrap_or_default(), + env::var("MAKEPASS_NORMAL").unwrap_or_default(), + env::var("MAKEPASS_SPECIAL").unwrap_or_default(), + ); + + // length + if !env.0.is_empty() { + length = env.0.parse().unwrap_or_else(|_| { + // length is a <u8>, which is 0-255, so no need for further error-checking + eprintln!( + "Error: MAKEPASS_LENGTH is not a valid number. Valid numbers are between 0 and 255" + ); + exit(1); + }); + } + // number + if !env.1.is_empty() { + // number is a <u8>, but takes a minimum value of 1 + match env.1.parse::<u8>() { + Ok(n) if n >= 1 => number = n, + _ => { + eprintln!("Error: MAKEPASS_NUMBER is not a valid number. Valid numbers are between 1 and 255"); + exit(1); + } + } + } + if !env.2.is_empty() { // wordlist + wordlist = env.2; + } + if !env.3.is_empty() { // Overwrite normal character array if MAKEPASS_NORMAL is present + normal = env.3.chars().collect::<Vec<_>>(); + } + if !env.4.is_empty() { // Overwrite special character array if MAKEPASS_SPECIAL is present + special = env.4.chars().collect::<Vec<_>>(); + } + + // + // Args + // + let opts = cli().get_matches(); + + // Set to new value if exists, or just set to old value + length = *opts.get_one::<u8>("length_flag").unwrap_or(&length); + number = *opts.get_one::<u8>("number").unwrap_or(&number); + length = *opts.get_one::<u8>("length_arg").unwrap_or(&length); + let printbool = opts.get_flag("printlen"); + + // + // Other logic + // + let printlen: u8 = if printbool && length < 100 { + 2 + } else if printbool { + 3 + } else { + 0 + }; + + // Get the width of the terminal + let size = terminal_size(); + let term_width: u16 = match size { + Some((Width(w), _)) => w, + None => 1, + }; + + // Calculate how wide each column has to be + let col_width: u8 = match length { + 0 => RANGE_MAX + 2, + _ => length + 2, + }; + + // Number of columns to print + // If printbool is true, col_num is term_width / (col_width + printlen + 1) + // Else, col_num is term_width / col_width + // col_num is never 0. Use std::cmp::max to use the largest value of 1 and the above. + let col_num: u16 = std::cmp::max( + 1, + match printbool { + true => term_width / (col_width + printlen + 1) as u16, + false => term_width / col_width as u16, + }, + ); + + // Fill in common print data + let data = CommonPrintData { + length: length, + alpha: &alpha, + alnum: &alnum, + printbool: printbool, + printlen: printlen, + col_width: col_width, + col_num: col_num, + }; + + // + // Do the dirty! + // + + // Normal passwords + print_columns("Normal passwords", number, &normal, &data); + + println!(""); + + // Special passwords + print_columns( + "Passwords with special characters", + number / 3 * 2 + 1, + &special, + &data, + ); + + // Passphrases + if fs::metadata(&wordlist).is_ok() { + // Open the file + let file = fs::File::open(&wordlist).unwrap(); + + // Create a buffered reader to handle file contents + let reader = io::BufReader::new(file); + + // Read words into a Vector + let words = reader.lines().map(|l| l.unwrap()).collect(); + + // Print + println!(""); + println!("Passphrases:"); + + for _ in 0..number / 2 { + println!("{}", passphrase(&words)); + } + } +} + +// +// Print passwords in neat columns +// +fn print_columns(title: &str, num: u8, chars: &Vec<char>, data: &CommonPrintData) { + let mut strings: Vec<String> = Vec::new(); + for _ in 0..num { + strings.push(randstring(data.length, chars, data.alpha, data.alnum)); + } + + println!("{}:", title); + + let mut i: u16 = 0; + for s in &strings { + i += 1; + + if data.printbool { + print!("{1:00$} ", data.printlen as usize, s.len()); + } + print!("{1:<0$}", data.col_width as usize, s); + if i % data.col_num == 0 || (i == num as u16 && i % data.col_num > 0) { + println!(); + } + } +} + +// +// Generate random strings to use as passwords +// +fn randstring(len: u8, chars: &[char], alpha: &[char], alnum: &[char]) -> String { + let mut rng = rand::thread_rng(); + let length = if len == 0 { + rng.gen_range(RANGE_MIN..=RANGE_MAX) + } else { + len + }; + + let mut s = String::with_capacity(length as usize); + for i in 0..length { + let c = if i == 0 { + alpha.choose(&mut rng) + } else if i == length - 1 { + alnum.choose(&mut rng) + } else { + chars.choose(&mut rng) + }; + s.push(*c.unwrap()); + } + s +} + +// +// Generate passphrases +// +fn passphrase(wordlist: &Vec<String>) -> String { + let mut rng = rand::thread_rng(); + let mut passphrase = Vec::new(); + + for _ in 0..PASS_WORDS { + let word = wordlist.choose(&mut rng).unwrap().clone(); + passphrase.push(word); + } + + passphrase.join("-") +} + +// +// Handle command line arguments +// +fn cli() -> Command { + Command::new("makepass") + .about("makepass - create random passwords") + .author(crate_authors!()) + .arg( + Arg::new("length_flag") + .short('l') + .value_name("LENGTH") + .help(format!("Length of passwords (0..={0}). 0 = random.", MAX)) + .value_parser(value_parser!(u8).range(0..)), + ) + .arg( + Arg::new("number") + .short('n') + .value_name("NUM") + .help(format!("Number of passwords (1..={0}). [default: 10]", MAX)) + .value_parser(value_parser!(u8).range(1..)) + .long_help("this is the long help. What is it?"), + ) + .arg( + Arg::new("printlen") + .short('p') + .help("Print length") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("length_arg") + .value_name("LENGTH") + .help(format!( + "Length of passwords (0..={MAX}). 0 = random length." + )) + .value_parser(value_parser!(u8).range(0..)), + ) + .override_help("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. + +AUTHOR + Dennis Eriksen <https://dnns.no>") +} |