diff --git a/stow/bash/.bash_aliases b/stow/bash/.bash_aliases index bb9638f..6fe62d9 100644 --- a/stow/bash/.bash_aliases +++ b/stow/bash/.bash_aliases @@ -36,3 +36,156 @@ alias test_git_1p='echo "Testing Git 1Password credential helper for github.com: alias op_signin='op signin' alias op_list='op item list --format=json | jq -r ".[] | \"\(.title) - \(.id)\""' alias op_debug_item='function _op_debug() { op item get "$1" --format=json | jq -r ".fields[] | \"\(.label // .id): \(.value // \"empty\")\""; }; _op_debug' + + + +# Password generator: passgen +# Usage examples: +# passgen # 24 chars, a-zA-Z0-9, 1 password +# passgen -l 40 -c 5 # 5 passwords, length 40 +# passgen --symbols # include safe symbols for .env +# passgen --no-upper # remove uppercase +# passgen --exclude '$"' # exclude specific chars +# passgen --ambiguous # remove ambiguous chars (O0Il1) +# passgen --strict # enforce "at least one of each selected class" + +passgen() { + local length=24 count=1 + local use_upper=1 use_lower=1 use_digits=1 use_symbols=0 + local strict=0 ambiguous=0 + local exclude="" + local safe_symbols="!@#%^&*()_+.-" # .env-friendly (no quotes, $, `, \, =, :, space) + local symbol_set="$safe_symbols" # can be extended later if you want a "wide" set + + # parse args + while [[ $# -gt 0 ]]; do + case "$1" in + -l|--length) length="${2:-}"; shift 2 ;; + -c|--count) count="${2:-}"; shift 2 ;; + --no-upper) use_upper=0; shift ;; + --no-lower) use_lower=0; shift ;; + --no-digits) use_digits=0; shift ;; + --symbols) use_symbols=1; shift ;; + --no-symbols) use_symbols=0; shift ;; + --exclude) exclude="${2:-}"; shift 2 ;; + --ambiguous) ambiguous=1; shift ;; + --strict) strict=1; shift ;; + -h|--help) + cat <<'EOF' +passgen - flexible password generator + +Options: + -l, --length N Length of each password (default 24) + -c, --count N How many passwords to generate (default 1) + --no-upper Exclude A-Z + --no-lower Exclude a-z + --no-digits Exclude 0-9 + --symbols Include .env-safe symbols (!@#%^&*()_+.-) + --no-symbols Exclude symbols (default) + --exclude "xyz" Remove these characters from output + --ambiguous Remove ambiguous characters: O 0 I l 1 + --strict Ensure each selected class appears at least once + -h, --help Show this help + +Notes: +- Default charset: a-z A-Z 0-9 +- Symbols are .env-safe by default (no quotes, $, `, \, =, :, space) +EOF + return 0 + ;; + *) + echo "passgen: unknown option: $1" >&2; return 2 ;; + esac + done + + # sanity checks + [[ "$length" =~ ^[0-9]+$ ]] || { echo "passgen: length must be a number" >&2; return 2; } + [[ "$count" =~ ^[0-9]+$ ]] || { echo "passgen: count must be a number" >&2; return 2; } + + # build class strings explicitly (no tr bracket weirdness) + local UPPER="ABCDEFGHIJKLMNOPQRSTUVWXYZ" + local LOWER="abcdefghijklmnopqrstuvwxyz" + local DIGIT="0123456789" + local SYMS="$symbol_set" + + # optionally remove ambiguous chars + if (( ambiguous )); then + UPPER=${UPPER//O/} + UPPER=${UPPER//I/} + LOWER=${LOWER//l/} + DIGIT=${DIGIT//0/} + DIGIT=${DIGIT//1/} + fi + + # compose allowed set + local allowed="" + (( use_upper )) && allowed+="$UPPER" + (( use_lower )) && allowed+="$LOWER" + (( use_digits )) && allowed+="$DIGIT" + (( use_symbols ))&& allowed+="$SYMS" + + # if nothing selected, default to alnum + if [[ -z "$allowed" ]]; then + allowed="$UPPER$LOWER$DIGIT" + fi + + # remove any user-excluded chars from allowed (do this via tr to avoid globbing issues) + if [[ -n "$exclude" ]]; then + # printf each char of allowed, delete excluded via tr, then reassemble + allowed="$(printf %s "$allowed" | tr -d "$exclude")" + fi + + # final check + if [[ -z "$allowed" ]]; then + echo "passgen: allowed set is empty after exclusions; loosen options." >&2 + return 2 + fi + + # helper: generate one password meeting optional strict policy + _gen_one() { + local need_upper=$use_upper + local need_lower=$use_lower + local need_digit=$use_digits + local need_symbol=$use_symbols + local out="" + + # seed with at least one of each selected class if --strict + if (( strict )); then + if (( need_upper )); then out+="${UPPER:RANDOM%${#UPPER}:1}"; fi + if (( need_lower )); then out+="${LOWER:RANDOM%${#LOWER}:1}"; fi + if (( need_digit )); then out+="${DIGIT:RANDOM%${#DIGIT}:1}"; fi + if (( need_symbol )); then out+="${SYMS:RANDOM%${#SYMS}:1}"; fi + fi + + # fill the rest from the full allowed set, using /dev/urandom + local needed=$(( length - ${#out} )) + if (( needed > 0 )); then + # draw more than needed, then cut (helps when exclusions reduce size) + local draw=$(( needed * 3 )) + local extra="$(LC_ALL=C tr -dc "$allowed" 0; i-- )); do + # get a random index j using bytes from /dev/urandom + j=$(( $(od -An -N2 -tu2 < /dev/urandom) % (i+1) )) + tmp=${arr[i]}; arr[i]=${arr[j]}; arr[j]=$tmp + done + printf "%s" "${arr[*]}" | tr -d ' ' + } + + # generate the requested count + local i + for (( i=1; i<=count; i++ )); do + _gen_one + printf "\n" + done +}