From 80d501cba87fee12cb757f149d5cd6ef0a1ba903 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 14:18:57 -0700 Subject: [PATCH 1/8] Update to pwd.sh parity --- purse.sh | 261 ++++++++++++++++++++++++++----------------------------- 1 file changed, 125 insertions(+), 136 deletions(-) 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 From 02b910b326b88a41e7627513cf95923921f10449 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 14:23:36 -0700 Subject: [PATCH 2/8] Add configuration options to README --- README.md | 52 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ee7765f..6f6e992 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,35 @@ -# Purse - Purse is a fork of [drduh/pwd.sh](https://github.com/drduh/pwd.sh). Both programs are Bash shell scripts which use [GnuPG](https://www.gnupg.org/) to manage passwords and other secrets in encrypted text files. Purse is based on asymmetric (public-key) authentication, while pwd.sh is based on symmetric (password-based) authentication. While both scripts use a trusted crypto implementation (GnuPG) and safely handle passwords (never saving plaintext to disk, only using shell built-ins), Purse eliminates the need to remember a master password - just plug in a YubiKey, enter the PIN, then touch it to decrypt a password to clipboard. -# Release notes +# Install -See [Releases](https://github.com/drduh/Purse/releases) +This script requires a GnuPG identity - see [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) to set one up. -# Use - -This script requires a GnuPG identity - see [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) to set one up. Multiple identities stored on several YubiKeys are recommended for improved durability and reliability. - -Clone the repository: +For the latest version, clone the repository or download the script directly: ```console git clone https://github.com/drduh/Purse -``` -Or download the script directly: - -```console wget https://github.com/drduh/Purse/blob/master/purse.sh ``` +Versioned [Releases](https://github.com/drduh/Purse/releases) are also available. + +# Use + Run the script interactively using `./purse.sh` or symlink to a directory in `PATH`: -* Type `w` to write a password -* Type `r` to read a password -* Type `l` to list passwords -* Type `b` to create an archive for backup -* Type `h` to print the help text +- `w` to write a password +- `r` to read a password +- `l` to list passwords +- `b` to create an archive for backup +- `h` to print the help text Options can also be passed on the command line. -Example usage: - Create a 20-character password for `userName`: ```console @@ -50,7 +42,7 @@ Read password for `userName`: ./purse.sh r userName ``` -Passwords are stored with a 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: +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: ```console ./purse.sh l @@ -70,8 +62,24 @@ Restore an archive from backup: tar xvf purse*tar ``` +# Configure + +Several customizable options and features are also available, and can be configured with environment variables, for example in the [shell rc](https://github.com/drduh/config/blob/master/zshrc) file: + +Variable | Description | Default | Values +-|-|-|- +`PURSE_TIME` | seconds to clear password from clipboard/screen | `10` | any valid integer +`PURSE_LEN` | default generated password length | `14` | any valid integer +`PURSE_COPY` | copy password to clipboard before write | unset (disabled) | `1` or `true` to enable +`PURSE_DAILY` | create daily backup archive on write | unset (disabled) | `1` or `true` to enable +`PURSE_COMMENT` | **unencrypted** comment to include in index and safe files | unset | any valid string +`PURSE_DEST` | password output destination, will set to `screen` without clipboard | `clipboard` | `clipboard` or `screen` +`PURSE_SAFE` | safe directory name | `safe` | any valid string +`PURSE_INDEX` | index file name | `purse.index` | any valid string +`PURSE_BACKUP` | backup archive file name | `purse.$hostname.$today.tar` | any valid string + **Note** For additional privacy, the recipient key ID is **not** included in metadata (`throw-keyids` option). The password index file can also be encrypted by changing the `encrypt_index` variable to `true` in the script, although two touches will be required for two separate decryption operations. -See [config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional configuration options. +See [config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GnuPG options. From f429a526253e1f1ea5b7c82e6468335e6ac30e40 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 14:32:02 -0700 Subject: [PATCH 3/8] Safer archive backups --- README.md | 4 ++-- purse.sh | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6f6e992..46c5f7d 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ Variable | Description | Default | Values `PURSE_INDEX` | index file name | `purse.index` | any valid string `PURSE_BACKUP` | backup archive file name | `purse.$hostname.$today.tar` | any valid string -**Note** For additional privacy, the recipient key ID is **not** included in metadata (`throw-keyids` option). +**Note** For additional privacy, the recipient key ID is **not** included in metadata (GnuPG `throw-keyids` option). -The password index file can also be encrypted by changing the `encrypt_index` variable to `true` in the script, although two touches will be required for two separate decryption operations. +The password index file can also be encrypted by changing the `encrypt_index` variable to `true` in the script, however 2 YubiKey touches will be required (for 2 separate decryption operations). See [config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GnuPG options. diff --git a/purse.sh b/purse.sh index 6e673fc..27fb042 100755 --- a/purse.sh +++ b/purse.sh @@ -104,7 +104,6 @@ read_pass () { tail -1 | cut -d ":" -f2) fi - set -x prompt_key "password" if [[ -s "${spath}" ]] ; then decrypt "${spath}" || fail "Failed to decrypt ${spath}" @@ -175,13 +174,15 @@ list_entry () { backup () { # Archive index, safe and configuration. - 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 + 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 () { @@ -192,14 +193,13 @@ clip () { else "${copy}" < "${1}" ; fi printf "\n" - while [ "${clip_timeout}" -gt 0 ] ; do + 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}" - if [[ "${clip_dest}" = "screen" ]] ; then - clear + if [[ "${clip_dest}" = "screen" ]] ; then clear else printf "\n" ; printf "" | "${copy}" ; fi } From 404d5402d143bde492fc897a4bac4d75df110de6 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 14:52:56 -0700 Subject: [PATCH 4/8] Style touch-ups --- purse.sh | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/purse.sh b/purse.sh index 27fb042..279a359 100755 --- a/purse.sh +++ b/purse.sh @@ -19,17 +19,18 @@ 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 +pass_len="${PURSE_LEN:=14}" # default 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. + # "Lock" files on trapped exits. ret=$? - chmod -R 0000 "${safe_ix}" "${safe_dir}" 2>/dev/null + chmod -R 0000 \ + "${safe_dir}" "${safe_ix}" 2>/dev/null exit ${ret} } @@ -53,11 +54,9 @@ get_pass () { printf "\n" while IFS= read -p "${prompt}" -r -s -n 1 char ; do - if [[ ${char} == $'\0' ]] ; then - break + if [[ ${char} == $'\0' ]] ; then break elif [[ ${char} == $'\177' ]] ; then - if [[ -z "${password}" ]] ; then - prompt="" + if [[ -z "${password}" ]] ; then prompt="" else prompt=$'\b \b' password="${password%?}" @@ -126,7 +125,9 @@ gen_pass () { Password length (default: ${pass_len}): " length else length="${3}" ; fi - if [[ ${length} =~ ^[0-9]+$ ]] ; then pass_len=${length} ; fi + if [[ "${length}" =~ ^[0-9]+$ ]] ; then + pass_len="${length}" + fi tr -dc "${pass_chars}" < /dev/urandom | \ fold -w "${pass_len}" | head -1 @@ -156,7 +157,8 @@ write_pass () { mv "${safe_ix}.${now}" "${safe_ix}" || \ fail "Failed saving ${safe_ix}.${now}" else - printf "%s@%s:%s\n" "${username}" "${now}" "${spath}" >> "${safe_ix}" + printf "%s@%s:%s\n" \ + "${username}" "${now}" "${spath}" >> "${safe_ix}" fi } @@ -177,7 +179,7 @@ backup () { 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}" \ + tar cf "${safe_backup}" "${safe_dir}" "${safe_ix}" \ "${BASH_SOURCE}" "gpg.conf.${today}" && \ printf "\nArchived %s\n" "${safe_backup}" rm -f "gpg.conf.${today}" @@ -249,7 +251,8 @@ new_entry () { print_help () { # Print help text. - 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 + 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 Purse can be used interactively or by passing one of the following options:\n * 'w' to write a password * 'r' to read a password @@ -294,9 +297,7 @@ while [[ -z "${action}" ]] ; do read -r -n 1 -p " printf "\n" done -if [[ "${action}" =~ ^([rR])$ ]] ; then - read_pass "$@" - +if [[ "${action}" =~ ^([rR])$ ]] ; then read_pass "$@" elif [[ "${action}" =~ ^([wW])$ ]] ; then purse_keygroup="$(grep "group purse_keygroup" "${gpg_conf}")" if [[ -z "${purse_keygroup}" ]] ; then @@ -305,9 +306,7 @@ elif [[ "${action}" =~ ^([wW])$ ]] ; then printf "\n %s\n" "${purse_keygroup}" new_entry "$@" write_pass - if [[ -n "${daily_backup}" && ! -f "${safe_backup}" ]] - then backup - fi + if [[ -n "${daily_backup}" ]] ; then backup ; fi elif [[ "${action}" =~ ^([lL])$ ]] ; then list_entry elif [[ "${action}" =~ ^([bB])$ ]] ; then backup else print_help ; fi From e6c3828504f80e076630aa5c74569c584df48eaf Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 15:57:33 -0700 Subject: [PATCH 5/8] Fix clip functionality --- .gitignore | 2 +- purse.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 780a118..f5fe9d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ purse.*.tar -purse.index +purse.index* safe/ diff --git a/purse.sh b/purse.sh index 279a359..3052c5e 100755 --- a/purse.sh +++ b/purse.sh @@ -105,7 +105,8 @@ read_pass () { prompt_key "password" if [[ -s "${spath}" ]] ; then - decrypt "${spath}" || fail "Failed to decrypt ${spath}" + clip <(decrypt "${spath}" | head -1) || \ + fail "Failed to decrypt ${spath}" else fail "Secret not available" fi } From 1b990c96a672704c7712f64b4e2b5f09c72d59e6 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 16:36:25 -0700 Subject: [PATCH 6/8] Add chars and echo config options --- README.md | 2 ++ purse.sh | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46c5f7d..147eeb5 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,10 @@ Variable | Description | Default | Values `PURSE_LEN` | default generated password length | `14` | any valid integer `PURSE_COPY` | copy password to clipboard before write | unset (disabled) | `1` or `true` to enable `PURSE_DAILY` | create daily backup archive on write | unset (disabled) | `1` or `true` to enable +`PURSE_CHARS` | character set for passwords | `[:alnum:]!?@#$%^&*();:+=` | any valid characters `PURSE_COMMENT` | **unencrypted** comment to include in index and safe files | unset | any valid string `PURSE_DEST` | password output destination, will set to `screen` without clipboard | `clipboard` | `clipboard` or `screen` +`PURSE_ECHO` | character used to echo password input | `*` | any valid character `PURSE_SAFE` | safe directory name | `safe` | any valid string `PURSE_INDEX` | index file name | `purse.index` | any valid string `PURSE_BACKUP` | backup archive file name | `purse.$hostname.$today.tar` | any valid string diff --git a/purse.sh b/purse.sh index 3052c5e..a55b15a 100755 --- a/purse.sh +++ b/purse.sh @@ -12,17 +12,18 @@ today="$(date +%F)" copy="$(command -v xclip || command -v pbcopy)" gpg="$(command -v gpg || command -v gpg2)" 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_echo="${PURSE_ECHO:=*}" # show "*" when typing passwords pass_len="${PURSE_LEN:=14}" # default 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}" +pass_chars="${PURSE_CHARS:='[:alnum:]!?@#$%^&*();:+='}" trap cleanup EXIT INT TERM cleanup () { @@ -62,7 +63,7 @@ get_pass () { password="${password%?}" fi else - prompt="*" + prompt="${pass_echo}" password+="${char}" fi done From c8ede9797a9b5c37d22f0d55232caf62738123d8 Mon Sep 17 00:00:00 2001 From: drduh Date: Tue, 26 Mar 2024 16:56:11 -0700 Subject: [PATCH 7/8] Encrypted index config option --- README.md | 5 +++-- purse.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 147eeb5..0f9cb3f 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,9 @@ Variable | Description | Default | Values `PURSE_LEN` | default generated password length | `14` | any valid integer `PURSE_COPY` | copy password to clipboard before write | unset (disabled) | `1` or `true` to enable `PURSE_DAILY` | create daily backup archive on write | unset (disabled) | `1` or `true` to enable -`PURSE_CHARS` | character set for passwords | `[:alnum:]!?@#$%^&*();:+=` | any valid characters +`PURSE_ENCIX` | also encrypte index for additional privacy ; 2 YubiKey touches will be required for separate decryption operations | unset (disabled) | `1` or `true` to enable `PURSE_COMMENT` | **unencrypted** comment to include in index and safe files | unset | any valid string +`PURSE_CHARS` | character set for passwords | `[:alnum:]!?@#$%^&*();:+=` | any valid characters `PURSE_DEST` | password output destination, will set to `screen` without clipboard | `clipboard` | `clipboard` or `screen` `PURSE_ECHO` | character used to echo password input | `*` | any valid character `PURSE_SAFE` | safe directory name | `safe` | any valid string @@ -82,6 +83,6 @@ Variable | Description | Default | Values **Note** For additional privacy, the recipient key ID is **not** included in metadata (GnuPG `throw-keyids` option). -The password index file can also be encrypted by changing the `encrypt_index` variable to `true` in the script, however 2 YubiKey touches will be required (for 2 separate decryption operations). + See [config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GnuPG options. diff --git a/purse.sh b/purse.sh index a55b15a..205c47e 100755 --- a/purse.sh +++ b/purse.sh @@ -17,6 +17,7 @@ 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 +encrypt_index="${PURSE_ENCIX:=}" # also keep index encrypted pass_copy="${PURSE_COPY:=}" # copy password before write pass_echo="${PURSE_ECHO:=*}" # show "*" when typing passwords pass_len="${PURSE_LEN:=14}" # default password length @@ -290,7 +291,6 @@ fi username="" password="" action="" -encrypt_index="" if [[ -n "${1+x}" ]] ; then action="${1}" ; fi From abcb88a032e25a6dea88ced430702d2a4a5a8e40 Mon Sep 17 00:00:00 2001 From: drduh Date: Wed, 27 Mar 2024 10:11:45 -0700 Subject: [PATCH 8/8] grammar --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f9cb3f..0499a36 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Purse is a fork of [drduh/pwd.sh](https://github.com/drduh/pwd.sh). Both programs are Bash shell scripts which use [GnuPG](https://www.gnupg.org/) to manage passwords and other secrets in encrypted text files. Purse is based on asymmetric (public-key) authentication, while pwd.sh is based on symmetric (password-based) authentication. -While both scripts use a trusted crypto implementation (GnuPG) and safely handle passwords (never saving plaintext to disk, only using shell built-ins), Purse eliminates the need to remember a master password - just plug in a YubiKey, enter the PIN, then touch it to decrypt a password to clipboard. +While both scripts use a trusted crypto implementation (GnuPG) and safely handle passwords (never saving plaintext to disk, only using shell built-ins), Purse eliminates the need to remember a main passphrase - just plug in a YubiKey, enter the PIN, then touch it to decrypt a password to clipboard. # Install @@ -72,7 +72,7 @@ Variable | Description | Default | Values `PURSE_LEN` | default generated password length | `14` | any valid integer `PURSE_COPY` | copy password to clipboard before write | unset (disabled) | `1` or `true` to enable `PURSE_DAILY` | create daily backup archive on write | unset (disabled) | `1` or `true` to enable -`PURSE_ENCIX` | also encrypte index for additional privacy ; 2 YubiKey touches will be required for separate decryption operations | unset (disabled) | `1` or `true` to enable +`PURSE_ENCIX` | encrypt index for additional privacy; 2 YubiKey touches will be required for separate decryption operations | unset (disabled) | `1` or `true` to enable `PURSE_COMMENT` | **unencrypted** comment to include in index and safe files | unset | any valid string `PURSE_CHARS` | character set for passwords | `[:alnum:]!?@#$%^&*();:+=` | any valid characters `PURSE_DEST` | password output destination, will set to `screen` without clipboard | `clipboard` | `clipboard` or `screen`