#!/bin/sh # # pash - simple password manager. pw_add() { name=$1 if yn "Generate a password?"; then # Use 'gpg' to generate the password. This could have # been 'openssl', '/dev/[u]random' or another utility, # however sticking to 'gpg' removes the need for another # dependency. # # The '-a' flag outputs the random bytes as a 'base64' # encoded string to allow for the password to be used as # well, a password. # # The 'cut' is required to actually truncate the password # to the set length as the 'base64' encoding makes the # resulting string longer than the given length. pass=$("$gpg" -a --gen-random 1 "${PASH_LENGTH:-50}" |\ cut -c -"${PASH_LENGTH:-50}") else printf 'Enter password: ' stty -echo read -r pass stty echo printf '\n' 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. The # 'GPG_TTY' environment variable is set to workaround cases # where 'gpg' cannot find an attached terminal. echo "$pass" | GPG_TTY=$(tty) "$gpg" "$@" -o "$name.gpg" && printf '%s\n' "Saved '$name' to the store." } pw_del() { yn "Delete pass file '$1'?" && { rm -f "$1.gpg" rmdir -p "${1%/*}" 2>/dev/null } } pw_show() { pass=$("$gpg" -dq "$1.gpg") # If '$2' is defined, don't print the password to the # terminal. For example, this is used when the password is # copied to the clipboard. [ "$2" ] || printf '%s\n' "$pass" } pw_copy() { pw_show "$1" copy if [ "$TMUX" ]; then tmux load-buffer "$pass" elif hash xclip; then echo "$pass" | xclip -selection clipboard fi } pw_list() { if hash tree 2>/dev/null; then tree --noreport else find . -mindepth 1 fi } 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]' || return 1 && return 0 } 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 1.0.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. Using a key pair: export PASH_KEYID=XXXXXXXX Password length: export PASH_LENGTH=50 Store location: export PASH_DIR=~/.local/share/pash " exit 1 } main() { : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" [ "$1" = '-?' ] || [ -z "$1" ] && usage # Look for both 'gpg' and 'gpg2', # preferring 'gpg2' if it is available. hash gpg 2>/dev/null && gpg=gpg hash gpg2 2>/dev/null && 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%/*}'."; } umask 077 case $1 in a*) pw_add "$2" ;; c*) pw_copy "$2" ;; d*) pw_del "$2" ;; s*) pw_show "$2" ;; l*) pw_list ;; *) usage esac } main "$@"