Purse/purse.sh

317 lines
8.5 KiB
Bash
Raw Permalink Normal View History

2015-07-03 18:59:27 +00:00
#!/usr/bin/env bash
# https://github.com/drduh/Purse/blob/master/purse.sh
2024-03-26 21:18:57 +00:00
#set -x # uncomment to debug
set -o errtrace
2015-07-02 02:03:55 +00:00
set -o nounset
set -o pipefail
umask 077
2024-03-26 21:18:57 +00:00
export LC_ALL="C"
2024-03-10 21:59:33 +00:00
now="$(date +%s)"
2024-03-26 21:18:57 +00:00
today="$(date +%F)"
2024-03-10 21:59:33 +00:00
copy="$(command -v xclip || command -v pbcopy)"
gpg="$(command -v gpg || command -v gpg2)"
2024-03-26 21:18:57 +00:00
gpg_conf="${GNUPGHOME}/gpg.conf"
clip_dest="${PURSE_DEST:=clipboard}" # set to 'screen' to print to stdout
clip_timeout="${PURSE_TIME:=10}" # seconds to clear clipboard/screen
comment="${PURSE_COMMENT:=}" # *unencrypted* comment in files
daily_backup="${PURSE_DAILY:=}" # daily backup archive on write
2024-03-26 23:56:11 +00:00
encrypt_index="${PURSE_ENCIX:=}" # also keep index encrypted
2024-03-26 21:18:57 +00:00
pass_copy="${PURSE_COPY:=}" # copy password before write
2024-03-26 23:36:25 +00:00
pass_echo="${PURSE_ECHO:=*}" # show "*" when typing passwords
2024-03-26 21:52:56 +00:00
pass_len="${PURSE_LEN:=14}" # default password length
2024-03-26 21:18:57 +00:00
safe_dir="${PURSE_SAFE:=safe}" # safe directory name
safe_ix="${PURSE_INDEX:=purse.index}" # index file name
safe_backup="${PURSE_BACKUP:=purse.$(hostname).${today}.tar}"
2024-03-26 23:36:25 +00:00
pass_chars="${PURSE_CHARS:='[:alnum:]!?@#$%^&*();:+='}"
2024-03-26 21:18:57 +00:00
trap cleanup EXIT INT TERM
cleanup () {
2024-03-26 21:52:56 +00:00
# "Lock" files on trapped exits.
2024-03-26 21:18:57 +00:00
ret=$?
2024-03-26 21:52:56 +00:00
chmod -R 0000 \
"${safe_dir}" "${safe_ix}" 2>/dev/null
2024-03-26 21:18:57 +00:00
exit ${ret}
}
2015-07-02 02:03:55 +00:00
fail () {
2024-03-26 21:18:57 +00:00
# Print an error in red and exit.
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
tput setaf 1 ; printf "\nERROR: %s\n" "${1}" ; tput sgr0
exit 1
}
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
warn () {
# Print a warning in yellow.
tput setaf 3 ; printf "\nWARNING: %s\n" "${1}" ; tput sgr0
}
2015-07-02 02:03:55 +00:00
get_pass () {
# Prompt for a password.
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
prompt=" ${1}"
printf "\n"
while IFS= read -p "${prompt}" -r -s -n 1 char ; do
2024-03-26 21:52:56 +00:00
if [[ ${char} == $'\0' ]] ; then break
2015-07-03 17:05:06 +00:00
elif [[ ${char} == $'\177' ]] ; then
2024-03-26 21:52:56 +00:00
if [[ -z "${password}" ]] ; then prompt=""
2015-07-03 13:07:54 +00:00
else
prompt=$'\b \b'
password="${password%?}"
2015-07-02 02:03:55 +00:00
fi
2015-07-03 13:07:54 +00:00
else
2024-03-26 23:36:25 +00:00
prompt="${pass_echo}"
password+="${char}"
2015-07-03 13:07:54 +00:00
fi
2015-07-02 02:03:55 +00:00
done
}
decrypt () {
# Decrypt with GPG.
2015-07-02 02:03:55 +00:00
2024-03-10 21:59:33 +00:00
cat "${1}" | \
${gpg} --armor --batch --decrypt 2>/dev/null
2015-07-02 02:03:55 +00:00
}
encrypt () {
# Encrypt to a group of hidden recipients.
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
${gpg} --encrypt --armor --batch --yes \
--hidden-recipient "purse_keygroup" \
2024-03-26 21:18:57 +00:00
--throw-keyids --comment "${comment}" \
--output "${1}" "${2}" 2>/dev/null
2015-07-02 02:03:55 +00:00
}
read_pass () {
# Read a password from safe.
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
if [[ ! -s "${safe_ix}" ]] ; then fail "${safe_ix} not found" ; fi
2015-08-07 18:11:11 +00:00
while [[ -z "${username}" ]] ; do
if [[ -z "${2+x}" ]] ; then read -r -p "
Username: " username
else username="${2}" ; fi
done
2024-03-26 21:18:57 +00:00
if [[ -n "${encrypt_index}" ]] ; then prompt_key "index"
spath=$(decrypt "${safe_ix}" | \
2024-03-10 21:59:33 +00:00
grep -F "${username}" | tail -1 | cut -d ":" -f2) || \
2024-03-26 21:18:57 +00:00
fail "Secret not available"
else spath=$(grep -F "${username}" "${safe_ix}" | \
tail -1 | cut -d ":" -f2)
fi
2018-06-02 20:33:18 +00:00
prompt_key "password"
2024-03-10 21:59:33 +00:00
if [[ -s "${spath}" ]] ; then
2024-03-26 22:57:33 +00:00
clip <(decrypt "${spath}" | head -1) || \
fail "Failed to decrypt ${spath}"
2024-03-10 21:59:33 +00:00
else fail "Secret not available"
fi
}
2018-06-02 20:33:18 +00:00
prompt_key () {
# Print a message if safe file exists.
2018-06-02 20:33:18 +00:00
2024-03-26 21:18:57 +00:00
if [[ -f "${safe_ix}" ]] ; then
printf "\n Touch key to access %s ...\n" "${1}"
2018-06-02 20:33:18 +00:00
fi
2015-07-02 02:03:55 +00:00
}
gen_pass () {
2024-03-26 21:18:57 +00:00
# Generate a password from urandom.
2015-07-02 02:03:55 +00:00
if [[ -z "${3+x}" ]] ; then read -r -p "
2024-03-10 21:59:33 +00:00
Password length (default: ${pass_len}): " length
else length="${3}" ; fi
2015-07-02 02:31:38 +00:00
2024-03-26 21:52:56 +00:00
if [[ "${length}" =~ ^[0-9]+$ ]] ; then
pass_len="${length}"
fi
2024-03-26 21:18:57 +00:00
tr -dc "${pass_chars}" < /dev/urandom | \
2024-03-10 21:59:33 +00:00
fold -w "${pass_len}" | head -1
}
2015-07-02 02:31:38 +00:00
write_pass () {
2024-03-10 21:59:33 +00:00
# Write a password and update the index.
2024-03-26 21:18:57 +00:00
spath="${safe_dir}/$(tr -dc "[:lower:]" < /dev/urandom | \
fold -w10 | head -1)"
if [[ -n "${pass_copy}" ]] ; then
2024-03-10 21:59:33 +00:00
clip <(printf '%s' "${userpass}")
fi
2015-07-02 02:03:55 +00:00
printf '%s\n' "${userpass}" | \
encrypt "${spath}" - || \
2024-03-26 21:18:57 +00:00
fail "Failed saving ${spath}"
2024-03-26 21:18:57 +00:00
if [[ -n "${encrypt_index}" ]] ; then
prompt_key "index"
2024-03-26 21:18:57 +00:00
( if [[ -f "${safe_ix}" ]] ; then
decrypt "${safe_ix}" || return ; fi
printf "%s@%s:%s\n" "${username}" "${now}" "${spath}") | \
2024-03-26 21:18:57 +00:00
encrypt "${safe_ix}.${now}" - && \
mv "${safe_ix}.${now}" "${safe_ix}" || \
fail "Failed saving ${safe_ix}.${now}"
else
2024-03-26 21:52:56 +00:00
printf "%s@%s:%s\n" \
"${username}" "${now}" "${spath}" >> "${safe_ix}"
fi
2015-07-02 02:03:55 +00:00
}
list_entry () {
# Decrypt the index to list entries.
2024-03-26 21:18:57 +00:00
if [[ ! -s "${safe_ix}" ]] ; then fail "${safe_ix} not found" ; fi
2024-03-26 21:18:57 +00:00
if [[ -n "${encrypt_index}" ]] ; then prompt_key "index"
decrypt "${safe_ix}" || fail "${safe_ix} not available"
else printf "\n" ; cat "${safe_ix}"
fi
}
backup () {
2024-03-10 21:59:33 +00:00
# Archive index, safe and configuration.
2024-03-26 21:32:02 +00:00
if [[ ! -f ${safe_backup} ]] ; then
if [[ -f "${safe_ix}" && -d "${safe_dir}" ]] ; then
cp "${gpg_conf}" "gpg.conf.${today}"
2024-03-26 21:52:56 +00:00
tar cf "${safe_backup}" "${safe_dir}" "${safe_ix}" \
2024-03-26 21:32:02 +00:00
"${BASH_SOURCE}" "gpg.conf.${today}" && \
printf "\nArchived %s\n" "${safe_backup}"
rm -f "gpg.conf.${today}"
else fail "Nothing to archive" ; fi
else warn "${safe_backup} exists, skipping archive" ; fi
}
clip () {
2024-03-26 21:18:57 +00:00
# Use clipboard or stdout and clear after timeout.
2024-03-26 21:18:57 +00:00
if [[ "${clip_dest}" = "screen" ]] ; then
printf '\n%s\n' "$(cat ${1})"
else "${copy}" < "${1}" ; fi
printf "\n"
2024-03-26 21:32:02 +00:00
while [[ "${clip_timeout}" -gt 0 ]] ; do
2024-03-26 21:18:57 +00:00
printf "\r\033[K Password on %s! Clearing in %.d" \
"${clip_dest}" "$((clip_timeout--))" ; sleep 1
done
2024-03-26 21:18:57 +00:00
printf "\r\033[K Clearing password from %s ..." "${clip_dest}"
2024-03-26 21:32:02 +00:00
if [[ "${clip_dest}" = "screen" ]] ; then clear
2024-03-26 21:18:57 +00:00
else printf "\n" ; printf "" | "${copy}" ; fi
}
2015-07-02 02:03:55 +00:00
setup_keygroup() {
2024-03-26 21:18:57 +00:00
# Configure one or more recipients.
purse_keygroup="group purse_keygroup ="
keyid=""
recommend="$(${gpg} -K | grep "sec#" | \
awk -F "/" '{print $2}' | cut -c-18 | tr "\n" " ")"
2024-03-26 21:18:57 +00:00
printf "\n Setting up keygroup ...\n
Found recommended key IDs: %s\n
Enter one or more key IDs, preferred one last\n" "${recommend}"
2024-03-26 21:18:57 +00:00
while [[ -z "${keyid}" ]] ; do read -r -p "
Key ID or Enter to continue: " keyid
if [[ -z "${keyid}" ]] ; then
2024-03-26 21:18:57 +00:00
printf "%s\n" "${purse_keygroup}" >> "${gpg_conf}"
break
fi
purse_keygroup="${purse_keygroup} ${keyid}"
keyid=""
done
}
new_entry () {
2024-03-10 21:59:33 +00:00
# Prompt for username and password.
2015-07-02 02:03:55 +00:00
while [[ -z "${username}" ]] ; do
if [[ -z "${2+x}" ]] ; then read -r -p "
2015-07-03 21:03:26 +00:00
Username: " username
else username="${2}" ; fi
done
2024-03-26 21:18:57 +00:00
if [[ -z "${3+x}" ]] ; then
get_pass "Password for \"${username}\" (Enter to generate): "
userpass="${password}"
fi
2015-07-03 21:03:26 +00:00
2024-03-26 21:18:57 +00:00
printf "\n"
2024-03-10 21:59:33 +00:00
if [[ -z "${password}" ]] ; then
userpass=$(gen_pass "$@")
fi
2015-07-02 02:03:55 +00:00
}
print_help () {
# Print help text.
2015-07-02 02:03:55 +00:00
2024-03-26 21:52:56 +00:00
printf """
Purse is a Bash shell script to manage passwords with GnuPG asymmetric encryption. It is designed and recommended to be used with YubiKey as the secret key storage.\n
2024-03-26 21:18:57 +00:00
Purse can be used interactively or by passing one of the following options:\n
* 'w' to write a password
* 'r' to read a password
* 'l' to list passwords
2024-03-26 21:18:57 +00:00
* 'b' to create an archive for backup\n
Options can also be passed on the command line.\n
* Create a 20-character password for userName:
./purse.sh w userName 20\n
* Read password for userName:
./purse.sh r userName\n
* Passwords are stored with an epoch timestamp for revision control. The most recent version is copied to clipboard on read. To list all passwords or read a specific version of a password:
./purse.sh l
./purse.sh r userName@1574723625\n
* Create an archive for backup:
./purse.sh b\n
* Restore an archive from backup:
tar xvf purse*tar\n"""
2015-07-02 02:03:55 +00:00
}
2024-03-26 21:18:57 +00:00
if [[ -z "${gpg}" || ! -x "${gpg}" ]] ; then fail "GnuPG is not available" ; fi
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
if [[ ! -f "${gpg_conf}" ]] ; then fail "GnuPG config is not available" ; fi
2024-03-26 21:18:57 +00:00
if [[ ! -d "${safe_dir}" ]] ; then mkdir -p "${safe_dir}" ; fi
2024-03-10 21:59:33 +00:00
2024-03-26 21:18:57 +00:00
chmod -R 0700 "${safe_dir}" "${safe_ix}" 2>/dev/null
2024-03-26 21:18:57 +00:00
if [[ -z "${copy}" || ! -x "${copy}" ]] ; then
warn "Clipboard not available, passwords will print to screen/stdout!"
clip_dest="screen"
fi
2024-03-26 21:18:57 +00:00
username=""
password=""
action=""
2024-03-26 21:18:57 +00:00
if [[ -n "${1+x}" ]] ; then action="${1}" ; fi
2024-03-26 21:18:57 +00:00
while [[ -z "${action}" ]] ; do read -r -n 1 -p "
Read or Write (or Help for more options): " action
printf "\n"
done
2024-03-26 21:52:56 +00:00
if [[ "${action}" =~ ^([rR])$ ]] ; then read_pass "$@"
elif [[ "${action}" =~ ^([wW])$ ]] ; then
2024-03-26 21:18:57 +00:00
purse_keygroup="$(grep "group purse_keygroup" "${gpg_conf}")"
if [[ -z "${purse_keygroup}" ]] ; then
setup_keygroup
fi
printf "\n %s\n" "${purse_keygroup}"
new_entry "$@"
write_pass
2024-03-26 21:52:56 +00:00
if [[ -n "${daily_backup}" ]] ; then backup ; fi
2024-03-26 21:18:57 +00:00
elif [[ "${action}" =~ ^([lL])$ ]] ; then list_entry
elif [[ "${action}" =~ ^([bB])$ ]] ; then backup
else print_help ; fi
2024-03-10 21:59:33 +00:00
tput setaf 2 ; printf "\nDone\n" ; tput sgr0