diff --git a/README.md b/README.md index 52e8e55..85f1c16 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ This script uses GPG to manage an encrypted text file containing passwords. # Requirements Requires `gpg`. Install with `brew install gpg` or `apt-get install gnupg` or build and install it from [source](https://www.gnupg.org/download/index.html). -Requires `srm`. Install with `apt-get install secure-delete`. Already included in OS X. - # Installation git clone https://github.com/drduh/pwd.sh && cd pwd.sh @@ -24,5 +22,6 @@ Type `r` to print stored passwords. Can be piped to `grep` and `pbcopy` or `xsel Type `d` to delete a password by Username/ID. -To reset/erase keys and passwords, `rm pwd.sh.*`. +The encrypted file `pwd.sh.safe` can be safely shared between machines over public channels (Google Drive, Dropbox, etc). +A sample `gpg.conf` configuration file is provided for your consideration. diff --git a/gpg.conf b/gpg.conf new file mode 100644 index 0000000..439be5c --- /dev/null +++ b/gpg.conf @@ -0,0 +1,27 @@ +# ~/.gnupg/gpg.conf +# +# Suggested GnuPG configuration. + +auto-key-locate keyserver + +keyserver hkps://hkps.pool.sks-keyservers.net +keyserver-options no-honor-keyserver-url +keyserver-options debug +keyserver-options verbose + +# sudo curl -o /etc/gpg.pem https://sks-keyservers.net/sks-keyservers.netCA.pem +keyserver-options ca-cert-file=/etc/gpg.pem + +personal-cipher-preferences AES256 AES192 AES CAST5 +personal-digest-preferences SHA512 SHA384 SHA256 SHA224 +default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed +cert-digest-algo SHA512 + +charset utf-8 +fixed-list-mode +no-comments +no-emit-version +keyid-format 0xlong +list-options show-uid-validity +verify-options show-uid-validity +with-fingerprint diff --git a/pwd.sh b/pwd.sh index 9d85103..cef424a 100755 --- a/pwd.sh +++ b/pwd.sh @@ -1,217 +1,150 @@ #!/bin/bash # -# pwd.sh -# -# An interface to gpg for managing passwords. +# Script for managing passwords in a symmetrically encrypted file using GnuPG. -set -o pipefail +set -o errtrace set -o nounset - -umask 077 - -safe=pwd.sh.safe -public=pwd.sh.pub -secret=pwd.sh.sec - -del=$(which srm) -del_opts=("--force --zero") +set -o pipefail gpg=$(which gpg) -gpg_opts=("--no-default-keyring --keyring ./${public} --secret-keyring ./${secret}") +safe=pwd.sh.safe -name="nobody@pwd.sh" + +fail () { + # Print an error message and exit. + + tput setaf 1 ; echo "Error: ${1}" + exit 1 +} get_pass () { - # Fancy prompt for fetching a password. + # Prompt for a password. unset password - prompt="Password: " - while IFS= read -p "$prompt" -r -s -n 1 char - do - if [[ $char == $'\0' ]] ; then - break + prompt="${1}" + while IFS= read -p "${prompt}" -r -s -n 1 char ; do + if [[ ${char} == $'\0' ]] ; then + break fi prompt='*' - password+="$char" + password+="${char}" done + + if [ -z ${password+x} ] ; then + fail "No password provided" + fi } decrypt () { - # Decrypt a gpg-encrypted file with a password. + # Decrypt with a password. - ${gpg} ${gpg_opts} \ + ${gpg} \ --decrypt --armor --batch \ - --command-fd 0 --passphrase "${1}" "${2}" \ - 2>/dev/null + --command-fd 0 --passphrase "${1}" "${2}" 2>/dev/null } encrypt () { - # Encrypt and sign a file with a password. + # Encrypt with a password. - ${gpg} ${gpg_opts} \ - --encrypt --armor --sign --batch \ - --hidden-recipient "${name}" \ - --yes \ + ${gpg} \ + --symmetric --armor --batch --yes \ --command-fd 0 --passphrase "${1}" \ - --output "${2}" "${3}" \ - 2>/dev/null + --output "${2}" "${3}" 2>/dev/null } read_pass () { - # Reads a password. + # Read a password from safe. if [ ! -s ${safe} ] ; then - echo "Empty safe, no passwords!" - exit 3 + fail "No passwords found" else - echo "Enter password to unlock ${safe}." - get_pass ; echo - decrypt ${password} ${safe} + get_pass "Enter password to unlock ${safe}: " ; echo + decrypt ${password} ${safe} || fail "Decryption failed" fi } -create_id () { - # Creates a new Username/ID. - - read -p "Username/ID: " id - - read -p "Create random password? (y/n default: y) " rand_pass - if [ "${rand_pass}" == "n" ]; then - echo "Choose a password for '${id}'." - get_pass ; echo - user_pass=$password - else - user_pass=$(gen_pass) - fi -} - - -write_pass () { - # Writes a password to safe. - - echo "Enter password to unlock ${safe}." - get_pass ; echo - - # Create a temporary file to decrypt passwords to. - # TODO(any): can this be done without writing to disk? - tmp_secret=$(mktemp -q /tmp/pwd.sh.XXXXXX) - - # Decrypt safe, exclude specified ID in case of update/removal. - if [ -s ${safe} ] ; then - decrypt ${password} ${safe} | grep -v " ${id}" > ${tmp_secret} - fi - - # Append new password for ID, if one was provided. - if [ ! -z ${user_pass+x} ] ; then - echo "${user_pass} ${id}" >> ${tmp_secret} - fi - - # Encrypt plaintext to safe. - encrypt ${password} ${safe} ${tmp_secret} - - # Remove temporary plaintext. - ${del} ${del_opts} ${tmp_secret} - - echo "Updated password for '${id}' in ${safe}." - - unset id - unset password - unset user_pass -} - - gen_pass () { - # Generate a random password. + # Generate a password. - read -p "Password length? (min/avg/max default: max) " pass_length - if [ "$pass_length" == "min" ]; then - len=6 - elif [ "$pass_length" == "avg" ]; then - len=18 - else - len=36 + len=40 + read -p "Password length? (default: 40, max: 80) " length + + if [[ ${length} =~ ^[0-9]+$ ]] ; then + len=${length} fi - ${gpg} --gen-random -a 0 ${len} + ${gpg} --gen-random -a 0 80 | cut -c -${len} } -create_keys () { - # Create public and private GnuPG keys. +write_pass () { + # Write a password in safe. - echo "Choose a strong master password." - get_pass ; echo + # If no password provided, clear the entry by writing an empty line. + if [ -z ${userpass+x} ] ; then + new_entry=" " + else + new_entry="${userpass} ${username}" + fi - ${gpg} ${gpg_opts} \ - --gen-key --batch <( - cat </dev/null + get_pass "Enter password to unlock ${safe}: " ; echo - echo "Created keys: ${public} and ${secret}." - - unset password + # If safe exists, decrypt it and filter out username, or bail on error. + # If successful, append new entry, or blank line. + # Filter out any blank lines. + # Finally, encrypt it all to a new safe file, or fail. + # If successful, update to new safe file. + ( if [ -f ${safe} ] ; then + decrypt ${password} ${safe} | \ + grep -v -e " ${username}$" || return + fi ; \ + echo "${new_entry}") | \ + grep -v -e "^[[:space:]]*$" | \ + encrypt ${password} ${safe}.new - || fail "Write to safe failed" + mv ${safe}.new ${safe} } -create_safe () { - # Create an encrypted "safe" file to store passwords. +create_username () { + # Create a new username and password. - touch ${safe} ; chmod 0600 ${safe} - echo "Created encrypted safe file: ${safe}." + read -p "Username: " username + read -p "Generate password? (y/n, default: y) " rand_pass + if [ "${rand_pass}" == "n" ]; then + get_pass "Enter password for \"${username}\": " ; echo + userpass=$password + else + userpass=$(gen_pass) + fi } sanity_check () { - # Make sure all necessary programs are installed and files exist. + # Make sure required programs are installed and can be executed. if [[ -z ${gpg} && ! -x ${gpg} ]] ; then - echo "GnuPG is not available!" - exit 127 - fi - - if [[ -z ${del} && ! -x ${del} ]] ; then - echo "srm is not available!" - exit 127 - fi - - if [ ! -f ${secret} ] ; then - echo "No keys found, creating new keys ..." - create_keys - else - chmod 0600 ${secret} - fi - - if [ ! -f ${safe} ] ; then - echo "No safe found, creating new safe file ..." - create_safe - else - chmod 0600 ${safe} + fail "GnuPG is not available" fi } sanity_check -read -p "Read, write, or delete a password? (r/w/d default: r) " action +read -p "Read, write, or delete a password? (r/w/d, default: r) " action + if [ "${action}" == "w" ] ; then - create_id - write_pass + create_username && write_pass elif [ "${action}" == "d" ] ; then - read -p "Which Username/ID to delete? " id - write_pass + read -p "Username to delete? " username + write_pass else read_pass fi +tput setaf 2 ; echo "Done" +