#!/usr/bin/env zsh emulate zsh # Filename: ~/bin/makepass.zsh # Purpose: Creating random passwords. # Authors: Dennis Eriksen # 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. ################################################################################ # Copyright (c) 2018-2023 Dennis Eriksen • d@ennis.no # Help-function function _makepass_help() { local string='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 - 10 random strings with letters (both lower and upper case), numbers, and dashes and underscores. - Passwords with special characters - six random strings generated from lower and upper case letters, numbers, and the following characters: !#$%&/()=?+-_,.;:<>[]{}|\@* - Passphrases - if we find a dictionary, five 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. 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 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 NUM overrides that again. So `MAKEPASS_LENGTH=10 makepass -l 12 14` will give passwords that are 14 characters long. MAKEPASS_NUMBER The number of passwords from each group to output. This formula is used: 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. 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/urandom. This should provide enough randomnes to generate sufficiently secure passwords. AUTHOR Dennis Eriksen ' print -- $string return 0 } # Create random passphrase function passphrase() { setopt localoptions rematch_pcre local -i len=${1:-8} local prestring string # Put together $len random words, separated by '-' repeat $len prestring+=$words[$((RANDOM % $#words + 1))]'-' prestring=$prestring[1,-2] # remove trailing dash # This while-loop removes any characters NOT in '[^0-9a-zA-Z_-]' while [[ -n $prestring ]]; do if [[ $prestring =~ '[^0-9a-zA-Z_-]' ]]; then string+=${prestring[1,MBEGIN-1]} prestring=${prestring[MEND+1,-1]} else break fi done string+=$prestring # append the rest of $prestring printf '%s\n' $string; return } # Function to create random strings function randstring() { # Default is a number in the range local -i len=$(( $1 ? $1 : RANDOM % (RANGE_MAX - RANGE_MIN + 1) + RANGE_MIN )) local chars=${2:-$ALNUM} local flc=${3:-} # first-last-character local string repeat $len string+=$chars[$((RANDOM % $#chars + 1))] # If a third value is provided, it is used as the first and last character in # the string # TODO: Maybe this needs to be redone? Do it like in the perl-version? if [[ -n $flc ]]; then string=$flc[$((RANDOM % $#flc + 1))]$string[2,-2] (( len >= 2 )) && string+=$flc[$((RANDOM % $#flc + 1))] fi printf '%s\n' "$string"; return } # Function to die function die() { print -u2 -- "$@" print -u2 -- "Maybe try running \`$ZSH_SCRIPT -h\` for help" exit 1 } # main-function. This is where the magic happens function main() { setopt localoptions integer MAX_LENGTH=255 # max length of passwords integer RANGE_MAX=42 # max length when using random length integer RANGE_MIN=8 # min length when using random length integer PASS_WORDS=8 # number of words in passphrases readonly MAX_LENGTH RANGE_MAX RANGE_MIN PASS_WORDS readonly LOWER='abcdefghijklmnopqrstuvwxyz' readonly UPPER='ABCDEFGHIJKLMNOPQRSTUVWXYZ' readonly DIGIT='0123456789' readonly OTHER='!#$%&/()=?+-_,.;:<>[]{}|\@*' readonly ALPHA=${LOWER}${UPPER} readonly ALNUM=${ALPHA}${DIGIT} readonly EVERY=${ALNUM}${OTHER} integer length=${MAKEPASS_LENGTH:-0} # length of passwords integer number=${MAKEPASS_NUMBER:-10} # number of passwords local normal=${MAKEPASS_NORMAL:-$ALNUM'-_'} local special=${MAKEPASS_SPECIAL:-$EVERY} local wordlist=${MAKEPASS_WORDLIST:-/usr/share/dict/words} # Seed $RANDOM with a random 32bit integer from /dev/urandom local r4 # will be filled with 4 random bytes from /dev/urandom IFS= read -rk4 -u0 r4 < /dev/urandom || return local b1=$r4[1] b2=$r4[2] b3=$r4[3] b4=$r4[4] RANDOM=$(( #b1 << 24 | #b2 << 16 | #b3 << 8 | #b4 )) # Getopts while getopts 'hl:n:' opt; do case $opt in h) _makepass_help && return 0;; l) [[ ! $OPTARG = <0-255> ]] && die "-l takes a number between 0 and 255" length=$OPTARG;; n) [[ ! $OPTARG = <1-255> ]] && die "-n takes a number between 1 and 255" number=$OPTARG;; *) die "Unknown argument";; esac done shift $((OPTIND - 1)) # # Some error-checking # # We only take one argument in addition to the optargs (( ARGC > 1 )) && die "only one argument" # if there is an argument, set it to $length [[ -n $1 ]] && length=$1 # Check $length and $number [[ $length = <0-255> ]] || die "length must be a number between 0 and 255" [[ $number = <1-255> ]] || die "number-argument must be between 1 and 255" # # Print! # # Normal passwords print "Normal passwords:" print -c -- $(repeat $number { randstring $length $normal; : $RANDOM }) : $RANDOM print # Passowrds with special characters print "Passwords with special characters:" print -c -- $(repeat $((number/3*2+1)) { randstring $length $special $ALPHA; : $RANDOM }) : $RANDOM # Passphrases - but only if a wordlist is available if [[ -r $wordlist ]] && ((number / 2 > 0)); then print print "Passphrases:" local -a words=(${(f)"$(<$wordlist)"}) repeat $((number / 2)) passphrase fi } main "${@:-}" # Last time I redid this script I benchmarked some ways to generate random # strings. Here's the results: # subshell with tr | fold | head # % time (repeat 10000 { string=''; string=$(tr -cd '[:alpha:]'