diff options
Diffstat (limited to 'makepass.sh')
-rwxr-xr-x | makepass.sh | 352 |
1 files changed, 275 insertions, 77 deletions
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 <git@dnns.no> # 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 <<EOL +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 script tries hard to be POSIX compliant. But it might not be? Let me + know if you find any errors! + +AUTHOR + Dennis Eriksen <https://dnns.no> +EOL + return 0 -) +} -makepass "${@:-}" +# RUN IT! +main "${@:-}" ## END OF FILE ################################################################# |