diff --git a/bin/.local/bin/pash b/bin/.local/bin/pash new file mode 100755 index 0000000..bb0d3f9 --- /dev/null +++ b/bin/.local/bin/pash @@ -0,0 +1,242 @@ +#!/bin/sh +# +# pash - simple password manager. +# Licensed under the MIT License + +pw_add() { + name=$1 + + if yn "Generate a password?"; then + # Generate a password by reading '/dev/urandom' with the + # 'tr' command to translate the random bytes into a + # configurable character set. + # + # The 'dd' command is then used to read only the desired + # password length. + # + # Regarding usage of '/dev/urandom' instead of '/dev/random'. + # See: https://www.2uo.de/myths-about-urandom + pass=$(LC_ALL=C tr -dc "${PASH_PATTERN:-_A-Z-a-z-0-9}" < /dev/urandom | + dd ibs=1 obs=1 count="${PASH_LENGTH:-50}" 2>/dev/null) + + else + # 'sread()' is a simple wrapper function around 'read' + # to prevent user input from being printed to the terminal. + sread pass "Enter password" + sread pass2 "Enter password (again)" + + # Disable this check as we dynamically populate the two + # passwords using the 'sread()' function. + # shellcheck disable=2154 + [ "$pass" = "$pass2" ] || die "Passwords do not match" + fi + + [ "$pass" ] || die "Failed to generate a password" + + # Mimic the use of an array for storing arguments by... using + # the function's argument list. This is very apt isn't it? + if [ "$PASH_KEYID" ]; then + set -- --trust-model always -aer "$PASH_KEYID" + else + set -- -c + fi + + # Use 'gpg' to store the password in an encrypted file. + # A heredoc is used here instead of a 'printf' to avoid + # leaking the password through the '/proc' filesystem. + # + # Heredocs are sometimes implemented via temporary files, + # however this is typically done using 'mkstemp()' which + # is more secure than a leak in '/proc'. + "$gpg" "$@" -o "$name.gpg" <<-EOF && + $pass + EOF + printf '%s\n' "Saved '$name' to the store." +} + +pw_del() { + yn "Delete pass file '$1'?" && { + rm -f "$1.gpg" + + # Remove empty parent directories of a password + # entry. It's fine if this fails as it means that + # another entry also lives in the same directory. + rmdir -p "${1%/*}" 2>/dev/null || : + } +} + +pw_show() { + "$gpg" -dq "$1.gpg" +} + +pw_copy() { + # Disable warning against word-splitting as it is safe + # and intentional (globbing is disabled). + # shellcheck disable=2086 + : "${PASH_CLIP:=xclip -sel c}" + + # Wait in the background for the password timeout and + # clear the clipboard when the timer runs out. + # + # If the 'sleep' fails, kill the script. This is the + # simplest method of aborting from a subshell. + [ "$PASH_TIMEOUT" != off ] && { + printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}" + + sleep "$PASH_TIMEOUT" || kill 0 + $PASH_CLIP /dev/null 2>&1 || + die "'tree' command not found" + + tree --noreport | sed 's/\.gpg$//' +} + +yn() { + printf '%s [y/n]: ' "$1" + + # Enable raw input to allow for a single byte to be read from + # stdin without needing to wait for the user to press Return. + stty -icanon + + # Read a single byte from stdin using 'dd'. POSIX 'read' has + # no support for single/'N' byte based input from the user. + answer=$(dd ibs=1 count=1 2>/dev/null) + + # Disable raw input, leaving the terminal how we *should* + # have found it. + stty icanon + + printf '\n' + + # Handle the answer here directly, enabling this function's + # return status to be used in place of checking for '[yY]' + # throughout this program. + glob "$answer" '[yY]' +} + +sread() { + printf '%s: ' "$2" + + # Disable terminal printing while the user inputs their + # password. POSIX 'read' has no '-s' flag which would + # effectively do the same thing. + stty -echo + read -r "$1" + stty echo + + printf '\n' +} + +glob() { + # This is a simple wrapper around a case statement to allow + # for simple string comparisons against globs. + # + # Example: if glob "Hello World" '* World'; then + # + # Disable this warning as it is the intended behavior. + # shellcheck disable=2254 + case $1 in $2) return 0; esac; return 1 +} + +die() { + printf 'error: %s.\n' "$1" >&2 + exit 1 +} + +usage() { printf %s "\ +pash 2.3.0 - simple password manager. + +=> [a]dd [name] - Create a new password entry. +=> [c]opy [name] - Copy entry to the clipboard. +=> [d]el [name] - Delete a password entry. +=> [l]ist - List all entries. +=> [s]how [name] - Show password for an entry. +=> [t]ree - List all entries in a tree. + +Using a key pair: export PASH_KEYID=XXXXXXXX +Password length: export PASH_LENGTH=50 +Password pattern: export PASH_PATTERN=_A-Z-a-z-0-9 +Store location: export PASH_DIR=~/.local/share/pash +Clipboard tool: export PASH_CLIP='xclip -sel c' +Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable) +" +exit 0 +} + +main() { + : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" + + # Look for both 'gpg' and 'gpg2', + # preferring 'gpg2' if it is available. + command -v gpg >/dev/null 2>&1 && gpg=gpg + command -v gpg2 >/dev/null 2>&1 && gpg=gpg2 + + [ "$gpg" ] || + die "GPG not found" + + mkdir -p "$PASH_DIR" || + die "Couldn't create password directory" + + cd "$PASH_DIR" || + die "Can't access password directory" + + glob "$1" '[acds]*' && [ -z "$2" ] && + die "Missing [name] argument" + + glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] && + die "Pass file '$2' doesn't exist" + + glob "$1" 'a*' && [ -f "$2.gpg" ] && + die "Pass file '$2' already exists" + + glob "$2" '*/*' && glob "$2" '*../*' && + die "Category went out of bounds" + + glob "$2" '/*' && + die "Category can't start with '/'" + + glob "$2" '*/*' && { mkdir -p "${2%/*}" || + die "Couldn't create category '${2%/*}'"; } + + # Set 'GPG_TTY' to the current 'TTY' if it + # is unset. Fixes a somewhat rare `gpg` issue. + export GPG_TTY=${GPG_TTY:-$(tty)} + + # Restrict permissions of any new files to + # only the current user. + umask 077 + + # Ensure that we leave the terminal in a usable + # state on exit or Ctrl+C. + [ -t 1 ] && trap 'stty echo icanon' INT EXIT + + case $1 in + a*) pw_add "$2" ;; + c*) pw_copy "$2" ;; + d*) pw_del "$2" ;; + s*) pw_show "$2" ;; + l*) pw_list ;; + t*) pw_tree ;; + *) usage + esac +} + +# Ensure that debug mode is never enabled to +# prevent the password from leaking. +set +x + +# Ensure that globbing is globally disabled +# to avoid insecurities with word-splitting. +set -f + +[ "$1" ] || usage && main "$@"