From 1b58c2a48083281fc1901221ec2571bc58902e93 Mon Sep 17 00:00:00 2001 From: Dennis Eriksen Date: Mon, 11 Sep 2023 20:56:52 +0200 Subject: Redid the sh-version. It's almost twice as fast now! Had to use some hacks because POSIX sh don't support arrays. Well, it supports *one*... Still needs some cleanup, but it now has the same functionality as the others! --- README.md | 30 +++--- makepass.sh | 352 +++++++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 290 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 21bb9ce..eee6ee2 100644 --- a/README.md +++ b/README.md @@ -163,31 +163,31 @@ Here are the results so far: ``` % hyperfine --time-unit=millisecond --warmup=1 --shell=none ./makepass.* Benchmark 1: ./makepass.bash - Time (mean ± σ): 75.5 ms ± 2.1 ms [User: 32.1 ms, System: 38.2 ms] - Range (min … max): 71.9 ms … 79.5 ms 39 runs + Time (mean ± σ): 75.4 ms ± 1.8 ms [User: 29.5 ms, System: 37.9 ms] + Range (min … max): 71.4 ms … 78.3 ms 39 runs Benchmark 2: ./makepass.go - Time (mean ± σ): 6.9 ms ± 0.2 ms [User: 1.5 ms, System: 4.1 ms] - Range (min … max): 6.4 ms … 8.2 ms 432 runs + Time (mean ± σ): 7.0 ms ± 0.2 ms [User: 1.6 ms, System: 4.8 ms] + Range (min … max): 6.4 ms … 7.5 ms 424 runs Benchmark 3: ./makepass.pl - Time (mean ± σ): 38.6 ms ± 0.3 ms [User: 16.8 ms, System: 20.8 ms] - Range (min … max): 38.0 ms … 39.4 ms 77 runs + Time (mean ± σ): 38.7 ms ± 0.3 ms [User: 18.8 ms, System: 18.5 ms] + Range (min … max): 38.1 ms … 39.9 ms 78 runs Benchmark 4: ./makepass.sh - Time (mean ± σ): 1138.7 ms ± 15.4 ms [User: 268.0 ms, System: 1875.0 ms] - Range (min … max): 1114.3 ms … 1157.5 ms 10 runs + Time (mean ± σ): 624.1 ms ± 2.5 ms [User: 159.0 ms, System: 903.0 ms] + Range (min … max): 620.1 ms … 626.7 ms 10 runs Benchmark 5: ./makepass.zsh - Time (mean ± σ): 26.3 ms ± 0.7 ms [User: 12.1 ms, System: 11.6 ms] - Range (min … max): 24.7 ms … 28.3 ms 112 runs + Time (mean ± σ): 26.3 ms ± 0.6 ms [User: 12.2 ms, System: 11.1 ms] + Range (min … max): 25.1 ms … 27.8 ms 114 runs Summary './makepass.go' ran - 3.80 ± 0.16 times faster than './makepass.zsh' - 5.57 ± 0.19 times faster than './makepass.pl' - 10.89 ± 0.47 times faster than './makepass.bash' - 164.37 ± 5.94 times faster than './makepass.sh' + 3.76 ± 0.12 times faster than './makepass.zsh' + 5.53 ± 0.13 times faster than './makepass.pl' + 10.78 ± 0.36 times faster than './makepass.bash' + 89.19 ± 2.10 times faster than './makepass.sh' ``` ## Versions @@ -206,7 +206,7 @@ $ go build -o ../makepass.go -C go/ -ldflags "-s -w" makepass.go Perl version. I like this version <3 ### Shell -Needs more work. *Should* be pure POSIX sh. +*Should* be pure POSIX sh. Needs some cleanup. ### Zsh This is currently the "main" version. It uses pure zsh, with no forking out to diff --git a/makepass.sh b/makepass.sh index bb56aef..41d6918 100755 --- a/makepass.sh +++ b/makepass.sh @@ -5,108 +5,306 @@ # Bug-Reports: Email # License: This file is licensed under the BSD 3-Clause license. ################################################################################ -# This file takes randomness from /dev/urandom and turns it into random -# passwords. -# -# This particualr script is meant to be fully POSIX compatible. +# This script generates random passwords. ################################################################################ +# Copyright (c) 2018-2023 Dennis Eriksen • d@ennis.no +# +# TODO: Clean up and comment. +MAX=255 # max length of passwords +RANGE_MAX=42 # max length when using random length +RANGE_MIN=8 # min length when using random length +PASS_WORDS=8 # number of words in passphrases -# Copyright (c) 2018-2023 Dennis Eriksen • d@ennis.no +LOWER='abcdefghijklmnopqrstuvwxyz' +UPPER='ABCDEFGHIJKLMNOPQRSTUVWXYZ' +DIGIT='0123456789' +OTHER='!#$%&/()=?+_,.;:<>[]{}|@*-' +ALPHA=${LOWER}${UPPER} +ALNUM=${ALPHA}${DIGIT} +EVERY=${ALNUM}${OTHER} + +LENGTH=${MAKEPASS_LENGTH:-0} # length of passwords. 0 means "random number between RANGE_MIN and RANGE_MAX" +NUMBER=${MAKEPASS_NUMBER:-10} # number of passwords +PRINTLEN=${MAKEPASS_PRINTLEN:-0} # print length of passwords +NORMAL=${MAKEPASS_NORMAL:-$ALNUM'_-'} +SPECIAL=${MAKEPASS_SPECIAL:-$EVERY} +WORDLIST=${MAKEPASS_WORDLIST:-/usr/share/dict/words} +XWIDTH=$(tput cols) + +# +# Functions +# # makepass-function -makepass() { - MAKEPASS_WORDLIST=${MAKEPASS_WORDLIST:-/usr/share/dict/words} +main() { + + # Getopts + while getopts 'hl:n:p' opt; do + case $opt in + h) + help && return 0;; + l) + if ! int_in_range "$OPTARG" 0 "$MAX"; then die "-l takes a number between 0 and $MAX"; fi + LENGTH=$OPTARG;; + n) + if ! int_in_range "$OPTARG" 1 "$MAX"; then die "-n takes a number between 1 and $MAX"; fi + NUMBER=$OPTARG;; + p) + PRINTLEN=1;; + *) + die "Unknown argument";; + esac + done + shift $((OPTIND - 1)) - # We only take one argument - [ "$#" -gt 1 ] && printf '%s\n' 'only one argument' && return 1 - # check if $1 is a number or whatnot - if [ -n "$1" ]; then - if ! printf '%d' "$1" >/dev/null 2>&1; then printf '%s\n' 'not a number' && return 1; fi - if [ "$1" -le 0 ] || [ "$1" -ge 255 ]; then printf '%s\n' 'not a number above 0'; return 1; fi + # + # Some error-checking + # + + if [ "$#" -gt 1 ]; then die "only one argument"; fi + + if [ -n "$1" ]; then LENGTH=$1; fi + + # Check $LENGTH and $NUMBER + if ! int_in_range "$LENGTH" 0 "$MAX"; then die "length must be a number between 0 and $MAX"; fi + if ! int_in_range "$NUMBER" 0 "$MAX"; then die "number-argument must be between and $MAX"; fi + + + # + # Some other work + # + + if [ "$PRINTLEN" -gt 0 ]; then + if [ "$LENGTH" -lt 100 ]; then + PRINTLEN=3 + else + PRINTLEN=4 + fi fi - # Go! - len=$1 + COL_WIDTH=$(( ( LENGTH ? LENGTH : RANGE_MAX ) + 2 )) + if [ "$LENGTH" -gt 0 ]; then + COL_WIDTH=$((LENGTH + 2)) + else + COL_WIDTH=$((RANGE_MAX + 2)) + fi + COL_NUM=$(( XWIDTH / ( COL_WIDTH + PRINTLEN) )) - printf '%s\n' 'Normal passwords:' - i=0 - while [ $i -lt 10 ]; do - _random "${len:-}" 'A-Z-a-z-0-9_-' true - i=$((i + 1)) - done | column - printf '\n' - printf '%s\n' 'Passwords with special characters:' - i=0 - while [ $i -lt 6 ]; do - _random "${len:-}" '!#$%&/()=?+-_,.;:<>[]{}|\@*^A-Z-a-z-0-9' true - i=$((i + 1)) - done | column - - if [ -r "${MAKEPASS_WORDLIST}" ]; then - printf '\n' - printf '%s\n' 'Passphrases:' - lines=$(wc -l < "${MAKEPASS_WORDLIST}") + + # + # Print! + # + + + print_columns "Normal passwords" "$NUMBER" "$NORMAL" + + echo "" + + print_columns "Passwords with special characters" "$((NUMBER*2/3))" "$SPECIAL" + + if [ -r "$WORDLIST" ]; then + echo "" + echo "Passphrases:" + + LINES=$(wc -l < "$WORDLIST") + i=0 - while [ $i -lt 5 ]; do - # shuf is the best solution here, but it is very much not portable. - #words=$(shuf -n 8 "${MAKEPASS_WORDLIST}" | tr '\n' '-' | tr -dc '_A-Z-a-z-0-9') - words="" - j=0 - while [ $j -lt 8 ]; do - words="${words}-$(sed -n $(($(_RANDOM) % lines + 1))p "${MAKEPASS_WORDLIST}" | tr -dc 'A-Z-a-z-0-9_-')" - j=$((j + 1)) - done - printf '%s\n' "${words#-}" - i=$((i + 1)) - done + while [ "$i" -lt $((NUMBER / 2)) ]; do + passphrase + i=$((i+=1)) + done; fi - unset len i lines j words - return 0 -} -# get random number -_RANDOM() { - N=0 - while [ ! "$N" = "${N#0}" ]; do - N=$(head -n 100 /dev/urandom | tr -cd "[:digit:]" | tail -c 8) - done - printf '%s\n' "$N" && return 0 } -# Function to create random stuff -_random() ( - # sh does not like leading zeroes when doing math with numbers. Let's reset until we have a number that doesn't start with 0. +# Function to print passwords in neat columns +print_columns() { + title=$1 + num=$2 + chars=$3 - # Default is a number between 8 and 44 - len=${1:-$(($(_RANDOM) % (44 - 8 + 1) + 8))} + # Abuse `set` to get arrays! + # shellcheck disable=SC2046 # we want the spaces to separate items + set -- $(randstrings "$chars" "$num") - # Default to [:alnum:] if no chars are specified - chars=${2:-'[:alnum:]'} + echo "${title}:" - # First-Last-Alpha - if you want the first and the last letter to be from [:alpha:] - fla=${3:-'false'} + i=0 + for s in "$@"; do + i=$((i+=1)) + if [ "$PRINTLEN" -gt 0 ]; then printf "%0$((PRINTLEN-1))i " "${#s}"; fi + printf "%-${COL_WIDTH}s" "$s" + if [ "$(( i % COL_NUM))" -eq 0 ]; then + echo "" + elif [ "$i" -eq "$num" ] && [ "$((i % COL_NUM))" -gt 0 ]; then + echo ""; + fi + done +} - if [ "$fla" = "true" ]; then - if [ "$len" -le 2 ]; then - string="$(head -n 10 /dev/urandom | tr -cd '[:alpha:]' | tail -c "$len")" +# Function to create random strings +randstrings() { + i=0 + num=$2 + chars="${1:-$NORMAL}" + while [ "$i" -lt "$num" ]; do + if [ "$LENGTH" -gt 0 ]; then + len="$LENGTH" else - string="$(head -n 10 /dev/urandom | tr -cd '[:alpha:]' | tail -c 1)" - string="${string}$(head -n 100 /dev/urandom | tr -cd "$chars" | tail -c $((len-2)))" - string="${string}$(head -n 10 /dev/urandom | tr -cd '[:alpha:]' | tail -c 1)" + r=$(_RANDOM) + len=$(( r % (RANGE_MAX - RANGE_MIN + 1) + RANGE_MIN )) fi - else - string="$(head -n 100 /dev/urandom | tr -cd "$chars" | tail -c "$len")" - fi - printf '%s\n' "$string" - unset len chars fla string + string=$(head -n 2 /dev/random | tr -dc '[:alpha:]' | tail -c1) + string=${string}$(head -n 100 /dev/random | tr -dc "$chars" | tail -c $((len-2))) + if [ "$len" -gt 2 ]; then + string=${string}$(head -n 2 /dev/random | tr -dc '[:alnum:]' | tail -c1) + fi + + printf "%s " "$string" + i=$((i+=1)) + done +} + +_RANDOM() { + #N=$(head -n 2 /dev/urandom | tr -cd "[:digit:]" | tail -c 8 | sed 's/^[0]*//') + N=$(od -An -N2 /dev/random | sed 's/[ ]*[0]*//') + + printf "%s" "$N" +} + +# PASSPHRASE!! +passphrase() { + #string="" + #j=0 + #while [ "$j" -lt "$PASS_WORDS" ]; do + # #r=$((($(_RANDOM) % LINES ) + 1)) + # #string="${string}$(sed -n "${r}p" "$WORDLIST")-" + # j=$((j+=1)) + #done + r=$(_RANDOM) + string="$(awk "BEGIN{srand($r);}{a[NR]=\$0}END{for(i=1; i<$PASS_WORDS; i++){x=int(rand()*NR) + 1; print a[x];}}" "$WORDLIST" | xargs printf '%s-')" + + printf "%s\n" "${string%?}" +} + +# Check if int and if in range +int_in_range() { + num=$1 + min=$2 + max=$3 + + # Abuse case for regex-matching of numbers with only POSIX + case "$num" in + (*[!0-9]*|'') return 1;; + (*) if [ "$min" -le "$num" ] && [ "$num" -le "$max" ]; then + return 0 + fi ;; + esac + return 1 +} + +# Function to die +die() { + echo -- "$@" + echo -- "Maybe try running \`makepass -h\` for help" + exit 1 +} + +# Help-function +help() { + cat <[]{}|@* + + - 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 script tries hard to be POSIX compliant. But it might not be? Let me + know if you find any errors! + +AUTHOR + Dennis Eriksen +EOL + return 0 -) +} -makepass "${@:-}" +# RUN IT! +main "${@:-}" ## END OF FILE ################################################################# -- cgit v1.2.3