Purse/purse.sh

316 lines
8.4 KiB
Bash
Raw 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"
pass_chars="[:alnum:]!?@#$%^&*();:+="
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
pass_copy="${PURSE_COPY:=}" # copy password before write
pass_len="${PURSE_LEN:=14}" # default generated password length
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}"
trap cleanup EXIT INT TERM
cleanup () {
# "Lock" safe on trapped exits.
ret=$?
chmod -R 0000 "${safe_ix}" "${safe_dir}" 2>/dev/null
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
2015-07-03 13:07:54 +00:00
if [[ ${char} == $'\0' ]] ; then
break
2015-07-03 17:05:06 +00:00
elif [[ ${char} == $'\177' ]] ; then
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
2015-07-03 17:05:06 +00:00
prompt="*"
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 21:18:57 +00:00
decrypt "${spath}" || 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-10 21:59:33 +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:18:57 +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}"
tar cf "${safe_backup}" "${safe_ix}" "${safe_dir}" \
"${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:18:57 +00:00
printf """\nPurse 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
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
encrypt_index=""
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:18:57 +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:18:57 +00:00
if [[ -n "${daily_backup}" && ! -f "${safe_backup}" ]]
then backup
2024-03-10 21:59:33 +00:00
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