pash/pash

231 lines
6.0 KiB
Plaintext
Raw Normal View History

#!/bin/sh
2019-02-24 14:36:47 -06:00
#
# pash - simple password manager.
pw_add() {
2019-11-26 06:21:45 -06:00
name=$1
if yn "Generate a password?"; then
2019-11-29 14:59:12 -06:00
# 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.
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)
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
else
printf 'Enter password: '
2019-02-24 15:04:28 -06:00
2019-11-29 15:09:42 -06:00
# Disable terminal printing while the user inputs their
# password. POSIX 'read' has no '-s' flag which would
# effectively do the same thing.
2019-11-26 06:21:45 -06:00
stty -echo
read -r pass
stty echo
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
printf '\n'
fi
2019-02-24 14:36:47 -06:00
2019-11-30 09:02:10 -06:00
[ "$pass" ] || die "Failed to generate a password"
2019-11-26 06:21:45 -06:00
# 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
2019-11-08 06:43:06 -06:00
2019-11-29 15:07:29 -06:00
# Use 'gpg' to store the password in an encrypted file.
2019-11-30 05:27:03 -06:00
# 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 '/proc'.
"$gpg" "$@" -o "$name.gpg" <<-EOF && \
printf '%s\n' "Saved '$name' to the store."
2019-11-30 05:27:03 -06:00
$pass
EOF
2019-02-24 14:36:47 -06:00
}
pw_del() {
2019-11-26 06:21:45 -06:00
yn "Delete pass file '$1'?" && {
2019-11-14 09:47:59 -06:00
rm -f "$1.gpg"
rmdir -p "${1%/*}" 2>/dev/null
}
2019-02-24 14:36:47 -06:00
}
pw_show() {
2019-11-28 12:42:28 -06:00
"$gpg" -dq "$1.gpg"
2019-02-24 14:36:47 -06:00
}
2019-05-22 13:52:11 -05:00
pw_copy() {
2019-11-29 09:40:10 -06:00
# Disable warning against word-splitting as it is safe
2019-11-28 12:37:56 -06:00
# and intentional (globbing is disabled).
# shellcheck disable=2086
2019-11-30 05:09:14 -06:00
: "${PASH_CLIP:=xclip -sel c}"
2019-11-30 04:19:32 -06:00
# 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 ] && {
2019-11-30 05:57:12 -06:00
printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}"
sleep "$PASH_TIMEOUT" || kill 0
$PASH_CLIP </dev/null
} &
2019-11-30 04:19:32 -06:00
pw_show "$1" | $PASH_CLIP
}
2019-05-22 13:52:11 -05:00
pw_list() {
2019-11-30 09:00:57 -06:00
set +f
find -- * -type f -name \*.gpg
}
2019-11-28 12:56:09 -06:00
2019-11-30 09:00:57 -06:00
pw_tree() {
command -v tree >/dev/null 2>&1 ||
die "'tree' command not found"
tree --noreport
2019-11-26 06:21:45 -06:00
}
2019-05-22 13:52:11 -05:00
2019-11-26 06:21:45 -06:00
yn() {
printf '%s [y/n]: ' "$1"
2019-02-25 14:07:50 -06:00
2019-11-26 06:21:45 -06:00
# 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
2019-02-25 13:46:57 -06:00
2019-11-26 06:21:45 -06:00
# 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)
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
# Disable raw input, leaving the terminal how we *should*
# have found it.
stty icanon
printf '\n'
2019-02-25 14:07:50 -06:00
2019-11-26 06:21:45 -06:00
# Handle the answer here directly, enabling this function's
# return status to be used in place of checking for '[yY]'
# throughout this program.
2019-11-28 12:48:17 -06:00
glob "$answer" '[yY]'
2019-02-24 14:36:47 -06:00
}
2019-11-26 06:21:45 -06:00
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
2019-02-24 15:04:28 -06:00
}
2019-02-24 14:36:47 -06:00
die() {
2019-11-30 09:02:10 -06:00
printf 'error: %s.\n' "$1" >&2
2019-02-24 14:36:47 -06:00
exit 1
}
2019-11-08 06:43:06 -06:00
usage() { printf %s "\
2019-11-30 09:00:57 -06:00
pash 2.3.0 - simple password manager.
2019-11-08 06:43:06 -06:00
2019-05-22 13:52:11 -05:00
=> [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.
2019-11-30 09:00:57 -06:00
=> [t]ree - List all entries in a tree.
2019-11-08 06:43:06 -06:00
2019-11-30 04:19:32 -06:00
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
2019-11-30 05:09:14 -06:00
Clipboard tool: export PASH_CLIP='xclip -sel c'
2019-11-30 05:41:28 -06:00
Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable)
2019-02-24 14:36:47 -06:00
"
exit 1
}
main() {
2019-11-26 06:21:45 -06:00
: "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
[ "$1" = '-?' ] || [ -z "$1" ] &&
2019-05-22 13:52:11 -05:00
usage
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
# Look for both 'gpg' and 'gpg2',
# preferring 'gpg2' if it is available.
2019-11-30 04:03:08 -06:00
command -v gpg >/dev/null 2>&1 && gpg=gpg
command -v gpg2 >/dev/null 2>&1 && gpg=gpg2
2019-11-26 06:21:45 -06:00
[ "$gpg" ] ||
2019-11-30 09:02:10 -06:00
die "GPG not found"
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
mkdir -p "$PASH_DIR" ||
2019-11-30 09:02:10 -06:00
die "Couldn't create password directory"
2019-02-24 14:36:47 -06:00
2019-11-08 07:05:49 -06:00
cd "$PASH_DIR" ||
2019-11-30 09:02:10 -06:00
die "Can't access password directory"
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
glob "$1" '[acds]*' && [ -z "$2" ] &&
2019-11-30 09:02:10 -06:00
die "Missing [name] argument"
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] &&
2019-11-30 09:02:10 -06:00
die "Pass file '$2' doesn't exist"
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
glob "$1" 'a*' && [ -f "$2.gpg" ] &&
2019-11-30 09:02:10 -06:00
die "Pass file '$2' already exists"
2019-02-24 14:36:47 -06:00
2019-11-26 06:21:45 -06:00
glob "$2" '*/*' && glob "$2" '*../*' &&
2019-11-30 09:02:10 -06:00
die "Category went out of bounds"
2019-11-26 06:21:45 -06:00
glob "$2" '/*' &&
2019-11-30 09:02:10 -06:00
die "Category can't start with '/'"
2019-11-26 06:21:45 -06:00
glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
2019-11-30 09:02:10 -06:00
die "Couldn't create category '${2%/*}'"; }
2019-11-29 13:59:57 -06:00
# Set 'GPG_TTY' to the current 'TTY' if it
# is unset. Fixes a somewhat rare `gpg` issue.
2019-11-26 15:19:22 -06:00
export GPG_TTY=${GPG_TTY:-$(tty)}
2019-11-29 13:59:57 -06:00
# Restrict permissions of any new files to
# only the current user.
2019-02-25 14:37:32 -06:00
umask 077
# Ensure that we leave the terminal in a usable
# state on exit or Ctrl+C.
2019-11-29 11:50:20 -06:00
trap 'stty echo icanon' INT EXIT
2019-02-24 14:36:47 -06:00
case $1 in
2019-11-26 06:21:45 -06:00
a*) pw_add "$2" ;;
2019-05-22 13:52:11 -05:00
c*) pw_copy "$2" ;;
2019-02-24 14:36:47 -06:00
d*) pw_del "$2" ;;
s*) pw_show "$2" ;;
# TODO: Better handle the removal
# of '.gpg' from list output.
2019-11-28 12:58:19 -06:00
l*) pw_list | sed 's/\.gpg$//' ;;
t*) pw_tree | sed 's/\.gpg$//' ;;
2019-05-22 13:52:11 -05:00
*) usage
2019-02-24 14:36:47 -06:00
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
2019-02-24 14:36:47 -06:00
main "$@"