#!/bin/bash # # passphrase_generator [option] # # Generate a passphrase using XKCD and HayStack Principles. # See http://www.ict.griffith.edu.au/anthony/info/crypto/passwd_generation.txt # # Options # -c Clean symbol separated words, no padding or pictograms. # Basically a clean XKCD style password of 4 words separated by # a symbol character. # # -o For old websites that insist on the older rules of at least: # an lowercase, uppercase, digit, and punctuation characters. # It does this by capitalising one word, and replacing another with # a 4 digit number. # # -d Debugging, report how the password evolves and changes # # You can use both -o and -c to generate a cleaner, old-style password # #### # # Script depends on the GNU program "shuf" (fedora 'coreutil' program) for # randomising permutations of arguments and files. # # Anthony Thyssen -- 18 August 2019 # #### # Discover where the shell script resides PROGNAME="${BASH_SOURCE##*/}" # script name (basename) PROGDIR="${BASH_SOURCE%/*}" # directory (dirname - may be relative) # Error/Usage/Help Routines Error() { # Just output an error condition and exit (no usage) echo >&2 "$PROGNAME:" "$@" exit 2 } Usage() { # Report error and Synopsis line only echo >&2 "$PROGNAME:" "$@" sed >&2 -n '1,2d; /^###/q; /^#/!q; /^#$/q; s/^# */Usage: /p;' \ "$PROGDIR/$PROGNAME" echo >&2 "For help use $PROGNAME --help" exit 10; } Help() { # Output Full header comments as documentation sed >&2 -n '1d; /^###/q; /^#/!q; s/^#*//; s/^ //; p' \ "$PROGDIR/$PROGNAME" exit 10; } Debug() { # Warning output can become shuffled, due to pipeline effects [[ -v DEBUG ]] && echo >&2 "Debug: $1" } # Option handling.. # Its a little strange, but then only one option is allowed. # And this way keeps the control variables together. # Option '-c' for a 'clean symbol seperated wordlist' # This variable is also used to disable later additions, # typcally done when a major complication has already been added. clean=0 # Do padding, unless told not to (using clean=99) # Option '-o' will ensure numbers and some capitals are used (old style) # variable is used as a multiplier to short-circuit some selections oldstyle=1 # do not short-circuit randomizations to zero # Process options while [ $# -gt 0 ]; do case "$1" in -\?|-help|--help) Help ;; # Standard help options. -c) clean=99 ;; # disable extra additions (simple 4word style) -o) oldstyle=0 ;; # ensure capitals and numbers are added -co|-oc)oldstyle=0; clean=99 ;; # both options (sic!) -d) DEBUG=true ;; # output password deveolpent stages --) shift; break ;; # forced end of user options -*) Usage "Unknown option \"$1\"" ;; *) break ;; # unforced end of user options esac shift # next option done [ $# -gt 0 ] && Usage "Too Many Arguments" # --------------------------------------------------------------------------- # # The dictionary source (adjust to suit) # # Default, a dictionary I put together (if present) dictionary="$HOME/lib/dict/dict_pwd_anthony" nwords=4 # 12Dict Project Dictionary, small common words dictionary "2+2+3cmn.txt" # http://wordlist.aspell.net/12dicts/ # The extracted list is about 12.8K words which is a good size for this! # Words are smaller in length, but that does not change the entropy. if [[ ! -f "$dictionary" ]]; then dictionary="$HOME/lib/dict/dict_12dict_cmn" nwords=4 fi # EFF Diceware Dictionary (EFF's Long Wordlist) # https://www.eff.org/dice # https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases # Smaller very common words, but not a big dict (7776 words) # But still makes for a very good password generator if [[ ! -f "$dictionary" ]]; then dictionary="$HOME/lib/dict/dict_diceware_eff_long" nwords=5 #dictionary="$HOME/lib/dict/dict_diceware_eff_short1" #dictionary="$HOME/lib/dict/dict_diceware_eff_short2" #nwords=6 fi # System Dictionary # 191K words extracted, making the shuffle a little slow. # Words are typically longer and very obsure, rather common easy to remember. if [[ ! -f "$dictionary" ]]; then dictionary=/usr/share/dict/words nwords=4 fi if [[ ! -f "$dictionary" ]]; then Error "Unable to find a word dictionary to use!" fi Debug "Dictionary: $dictionary" # ----------------------------------------------------------------------------- # Symbol Arrays # # Note: I don't like some symbols, as they are less visible between words # symbols=( @ ^ \* - _ + = : \| \~ . , / \\ ) # \# @ ! \? % \$ \& ) parenthesis=( '()' '[]' '{}' '<>' ) pictograms=( '-=#=-' '<=-=>' '__.oOo.__' '-/\-' '\_o_/' '-^.^-' ':-)' ';-)' ':-(' ':^)' 'O_o' '8D' ':::>=-' '>>--->' '@>-`-,-' '===8<--' '..><>' '><((*>' 'ABCdef' 'A1' 'AtoZ' '9to5' ) checkboxes=( '('{-,=,o,0}')' '['{-,=,+,x}']' '{'{-,=,+,x,\*}'}' '<'{-,+,x,^,v,V}'>' '/'{-,^,v}'\' '\'{-,^,v}'/' ) # DEBUG #echo "${pictograms[@]}"; exit #echo "${checkboxes[@]}"; exit # Select Symbols (they may not be used) sep=$( shuf -en1 -- "${symbols[@]}" ) par=$( shuf -en1 -- "${parenthesis[@]}" ) pad=$( shuf -en1 -- "${symbols[@]}" ) printf -v pads "%*s" 20; pads=${pads// /$pad} # repeated pad characters # Fix backslash for "paste -d" word joining (otherwise it breaks) [[ "$sep" == \\ ]] && sep='\\' # to replace first word in the list (50% total) with numbers or pictogram # If a pictogram is used, do not add further padding (clean) if (( RANDOM % 2 * oldstyle == 0)); then # 50% or oldstyle case $(( RANDOM % 6 * oldstyle )) in # 50% number (oldstyle), 50% pictogram [0-2]) # half the time numbers is used (forced if oldstyle set) pict=$( printf '%04d' $(( RANDOM % 10000 )) ) ;; # number (maybe forced) 3) pict="${pads:0:3}"; clean=9 ;; # 3 pads characters 4) pict="$( shuf -en1 -- "${pictograms[@]}" )"; clean=99 ;; # pictogram 5) pict="$( shuf -en1 -- "${checkboxes[@]}" )"; clean=99 ;; # checkbox esac fi Debug "Selected Symbols: sep:$sep pad:$pad par:$par pict:$pict" # ----------------------------------------------------------------------------- # # Pipelined password generation... # # Generate a random word list (XKCD style) # Remove any 'diceware numbers' at the start of lines sed 's/^[[:digit:]]\{1,\}[[:space:]]\{1,\}//' "$dictionary" | # We only want plain words, so limit size to 3 to 8 characters grep -E '^[[:alpha:]]{3,8}$' | # and shuffle select $nwords of those words shuf -n $nwords | # Output the initial word selection (not in final order) if [[ -v DEBUG ]]; then while read -r word; do echo "$word" words="$words $word" done Debug "Initial Words ($nwords): $words" else cat fi | #tee /dev/tty | # DEBUG: output the initial set of words # Adjust word case in some way (50% of the time, 100% if oldstyle). # Note, if Uppercase one work, do it to second word as the first may be # a pictogram or number. case $(( RANDOM % (oldstyle?18:9) )) in [0-2]) Debug "Uppercase second word" i=1 # the second word in list (first may be replaced) while read -r word; do (( i-- )) && echo "$word" || echo "${word^^}" done ;; [3-5]) Debug "Capitialise all words" sed 's/./\u&/' ;; 6) Debug "Capitialise second letter of all words" sed 's/\(.\)\(.\)/\1\u\2/' ;; 7) Debug "Capitialise third letter of all words" sed 's/\(..\)\(.\)/\1\u\2/' ;; 8) Debug "Capitialise last letter of all words" sed 's/\(.\)$/\u&/' ;; *) Debug "No case changes to words" # 50% cat ;; esac | # Replace first word of list with a pictagram or number (if selected above) # Then re-shuffle all the words to randomise position of the changed words if [[ "$pict" ]]; then Debug "Replace first word with number or pictogram" shuf -e -- $( tail -n+2 ) "$pict" else Debug "No number or pictogram replacement" shuf fi | # Join words using parenthesis or seperator (haystack) # Force use of seperator character if 'clean' case $(( RANDOM % 4 + clean )) in 0) Debug "Join shuffled words using parenthesis" # 25% while read -r word; do echo "$word" | paste -d"$par" /dev/null - /dev/null done | tr -d '[:space:]'; echo ;; *) Debug "Join shuffled words with seperator symbol" # 75% paste -d"$sep" -s # separator char (75% chance) #sed '1n; s/^/'"$sep"'/' | tr -d '[:space:]'; echo ;; esac | # Add some extra padding to start or end (40% chance, unless 'clean') case $(( RANDOM % 10 + clean )) in 0) Debug "Prepend some Padding" # 2 to 4 pad chars echo "${pads:0:((RANDOM%3+2))}$(cat)" ;; 1) Debug "Pad both ends" # 2 pad chars each echo "$pad$pad$(cat)$pad$pad" ;; 2) Debug "Append some Padding" # 3 to 6 pad chars echo "$(cat)${pads:0:((RANDOM%4+3))}" ;; 3) Debug "Append pictogram or checkbox" echo "$(cat)$( shuf -en1 -- "${pictograms[@]}" "${checkboxes[@]}" "${pictograms[@]}" )" ;; *) Debug "No Padding added" # clean style cat ;; esac