There are many ways to generate passwords, and to avoid passwords in the first place (which you really should do), but I wanted a little practice in writing interactive bash scripts. So I chose to create a password generator based on the Diceware algorithm, even though I don’t carry any dice with me.

Find of the day is the excellent gum tool that makes it easy to create pretty user interaction. Thanks Ari!

#!/bin/bash
set -eo pipefail

# Ref: https://theworld.com/~reinhold/diceware.html
# Ref: https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases

if [[ ! -x "$(command -v curl)" ]]; then
  echo "Please install curl"
  exit 1
fi

if [[ ! -x "$(command -v shuf)" ]]; then
  echo "Please install coreutils"
  exit 1
fi

if [[ ! -x "$(command -v gum)" ]]; then
  echo "Please install https://github.com/charmbracelet/gum"
  exit 1
fi

opt_five='five dice, long wordlist'
url_five='https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt'
dst_five='eff_large_wordlist.txt'

opt_four_short='four dice, short wordlist'
url_four_short='https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt'
dst_four_short='eff_short_wordlist_1.txt'

opt_four_long='four dice, long wordlist'
url_four_long='https://www.eff.org/files/2016/09/08/eff_short_wordlist_2_0.txt'
dst_four_long='eff_short_wordlist_2_0.txt'

echo 'Which wordlist to use?'
WORDLIST=$(gum choose "$opt_five" "$opt_four_short" "$opt_four_long")
echo ">> $WORDLIST"

echo 'How many words to pick?'
COUNT=$(gum input --value='4')
echo ">> $COUNT"

echo 'Which word separator to use?'
SEPARATOR=$(gum choose 'dash' 'space' 'underscore')
echo ">> $SEPARATOR"

CACHE_DIR="${HOME}/.diceware"
if [[ ! -d "$CACHE_DIR" ]]; then
  mkdir "$CACHE_DIR"
fi

word_src="$url_five"
word_file="${dst_five}"
if [[ "$WORDLIST" == "$opt_four_short" ]]; then
  word_src="$url_four_short"
  word_file="$dst_four_short"
fi
if [[ "$WORDLIST" == "$opt_four_long" ]]; then
  word_src="$url_four_long"
  word_file="$dst_four_long"
fi

word_file="${CACHE_DIR}/${word_file}"
if [[ ! -f "$word_file" ]]; then
  curl -sSf "$word_src" -o "$word_file"
fi

dice='4'
if [[ "$WORDLIST" == "$opt_five" ]]; then
  dice='5'
fi

separator=' '
if [[ "$SEPARATOR" == "dash" ]]; then
  separator='-'
elif [[ "$SEPARATOR" == "underscore" ]]; then
  separator='_'
fi

phrase=''
for i in $(seq "$COUNT"); do
  group=''
  for j in $(seq "$dice"); do
    roll=$(shuf -i 1-6 -n 1 --random-source=/dev/random)
    if [[ -z "$group" ]]; then
      group="$roll"
    else
      group="${group}${roll}"
    fi
  done
  word=$(grep "$group" "$word_file" | awk '{print $2}')
  if [[ -z "$phrase" ]]; then
    phrase="$word"
  else
    phrase="${phrase}${separator}${word}"
  fi
done

gum style \
  --border double --align center \
  --margin '1 2' --padding '2 4' \
  "$phrase"

Sample output:

$> ./passphrase.sh
Which wordlist to use?
>> four dice, long wordlist
How many words to pick?
>> 4
Which word separator to use?
>> dash

  ╔═════════════════════════════════════════╗
  ║                                         ║
  ║                                         ║
  ║    jawbreaker-saffron-bullfrog-kiosk    ║
  ║                                         ║
  ║                                         ║
  ╚═════════════════════════════════════════╝

Hope you find this useful.