#!/bin/sh # # pash - simple password manager. pw_add() { pass_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" --gen-random -a "${PASH_LENGTH:-50}" |\ cut -c -"${PASH_LENGTH:-50}") else printf 'Enter password: ' # Disable echoing of output to the # terminal while reading user input. stty -echo read -r pass # Enable echoing and leave the terminal # how we *should* have found it. 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 "$pass_name.gpg" } 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. This is useful when the user # would just like the password copied to the # clipboard. [ "$2" ] || printf '%s\n' "$pass" } pw_copy() { pw_show "$1" copy if [ "$TMUX" ]; then tmux load-buffer "$pass" else hash xclip && 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 or # 'N' character based input from the user. REPLY=$(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 repeating this code throughout. glob "$REPLY" '[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 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() { [ "$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:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" || 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" && printf '%s\n' "Saved '$2' to store." ;; c*) pw_copy "$2" ;; d*) pw_del "$2" ;; s*) pw_show "$2" ;; l*) pw_list ;; *) usage esac } main "$@"