diff --git a/purse.sh b/purse.sh index c869345..6e673fc 100755 --- a/purse.sh +++ b/purse.sh @@ -1,41 +1,56 @@ #!/usr/bin/env bash # https://github.com/drduh/Purse/blob/master/purse.sh +#set -x # uncomment to debug set -o errtrace set -o nounset set -o pipefail -#set -x # uncomment to debug - umask 077 - -cb_timeout=10 # seconds to keep password on clipboard -daily_backup="false" # if true, create daily archive on write -encrypt_index="false" # if true, requires 2 touches to decrypt -pass_copy="false" # if true, keep password on clipboard before write -pass_len=14 # default password length -pass_chars="[:alnum:]!@#$%^&*();:+=" - -gpgconf="${HOME}/.gnupg/gpg.conf" -backuptar="${PURSE_BACKUP:=purse.$(hostname).$(date +%F).tar}" -safeix="${PURSE_INDEX:=purse.index}" -safedir="${PURSE_SAFE:=safe}" +export LC_ALL="C" now="$(date +%s)" +today="$(date +%F)" copy="$(command -v xclip || command -v pbcopy)" gpg="$(command -v gpg || command -v gpg2)" -script="$(basename "${BASH_SOURCE}")" +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} +} fail () { - # Print an error message and exit. + # Print an error in red and exit. - tput setaf 1 ; printf "\nError: %s\n" "${1}" ; tput sgr0 + tput setaf 1 ; printf "\nERROR: %s\n" "${1}" ; tput sgr0 exit 1 } +warn () { + # Print a warning in yellow. + + tput setaf 3 ; printf "\nWARNING: %s\n" "${1}" ; tput sgr0 +} + get_pass () { # Prompt for a password. - password="" - prompt="${1}" + prompt=" ${1}" + printf "\n" while IFS= read -p "${prompt}" -r -s -n 1 char ; do if [[ ${char} == $'\0' ]] ; then @@ -64,39 +79,35 @@ decrypt () { encrypt () { # Encrypt to a group of hidden recipients. - ${gpg} --encrypt --armor --batch --yes --throw-keyids \ + ${gpg} --encrypt --armor --batch --yes \ --hidden-recipient "purse_keygroup" \ - --output "${1}" "${2}" + --throw-keyids --comment "${comment}" \ + --output "${1}" "${2}" 2>/dev/null } read_pass () { # Read a password from safe. - if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi + if [[ ! -s "${safe_ix}" ]] ; then fail "${safe_ix} not found" ; fi - username="" while [[ -z "${username}" ]] ; do if [[ -z "${2+x}" ]] ; then read -r -p " Username: " username else username="${2}" ; fi done - if [[ "${encrypt_index}" = "true" ]] ; then - prompt_key "index" - - spath=$(decrypt "${safeix}" | \ + if [[ -n "${encrypt_index}" ]] ; then prompt_key "index" + spath=$(decrypt "${safe_ix}" | \ grep -F "${username}" | tail -1 | cut -d ":" -f2) || \ - fail "Decryption failed" - else - spath=$(grep -F "${username}" "${safeix}" | \ - tail -1 | cut -d ":" -f2) + fail "Secret not available" + else spath=$(grep -F "${username}" "${safe_ix}" | \ + tail -1 | cut -d ":" -f2) fi + set -x prompt_key "password" - if [[ -s "${spath}" ]] ; then - clip <(decrypt "${spath}" | head -1) || \ - fail "Decryption failed" + decrypt "${spath}" || fail "Failed to decrypt ${spath}" else fail "Secret not available" fi } @@ -104,116 +115,110 @@ read_pass () { prompt_key () { # Print a message if safe file exists. - if [[ -f "${safeix}" ]] ; then + if [[ -f "${safe_ix}" ]] ; then printf "\n Touch key to access %s ...\n" "${1}" fi } gen_pass () { - # Generate a password using GPG. + # Generate a password from urandom. if [[ -z "${3+x}" ]] ; then read -r -p " - Password length (default: ${pass_len}): " length else length="${3}" ; fi if [[ ${length} =~ ^[0-9]+$ ]] ; then pass_len=${length} ; fi - LC_LANG=C tr -dc "${pass_chars}" < /dev/urandom | \ + tr -dc "${pass_chars}" < /dev/urandom | \ fold -w "${pass_len}" | head -1 } write_pass () { # Write a password and update the index. - if [[ "${pass_copy}" = "true" ]] ; then + spath="${safe_dir}/$(tr -dc "[:lower:]" < /dev/urandom | \ + fold -w10 | head -1)" + + if [[ -n "${pass_copy}" ]] ; then clip <(printf '%s' "${userpass}") fi - fpath="$(LC_LANG=C tr -dc '[:lower:]' < /dev/urandom | fold -w10 | head -1)" - spath="${safedir}/${fpath}" printf '%s\n' "${userpass}" | \ encrypt "${spath}" - || \ - fail "Failed to put ${spath}" - userpass="" + fail "Failed saving ${spath}" - if [[ "${encrypt_index}" = "true" ]] ; then + if [[ -n "${encrypt_index}" ]] ; then prompt_key "index" - ( if [[ -f "${safeix}" ]] ; then - decrypt "${safeix}" || return ; fi + ( if [[ -f "${safe_ix}" ]] ; then + decrypt "${safe_ix}" || return ; fi printf "%s@%s:%s\n" "${username}" "${now}" "${spath}") | \ - encrypt "${safeix}.${now}" - || \ - fail "Failed to put ${safeix}.${now}" - mv "${safeix}.${now}" "${safeix}" + encrypt "${safe_ix}.${now}" - && \ + mv "${safe_ix}.${now}" "${safe_ix}" || \ + fail "Failed saving ${safe_ix}.${now}" else - printf "%s@%s:%s\n" "${username}" "${now}" "${spath}" >> "${safeix}" + printf "%s@%s:%s\n" "${username}" "${now}" "${spath}" >> "${safe_ix}" fi } list_entry () { # Decrypt the index to list entries. - if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi + if [[ ! -s "${safe_ix}" ]] ; then fail "${safe_ix} not found" ; fi - if [[ "${encrypt_index}" = "true" ]] ; then - prompt_key "index" - decrypt "${safeix}" || fail "Decryption failed" - else - printf "\n" - cat "${safeix}" + if [[ -n "${encrypt_index}" ]] ; then prompt_key "index" + decrypt "${safe_ix}" || fail "${safe_ix} not available" + else printf "\n" ; cat "${safe_ix}" fi } backup () { # Archive index, safe and configuration. - if [[ -f "${safeix}" && -d "${safedir}" ]] ; then - cp "${gpgconf}" "gpg.conf.${now}" - tar --create --file "${backuptar}" \ - "${safeix}" "${safedir}" "gpg.conf.${now}" "${script}" - rm "gpg.conf.${now}" + 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 - - printf "\nArchived %s\n" "${backuptar}" } clip () { - # Use clipboard and clear after timeout. + # Use clipboard or stdout and clear after timeout. - ${copy} < "${1}" + if [[ "${clip_dest}" = "screen" ]] ; then + printf '\n%s\n' "$(cat ${1})" + else "${copy}" < "${1}" ; fi printf "\n" - shift - while [ $cb_timeout -gt 0 ] ; do - printf "\r\033[KPassword on clipboard! Clearing in %.d" $((cb_timeout--)) - sleep 1 + while [ "${clip_timeout}" -gt 0 ] ; do + printf "\r\033[K Password on %s! Clearing in %.d" \ + "${clip_dest}" "$((clip_timeout--))" ; sleep 1 done + printf "\r\033[K Clearing password from %s ..." "${clip_dest}" - printf "\n" - printf "" | ${copy} + if [[ "${clip_dest}" = "screen" ]] ; then + clear + else printf "\n" ; printf "" | "${copy}" ; fi } setup_keygroup() { - # Configure GPG keygroup. + # 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" " ")" - printf "\n Setting up GPG key group ... + printf "\n Setting up keygroup ...\n + Found recommended key IDs: %s\n + Enter one or more key IDs, preferred one last\n" "${recommend}" - Found key IDs: %s - - Enter backup key IDs first, preferred key IDs last. - " "${recommend}" - - while [[ -z "${keyid}" ]] ; do - read -r -p " + while [[ -z "${keyid}" ]] ; do read -r -p " Key ID or Enter to continue: " keyid if [[ -z "${keyid}" ]] ; then - printf "%s\n" "$purse_keygroup" >> "${gpgconf}" + printf "%s\n" "${purse_keygroup}" >> "${gpg_conf}" break fi purse_keygroup="${purse_keygroup} ${keyid}" @@ -224,18 +229,18 @@ setup_keygroup() { new_entry () { # Prompt for username and password. - username="" while [[ -z "${username}" ]] ; do if [[ -z "${2+x}" ]] ; then read -r -p " Username: " username else username="${2}" ; fi done - if [[ -z "${3+x}" ]] ; then get_pass " - Password for \"${username}\" (Enter to generate): " + if [[ -z "${3+x}" ]] ; then + get_pass "Password for \"${username}\" (Enter to generate): " userpass="${password}" fi + printf "\n" if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@") fi @@ -244,83 +249,67 @@ new_entry () { print_help () { # Print help text. - 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. - - Purse can be used interactively or by passing one of the following options: - + 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 - * 'b' to create an archive for backup - - Example usage: - - * Generate a 30 character password for 'userName': - ./purse.sh w userName 30 - - * Copy the password for 'userName' to clipboard: - ./purse.sh r userName - - * List stored passwords and copy a specific version: - ./purse.sh l - ./purse.sh r userName@1574723625 - - * Create an archive for backup: - ./purse.sh b - - * Restore an archive from backup: - tar xvf purse*tar""" + * '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""" } -if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi +if [[ -z "${gpg}" || ! -x "${gpg}" ]] ; then fail "GnuPG is not available" ; fi -if [[ -z ${copy} && ! -x ${copy} ]] ; then fail "Clipboard is not available" ; fi +if [[ ! -f "${gpg_conf}" ]] ; then fail "GnuPG config is not available" ; fi -if [[ ! -f ${gpgconf} ]] ; then fail "GnuPG config is not available" ; fi +if [[ ! -d "${safe_dir}" ]] ; then mkdir -p "${safe_dir}" ; fi -if [[ ! -d "${safedir}" ]] ; then mkdir -p "${safedir}" ; fi +chmod -R 0700 "${safe_dir}" "${safe_ix}" 2>/dev/null -chmod -R 0600 "${safeix}" 2>/dev/null -chmod -R 0700 "${safedir}" 2>/dev/null +if [[ -z "${copy}" || ! -x "${copy}" ]] ; then + warn "Clipboard not available, passwords will print to screen/stdout!" + clip_dest="screen" +fi +username="" password="" action="" +encrypt_index="" + if [[ -n "${1+x}" ]] ; then action="${1}" ; fi -while [[ -z "${action}" ]] ; do - read -r -n 1 -p " +while [[ -z "${action}" ]] ; do read -r -n 1 -p " Read or Write (or Help for more options): " action printf "\n" done -if [[ "${action}" =~ ^([hH])$ ]] ; then - print_help - -elif [[ "${action}" =~ ^([bB])$ ]] ; then - backup - -elif [[ "${action}" =~ ^([lL])$ ]] ; then - list_entry +if [[ "${action}" =~ ^([rR])$ ]] ; then + read_pass "$@" elif [[ "${action}" =~ ^([wW])$ ]] ; then - purse_keygroup=$(grep "group purse_keygroup" "${gpgconf}") + 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 - - if [[ "${daily_backup}" = "true" ]] ; then - if [[ ! -f ${backuptar} ]] ; then - backup - fi + if [[ -n "${daily_backup}" && ! -f "${safe_backup}" ]] + then backup fi - -else read_pass "$@" ; fi - -chmod -R 0400 "${safeix}" "${safedir}" 2>/dev/null +elif [[ "${action}" =~ ^([lL])$ ]] ; then list_entry +elif [[ "${action}" =~ ^([bB])$ ]] ; then backup +else print_help ; fi tput setaf 2 ; printf "\nDone\n" ; tput sgr0