bin: Add pash script
parent
78bd0d18f2
commit
f3eeab9d9c
|
@ -0,0 +1,242 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# pash - simple password manager.
|
||||
# Licensed under the MIT License
|
||||
|
||||
pw_add() {
|
||||
name=$1
|
||||
|
||||
if yn "Generate a password?"; then
|
||||
# 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.
|
||||
#
|
||||
# Regarding usage of '/dev/urandom' instead of '/dev/random'.
|
||||
# See: https://www.2uo.de/myths-about-urandom
|
||||
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)
|
||||
|
||||
else
|
||||
# 'sread()' is a simple wrapper function around 'read'
|
||||
# to prevent user input from being printed to the terminal.
|
||||
sread pass "Enter password"
|
||||
sread pass2 "Enter password (again)"
|
||||
|
||||
# Disable this check as we dynamically populate the two
|
||||
# passwords using the 'sread()' function.
|
||||
# shellcheck disable=2154
|
||||
[ "$pass" = "$pass2" ] || die "Passwords do not match"
|
||||
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.
|
||||
# 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 a leak in '/proc'.
|
||||
"$gpg" "$@" -o "$name.gpg" <<-EOF &&
|
||||
$pass
|
||||
EOF
|
||||
printf '%s\n' "Saved '$name' to the store."
|
||||
}
|
||||
|
||||
pw_del() {
|
||||
yn "Delete pass file '$1'?" && {
|
||||
rm -f "$1.gpg"
|
||||
|
||||
# Remove empty parent directories of a password
|
||||
# entry. It's fine if this fails as it means that
|
||||
# another entry also lives in the same directory.
|
||||
rmdir -p "${1%/*}" 2>/dev/null || :
|
||||
}
|
||||
}
|
||||
|
||||
pw_show() {
|
||||
"$gpg" -dq "$1.gpg"
|
||||
}
|
||||
|
||||
pw_copy() {
|
||||
# Disable warning against word-splitting as it is safe
|
||||
# and intentional (globbing is disabled).
|
||||
# shellcheck disable=2086
|
||||
: "${PASH_CLIP:=xclip -sel c}"
|
||||
|
||||
# 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 ] && {
|
||||
printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}"
|
||||
|
||||
sleep "$PASH_TIMEOUT" || kill 0
|
||||
$PASH_CLIP </dev/null
|
||||
} &
|
||||
|
||||
pw_show "$1" | $PASH_CLIP
|
||||
}
|
||||
|
||||
pw_list() {
|
||||
find . -type f -name \*.gpg | sed 's/..//;s/\.gpg$//'
|
||||
}
|
||||
|
||||
pw_tree() {
|
||||
command -v tree >/dev/null 2>&1 ||
|
||||
die "'tree' command not found"
|
||||
|
||||
tree --noreport | sed 's/\.gpg$//'
|
||||
}
|
||||
|
||||
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]'
|
||||
}
|
||||
|
||||
sread() {
|
||||
printf '%s: ' "$2"
|
||||
|
||||
# Disable terminal printing while the user inputs their
|
||||
# password. POSIX 'read' has no '-s' flag which would
|
||||
# effectively do the same thing.
|
||||
stty -echo
|
||||
read -r "$1"
|
||||
stty echo
|
||||
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
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 2.3.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.
|
||||
=> [t]ree - List all entries in a tree.
|
||||
|
||||
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
|
||||
Clipboard tool: export PASH_CLIP='xclip -sel c'
|
||||
Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable)
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
main() {
|
||||
: "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
|
||||
|
||||
# Look for both 'gpg' and 'gpg2',
|
||||
# preferring 'gpg2' if it is available.
|
||||
command -v gpg >/dev/null 2>&1 && gpg=gpg
|
||||
command -v gpg2 >/dev/null 2>&1 && 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%/*}'"; }
|
||||
|
||||
# Set 'GPG_TTY' to the current 'TTY' if it
|
||||
# is unset. Fixes a somewhat rare `gpg` issue.
|
||||
export GPG_TTY=${GPG_TTY:-$(tty)}
|
||||
|
||||
# Restrict permissions of any new files to
|
||||
# only the current user.
|
||||
umask 077
|
||||
|
||||
# Ensure that we leave the terminal in a usable
|
||||
# state on exit or Ctrl+C.
|
||||
[ -t 1 ] && trap 'stty echo icanon' INT EXIT
|
||||
|
||||
case $1 in
|
||||
a*) pw_add "$2" ;;
|
||||
c*) pw_copy "$2" ;;
|
||||
d*) pw_del "$2" ;;
|
||||
s*) pw_show "$2" ;;
|
||||
l*) pw_list ;;
|
||||
t*) pw_tree ;;
|
||||
*) usage
|
||||
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
|
||||
|
||||
[ "$1" ] || usage && main "$@"
|
Loading…
Reference in New Issue