aboutsummaryrefslogtreecommitdiffstats
path: root/makepass.sh
diff options
context:
space:
mode:
Diffstat (limited to 'makepass.sh')
-rwxr-xr-xmakepass.sh352
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 #################################################################