From 8d9ca6c14d24838d4d69fcba2b3b5484e9a5be4d Mon Sep 17 00:00:00 2001 From: drduh Date: Thu, 28 Nov 2019 15:18:48 -0800 Subject: [PATCH] Version 2 beta, see release notes in README. --- README.md | 90 +++++++++++++------- purse.sh | 243 +++++++++++++++++++++++++++++------------------------- 2 files changed, 191 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 304de48..e0307fd 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,100 @@ # Purse -![screencast gif](https://user-images.githubusercontent.com/12475110/40880505-3834ce1c-6667-11e8-89d0-6961886842c6.gif) - Purse is a fork of [drduh/pwd.sh](https://github.com/drduh/pwd.sh). -Both programs are shell scripts which use [GPG](https://www.gnupg.org/) to manage passwords in an encrypted text file. Purse uses asymmetric (public-key) authentication, while pwd.sh uses symmetric (password-based) authentication. +Both programs are Bash shell scripts which use [GPG](https://www.gnupg.org/) to manage passwords and other secrets in encrypted text files. Purse uses asymmetric (public-key) authentication, while pwd.sh uses symmetric (password-based) authentication. -While both scripts use a trusted crypto implementation (GPG) and safely handle passwords (never saving plaintext to disk), Purse eliminates the need to remember and use a master password - just plug in a YubiKey, enter the PIN, then touch it to decrypt the password safe to stdout. +While both scripts use a trusted crypto implementation (GPG) and safely handle passwords (never saving plaintext to disk), Purse eliminates the need to remember and use a master password - just plug in a YubiKey, enter the PIN, then touch it to decrypt a password to clipboard. -By using Purse with YubiKey, the risk of master password phishing and keylogging is eliminated - only physical possession of the key AND knowledge of the PIN can unlock the password safe. +By using Purse with YubiKey, the risk of master password theft or keylogging is eliminated - only physical possession of the Yubikey AND knowledge of the PIN can unlock the encrypted index and password files. -# Installation +# Release notes + +## Version 1 (2018) + +The original release which has been available for general use and review since June 2018 (forked from pwd.sh which dates to 2015). There are no known bugs nor security vulnerabilities identified in this stable version of purse.sh. Compatible on Linux, OpenBSD, macOS. + +## Version 2b (2019) + +The second release of purse.sh features several security and reliability improvements, and is an optional upgrade. Currently in beta testing. Compatible on Linux, OpenBSD, macOS. + +Known issues: +* Read actions now require two Yubikey touches, if touch to decrypt is enabled - once for the index and twice for the encrypted password file. + +Changelist: +* Passwords are now encrypted as individual files, rather than all encrypted as a single flat file. +* Individual password filenames are random, mapped to usernames in an encrypted index file. +* Index and password files are now "immutable" using chmod while purse.sh is not running. +* Read passwords are now copied to clipboard and cleared after a timeout, instead of printed to stdout. +* Use printf instead of echo for improved portability. +* New option: list passwords in the index. +* New option: create tar archive for backup. +* Removed option: delete password; the index is now a permanent ledger. +* Removed option: read all passwords; no use case for having a single command. +* Removed option: suppress generated password output; should be read from safe to verify save. + +# Use This script requires a GPG identity - see [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) to set one up. -To install Purse: - ```console $ git clone https://github.com/drduh/Purse ``` -Edit `purse.sh` to specify your GPG key ID. +Set your GPG key ID with `export PURSE_KEYID=0xFF3E7D88647EBCDB` or by editing `purse.sh`. -# Use +`cd purse.sh` and run the script interactively using `./purse.sh` or symlink to a directory in `PATH`: -`cd Purse` and run the script interactively using `./purse.sh` +* 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 -* Type `w` to write a password. -* Type `r` to read a password. -* Type `d` to delete a password. -* Type `h` to print the help text. +Options can also be passed on the command line. -Examples: +Example usage: -Create 30-character password for `gmail`: +Create a 30-character password for `userName`: ```console -$ ./purse.sh w gmail 30 +$ ./purse.sh w userName 30 ``` -Append `q` to create a password without displaying it. - -Read password for `user@github`: +Read password for `userName`: ```console -$ ./purse.sh r user@github +$ ./purse.sh r userName ``` -Delete password for `reddit`: +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 previous version of a password: ```console -$ ./purse.sh d reddit +$ ./purse.sh l + +$ ./purse.sh r userName@1574723600 ``` -Copy password for `github` to clipboard (substitute `pbcopy` on macOS): +Create an archive for backup: ```console -$ ./purse.sh r github | cut -f 1 -d ' ' | awk 'NR==4{print $1}' | xclip +$ ./purse.sh b ``` -This script and encrypted `purse.enc` file can be publicly shared between trusted computers. For additional privacy, the recipient key ID is **not** included in GPG metadata. +Restore an archive from backup: -See [drduh/config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GPG options. +```console +$ tar xvf purse*tar +``` + +The backup contains only encrypted files and can be publicly shared for use on trusted computers. For additional privacy, the recipient key ID is **not** included in GPG metadata (`throw-keyids` option). + +See [drduh/config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GPG configuration options. # Similar software * [drduh/pwd.sh](https://github.com/drduh/pwd.sh) -* [bndw/pick: command-line password manager for macOS and Linux](https://github.com/bndw/pick) -* [Pass: the standard unix password manager](https://www.passwordstore.org/) -* [anders/pwgen: generate passwords using OS X Security framework](https://github.com/anders/pwgen) +* [zx2c4/password-store](https://github.com/zx2c4/password-store) * [caodonnell/passman.sh: a pwd.sh fork](https://github.com/caodonnell/passman.sh) +* [bndw/pick: command-line password manager for macOS and Linux](https://github.com/bndw/pick) +* [anders/pwgen: generate passwords using OS X Security framework](https://github.com/anders/pwgen) diff --git a/purse.sh b/purse.sh index 8dc60fa..071e641 100755 --- a/purse.sh +++ b/purse.sh @@ -5,23 +5,26 @@ set -o errtrace set -o nounset set -o pipefail +#set -x # uncomment to debug + umask 077 -filter="$(command -v grep) -v -E" +now=$(date +%s) +copy="$(command -v xclip || command -v pbcopy)" gpg="$(command -v gpg || command -v gpg2)" -safe="${PURSE_SAFE:=purse.enc}" -keyid="0xFF3E7D88647EBCDB" - +backuptar="${PURSE_BACKUP:=purse.$(hostname).$(date +%F).tar}" +keyid="${PURSE_KEYID:=0xFF3E7D88647EBCDB}" +safeix="${PURSE_INDEX:=purse.index}" +safedir="${PURSE_SAFE:=safe}" +timeout=9 fail () { # Print an error message and exit. - printf "\n\n" - tput setaf 1 1 1 ; echo "Error: ${1}" ; tput sgr0 + tput setaf 1 1 1 ; printf "\nError: ${1}\n" ; tput sgr0 exit 1 } - get_pass () { # Prompt for a password. @@ -45,14 +48,12 @@ get_pass () { done } - decrypt () { - # Decrypt with authorized GPG key. + # Decrypt with GPG. cat "${1}" | ${gpg} --armor --batch --decrypt 2>/dev/null } - encrypt () { # Encrypt to a recipient. @@ -60,171 +61,191 @@ encrypt () { --recipient ${keyid} --output "${1}" "${2}" } - read_pass () { # Read a password from safe. - if [[ ! -s ${safe} ]] ; then fail "${safe} not found" ; fi + if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi - if [[ -z "${2+x}" ]] ; then read -r -p " - Username (Enter for all): " username - else - username="${2}" - fi + username="" + while [[ -z "${username}" ]] ; do + if [[ -z "${2+x}" ]] ; then read -r -p " + Username: " username + else username="${2}" ; fi + done - if [[ -z "${username}" || "${username}" == "all" ]] ; then username="" ; fi + prompt_key "index" - prompt_key - decrypt ${safe} | grep -F " ${username}" || fail "Decryption failed" + spath=$(decrypt ${safeix} | \ + grep -F "${username}" | tail -n1 | cut -d : -f2) || \ + fail "Decryption failed" + + prompt_key "password" + + clip <(decrypt ${spath} | head -n1) || \ + fail "Decryption failed" } - prompt_key () { - # Print a message when key touch is needed. + # Print a message if safe file exists. - if [[ -f "${safe}" ]] ; then - printf "\n Touch key to decrypt safe ...\n\n" + if [[ -f "${safeix}" ]] ; then + printf "\n Touch key to access ${1} ...\n\n" fi } - gen_pass () { - # Generate a password. + # Generate a password using GPG. len=20 max=80 if [[ -z "${3+x}" ]] ; then read -p " + Password length (default: ${len}, max: ${max}): " length - else - length="${3}" - fi + else length="${3}" ; fi if [[ ${length} =~ ^[0-9]+$ ]] ; then len=${length} ; fi # base64: 4 characters for every 3 bytes ${gpg} --armor --gen-random 0 "$((${max} * 3/4))" | cut -c -"${len}" - } - - -write_pass () { - # Write a password in safe. - - # If no password (delete action), clear the entry by writing an empty line. - if [[ -z "${userpass+x}" ]] ; then - entry=" " - else - entry="${userpass} ${username}" - fi - - prompt_key - - # If safe exists, decrypt it and filter out username, or bail on error. - # If successful, append entry, or blank line. - # Filter blank lines and previous timestamp, append fresh timestamp. - # Finally, encrypt it all to a new safe file, or fail. - # If successful, update to new safe file. - ( if [[ -f "${safe}" ]] ; then - decrypt ${safe} | ${filter} " ${username}$" || return - fi ; \ - echo "${entry}") | \ - (${filter} "^[[:space:]]*$|^mtime:[[:digit:]]+$";echo mtime:$(date +%s)) | \ - encrypt ${safe}.new - || fail "Write to safe failed" - mv ${safe}{.new,} } +write_pass () { + # Write a password and update index file. + + fpath=$(tr -dc "[:lower:]" < /dev/urandom | fold -w8 | head -n1) + spath=${safedir}/${fpath} + printf '%s\n' "${userpass}" | \ + encrypt ${spath} - || \ + fail "Failed to put ${spath}" + + prompt_key "index" + + ( if [[ -f "${safeix}" ]] ; then + decrypt ${safeix} || return ; fi + printf "${username}@${now}:${spath}\n") | \ + encrypt ${safeix}.${now} - || \ + fail "Failed to put ${safeix}.${now}" + + mv ${safeix}{.${now},} +} + +list_entry () { + # Decrypt the index to list entries. + + if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi + + prompt_key "index" + + decrypt ${safeix} || fail "Decryption failed" +} + +backup () { + # Archive encrypted index and safe directory. + + if [[ -f ${safeix} && -d ${safedir} ]] ; then \ + tar cfv ${backuptar} ${safeix} ${safedir} + else fail "Nothing to archive" ; fi + printf "\nArchived ${backuptar}\n" ; \ +} + +clip () { + # Use clipboard and clear after timeout. + + ${copy} < ${1} + + printf "\n" + shift + while [ $timeout -gt 0 ] ; do + printf "\r\033[KPassword on clipboard! Clearing in %.d" $((timeout--)) + sleep 1 + done + + printf "" | ${copy} +} new_entry () { # Prompt for new username and/or password. - if [[ -z "${2+x}" ]] ; then read -r -p " + username="" + while [[ -z "${username}" ]] ; do + if [[ -z "${2+x}" ]] ; then read -r -p " Username: " username - else - username="${2}" - fi + else username="${2}" ; fi + done 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 "$@") - if [[ -z "${4+x}" || ! "${4}" =~ ^([qQ])$ ]] ; then - printf "\n Password: ${userpass}\n" - fi - fi + if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@") ; fi } print_help () { # Print help text. - echo " - Purse is a password manager shell script using GnuPG asymmetric encryption. It is recommended for use with Yubikey or similar hardware token. Purse can be used interactively or with one of the following options: + 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: - * 'r' to read a password * 'w' to write a password - * 'd' to delete a password - * 'h' to print this help text + * 'r' to read a password + * 'l' to list passwords + * 'b' to create an archive for backup - A username, password length and 'q' options can also be used. + Example usage: - Examples: + * Generate a 30 character password for 'userName': + ./purse.sh w userName 30 - * Read all passwords: + * Copy the password for 'userName' to clipboard: + ./purse.sh r userName - ./purse.sh r all + * List stored passwords and copy a previous version: + ./purse.sh l + ./purse.sh r userName@1574723625 - * Write a password for 'github': + * Create an archive for backup: + ./purse.sh b - ./purse.sh w github - - * Generate a 50 character password for 'github' and write: - - ./purse.sh w github 50 - - * Generate a password and write without displaying it: - - ./purse.sh w github 50 q - - * Delete password for 'mail': - - ./purse.sh d mail - - A password cannot be supplied as an argument, nor is used as one in the script, to prevent it from appearing in process listing or logs." + * Restore an archive from backup: + tar xvf purse*tar""" } - if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi +if [[ ! -d ${safedir} ]] ; then mkdir -p ${safedir} ; fi + +chmod -R 0600 ${safeix} 2>/dev/null +chmod -R 0700 ${safedir} 2>/dev/null + password="" - action="" -if [[ -n "${1+x}" ]] ; then - action="${1}" -fi +if [[ -n "${1+x}" ]] ; then action="${1}" ; fi -while [[ -z "${action}" ]] ; - do read -n 1 -p " - Read, Write, or Delete password (or Help): " action +while [[ -z "${action}" ]] ; do + read -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 + elif [[ "${action}" =~ ^([wW])$ ]] ; then new_entry "$@" write_pass -elif [[ "${action}" =~ ^([dD])$ ]] ; then - if [[ -z "${2+x}" ]] ; then read -p " - Username: " username - else - username="${2}" - fi - write_pass -else - read_pass "$@" -fi -printf "\n" ; tput setaf 2 2 2 ; echo "Done" ; tput sgr0 +else read_pass "$@" ; fi + +chmod -R 0400 ${safeix} ${safedir} 2>/dev/null + +tput setaf 2 2 2 ; printf "\nDone\n" ; tput sgr0