pash/pash-posix

192 lines
4.8 KiB
Plaintext
Raw Normal View History

2019-11-25 15:30:07 -06:00
#!/bin/sh
#
# pash - simple password manager.
pw_add() {
2019-11-25 16:52:11 -06:00
name=$1
2019-11-25 15:30:07 -06:00
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.
2019-11-25 16:25:16 -06:00
#
# The '-a' flag outputs the random bytes as a 'base64'
# encoded string to allow for the password to be used as
# well, a password.
2019-11-25 16:25:16 -06:00
#
# 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.
2019-11-25 16:52:11 -06:00
pass=$("$gpg" -a --gen-random 1 "${PASH_LENGTH:-50}" |\
2019-11-25 15:30:07 -06:00
cut -c -"${PASH_LENGTH:-50}")
else
printf 'Enter password: '
2019-11-25 16:25:16 -06:00
2019-11-25 15:30:07 -06:00
stty -echo
read -r pass
stty echo
2019-11-25 16:25:16 -06:00
2019-11-25 15:30:07 -06:00
printf '\n'
fi
2019-11-25 16:52:11 -06:00
[ "$pass" ] || die "Failed to generate a password."
2019-11-25 15:30:07 -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?
2019-11-25 16:25:16 -06:00
if [ "$PASH_KEYID" ]; then
2019-11-25 15:30:07 -06:00
set -- --trust-model always -aer "$PASH_KEYID"
2019-11-25 16:25:16 -06:00
else
set -- -c
fi
2019-11-25 15:30:07 -06:00
# 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.
2019-11-25 16:52:11 -06:00
echo "$pass" | GPG_TTY=$(tty) "$gpg" "$@" -o "$name.gpg" &&
printf '%s\n' "Saved '$name' to the store."
2019-11-25 15:30:07 -06:00
}
pw_del() {
yn "Delete pass file '$1'?" && {
2019-11-25 15:30:07 -06:00
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.
2019-11-25 15:30:07 -06:00
[ "$2" ] || printf '%s\n' "$pass"
}
pw_copy() {
pw_show "$1" copy
if [ "$TMUX" ]; then
tmux load-buffer "$pass"
2019-11-25 16:52:11 -06:00
elif hash xclip; then
echo "$pass" | xclip -selection clipboard
2019-11-25 15:30:07 -06:00
fi
}
pw_list() {
2019-11-25 15:57:43 -06:00
if hash tree 2>/dev/null; then
2019-11-25 15:30:07 -06:00
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.
2019-11-25 15:30:07 -06:00
stty -icanon
# Read a single byte from stdin using 'dd'. POSIX 'read' has
# no support for single/'N' byte based input from the user.
2019-11-26 06:13:24 -06:00
answer=$(dd ibs=1 count=1 2>/dev/null)
# Disable raw input, leaving the terminal how we *should*
# have found it.
2019-11-25 15:30:07 -06:00
stty icanon
2019-11-25 15:30:07 -06:00
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.
2019-11-26 06:13:24 -06:00
glob "$answer" '[yY]' || return 1 && return 0
2019-11-25 15:30:07 -06:00
}
2019-11-25 15:57:43 -06:00
glob() {
# This is a simple wrapper around a case statement to allow
# for simple string comparisons against globs.
2019-11-25 15:57:43 -06:00
#
# Example: if glob "Hello World" '* World'; then
2019-11-25 17:43:25 -06:00
#
# Disable this warning as it is the intended behavior.
# shellcheck disable=2254
2019-11-25 15:57:43 -06:00
case $1 in $2) return 0; esac; return 1
}
2019-11-25 15:30:07 -06:00
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() {
2019-11-25 16:52:11 -06:00
: "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
2019-11-25 15:30:07 -06:00
[ "$1" = '-?' ] || [ -z "$1" ] &&
usage
2019-11-25 16:25:16 -06:00
# Look for both 'gpg' and 'gpg2',
# preferring 'gpg2' if it is available.
2019-11-25 15:57:43 -06:00
hash gpg 2>/dev/null && gpg=gpg
hash gpg2 2>/dev/null && gpg=gpg2
2019-11-25 15:30:07 -06:00
2019-11-26 06:13:24 -06:00
[ "$gpg" ] ||
die "GPG not found."
2019-11-25 15:30:07 -06:00
2019-11-25 16:52:11 -06:00
mkdir -p "$PASH_DIR" ||
2019-11-25 15:30:07 -06:00
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 '/'."
2019-11-26 06:13:24 -06:00
glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
die "Couldn't create category '${2%/*}'."; }
2019-11-25 15:30:07 -06:00
umask 077
case $1 in
2019-11-25 16:52:11 -06:00
a*) pw_add "$2" ;;
2019-11-25 15:30:07 -06:00
c*) pw_copy "$2" ;;
d*) pw_del "$2" ;;
s*) pw_show "$2" ;;
l*) pw_list ;;
*) usage
esac
}
main "$@"