2019-11-28 12:37:56 -06:00
|
|
|
#!/bin/sh -f
|
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
|
|
|
|
# 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}")
|
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-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-26 06:21:45 -06:00
|
|
|
[ "$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
|
2019-11-08 06:43:06 -06:00
|
|
|
|
2019-11-26 06:21:45 -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-28 12:31:30 -06:00
|
|
|
printf %s "$pass" | "$gpg" "$@" -o "$name.gpg" &&
|
2019-11-26 06:21:45 -06:00
|
|
|
printf '%s\n' "Saved '$name' to the store."
|
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"
|
2019-05-22 14:05:43 -05:00
|
|
|
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-28 12:31:30 -06:00
|
|
|
# Disable warning against words-splitting as it is safe
|
2019-11-28 12:37:56 -06:00
|
|
|
# and intentional (globbing is disabled).
|
2019-11-28 12:31:30 -06:00
|
|
|
# shellcheck disable=2086
|
2019-11-28 12:46:20 -06:00
|
|
|
pw_show "$1" | ${PASH_CLIP:-xclip -selection clipboard}
|
2019-02-26 01:43:26 -06:00
|
|
|
}
|
|
|
|
|
2019-05-22 13:52:11 -05:00
|
|
|
pw_list() {
|
2019-11-26 06:21:45 -06:00
|
|
|
if hash tree 2>/dev/null; then
|
|
|
|
tree --noreport
|
2019-11-28 12:56:09 -06:00
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
else
|
2019-11-28 12:56:09 -06:00
|
|
|
set +f
|
|
|
|
find -- * -type f
|
2019-11-26 06:21:45 -06:00
|
|
|
fi
|
|
|
|
}
|
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() {
|
|
|
|
printf 'error: %s\n' "$1" >&2
|
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
2019-11-08 06:43:06 -06:00
|
|
|
usage() { printf %s "\
|
2019-11-29 09:27:59 -06:00
|
|
|
pash 2.0.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-08 06:43:06 -06:00
|
|
|
|
2019-11-08 06:51:07 -06:00
|
|
|
Using a key pair: export PASH_KEYID=XXXXXXXX
|
2019-11-08 07:05:49 -06:00
|
|
|
Password length: export PASH_LENGTH=50
|
|
|
|
Store location: export PASH_DIR=~/.local/share/pash
|
2019-11-28 12:35:44 -06:00
|
|
|
Clipboard tool: export PASH_CLIP='xclip -selection clipboard'
|
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.
|
|
|
|
hash gpg 2>/dev/null && gpg=gpg
|
|
|
|
hash gpg2 2>/dev/null && gpg=gpg2
|
|
|
|
|
|
|
|
[ "$gpg" ] ||
|
2019-02-24 14:36:47 -06:00
|
|
|
die "GPG not found."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
mkdir -p "$PASH_DIR" ||
|
2019-02-24 14:36:47 -06:00
|
|
|
die "Couldn't create password directory."
|
|
|
|
|
2019-11-08 07:05:49 -06:00
|
|
|
cd "$PASH_DIR" ||
|
2019-02-24 14:36:47 -06:00
|
|
|
die "Can't access password directory."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$1" '[acds]*' && [ -z "$2" ] &&
|
2019-02-24 14:36:47 -06:00
|
|
|
die "Missing [name] argument."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] &&
|
2019-02-24 14:36:47 -06:00
|
|
|
die "Pass file '$2' doesn't exist."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$1" 'a*' && [ -f "$2.gpg" ] &&
|
2019-02-24 14:36:47 -06:00
|
|
|
die "Pass file '$2' already exists."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$2" '*/*' && glob "$2" '*../*' &&
|
2019-02-25 14:44:56 -06:00
|
|
|
die "Category went out of bounds."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$2" '/*' &&
|
2019-02-25 16:01:19 -06:00
|
|
|
die "Category can't start with '/'."
|
|
|
|
|
2019-11-26 06:21:45 -06:00
|
|
|
glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
|
|
|
|
die "Couldn't create category '${2%/*}'."; }
|
2019-02-25 14:44:56 -06:00
|
|
|
|
2019-11-26 15:19:22 -06:00
|
|
|
export GPG_TTY=${GPG_TTY:-$(tty)}
|
|
|
|
|
2019-02-25 14:37:32 -06:00
|
|
|
umask 077
|
|
|
|
|
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" ;;
|
2019-11-28 12:58:19 -06:00
|
|
|
l*) pw_list | sed 's/\.gpg$//' ;;
|
2019-05-22 13:52:11 -05:00
|
|
|
*) usage
|
2019-02-24 14:36:47 -06:00
|
|
|
esac
|
|
|
|
}
|
|
|
|
|
|
|
|
main "$@"
|