#!/usr/bin/env bash
# password-gen.sh - generates strong passwords or passphrases offline.
# Uses the OS CSPRNG (/dev/urandom). Prints to the terminal only;
# nothing is written to disk. Paste straight into your password manager.
# Run: bash password-gen.sh [length] [count]
#      bash password-gen.sh -p [words] [count]

set -u

MODE="password"
if [ "${1:-}" = "-p" ] || [ "${1:-}" = "--passphrase" ]; then
  MODE="passphrase"
  WORDS="${2:-6}"
  COUNT="${3:-5}"
else
  LENGTH="${1:-20}"
  COUNT="${2:-5}"
fi

# Safety floor: never generate fewer characters than the guaranteed classes.
if [ "$MODE" = "password" ] && [ "$LENGTH" -lt 8 ]; then
  LENGTH=8
fi

rand_below() {
  # Unbiased random below $1 via rejection sampling on /dev/urandom.
  local limit=$((4294967296 - 4294967296 % $1))
  local v
  while :; do
    v=$(od -An -N4 -tu4 < /dev/urandom | tr -d ' ')
    [ "$v" -lt "$limit" ] && break
  done
  echo $((v % $1))
}

CHARS_LOWER="abcdefghijkmnopqrstuvwxyz"
CHARS_UPPER="ABCDEFGHJKLMNPQRSTUVWXYZ"
CHARS_DIGIT="23456789"
CHARS_SYMBOL='!@#$%^&*()-_=+[]{};:,.?/'
ALL="${CHARS_LOWER}${CHARS_UPPER}${CHARS_DIGIT}${CHARS_SYMBOL}"

WORDS_LIST="able acid also area army away baby back bake ball band bank base bath bear beat been bell belt bend best bird blue boat body boil bomb bond bone book boom born boss both bowl bulk burn busy cafe cage cake calm camp card care case cash cast cave chat chip city club coal coat code cold come cook cool cope copy core cost crew crop dark data date dawn dead deal dear debt deep desk dial diet disk dive dock does dog done door dose down draw drop drug dual duck dust duty each east easy echo edge else even ever face fact fair fall farm fast fear feed feel feet fell felt file fill film find fine fire firm fish five flat flow food foot form four free fuel full game gate gave gear gift girl give glad goal goes gold golf gone good gray grew grow hair half hall hand hard harm hate have head hear heat held hell help here hero high hill hire hold hole home hope hour huge hang iron item join jump just keep kept kick kind king knew know lack lady lake lamp land last late lead left leg less life lift like line link lion list live load loan lock long look lose loss love made mail main make male many mark mass meal mean meat meet menu mere mess mile milk mind mine miss mode mood more most move much must name near neck need news next nice note okay once only onto open over pace pack page pain pair park part past path peak pick pink pipe plan play plot plus poem pool poor port pose pull pure push race rail rain rank rare rate read real rest rice rich ride ring rise risk road rock role roll roof room rose rule rush safe sail sale same sand save seat seed seek seem seen sell send shop shot show side sign sing sink site size slow snow soft soil some song soon sort soul star stay step stop such talk tall task team tell term test text than that them then thin this time tiny tool tour town tree trip true turn type unit upon user vary vast view vote wait wake walk wall want warm wash wave ways weak wear week well went were west what when wide wife wild will wind wine wish wood word wore work yard year your zone"

pick_char() { printf '%s' "$1" | cut -c "$(( $(rand_below ${#1}) + 1 ))"; }

if [ "$MODE" = "passphrase" ]; then
  echo "Passphrases ($WORDS words, hyphen-separated):"  picked=()
  total_words=$(echo $WORDS_LIST | wc -w)
  while [ "${#picked[@]}" -lt "$WORDS" ]; do
    idx=$(rand_below "$total_words")
    w=$(echo $WORDS_LIST | cut -d' ' -f$((idx + 1)))
    skip=0
    for p in "${picked[@]}"; do
      [ "$p" = "$w" ] && skip=1 && break
    done
    [ "$skip" -eq 0 ] && picked+=("$w")
  done
  (IFS='-'; echo "${picked[*]}")
else
  echo "Passwords ($LENGTH characters):"
  i=0
  while [ "$i" -lt "$COUNT" ]; do
    # Guarantee one of each class first.
    chars=("$(pick_char "$CHARS_LOWER")" "$(pick_char "$CHARS_UPPER")" "$(pick_char "$CHARS_DIGIT")" "$(pick_char "$CHARS_SYMBOL")")
    while [ "${#chars[@]}" -lt "$LENGTH" ]; do
      chars+=("$(pick_char "$ALL")")
    done
    # Fisher-Yates shuffle using the CSPRNG.
    n=${#chars[@]}
    while [ "$n" -gt 1 ]; do
      n=$((n - 1))
      k=$(rand_below $((n + 1)))
      tmp="${chars[$n]}"
      chars[$n]="${chars[$k]}"
      chars[$k]="$tmp"
    done
    echo "${chars[*]}" | tr -d ' '
    i=$((i + 1))
  done
fi

echo ""
echo "Tip: generated in memory only. Paste directly into your password manager; do not save it to a text file."