Permalink
| #!/usr/bin/env bash | |
| set -o errtrace | |
| set -o nounset | |
| set -o pipefail | |
| filter="$(command -v grep) -v -E" | |
| gpg="$(command -v gpg || command -v gpg2)" | |
| safe="${PWDSH_SAFE:=pwd.sh.safe}" | |
| fail () { | |
| # Print an error message and exit. | |
| printf "\n\n" | |
| tput setaf 1 1 1 ; echo "Error: ${1}" ; tput sgr0 | |
| exit 1 | |
| } | |
| get_pass () { | |
| # Prompt for a password. | |
| password="" | |
| prompt="${1}" | |
| while IFS= read -p "${prompt}" -r -s -n 1 char ; do | |
| if [[ ${char} == $'\0' ]] ; then | |
| break | |
| elif [[ ${char} == $'\177' ]] ; then | |
| if [[ -z "${password}" ]] ; then | |
| prompt="" | |
| else | |
| prompt=$'\b \b' | |
| password="${password%?}" | |
| fi | |
| else | |
| prompt="*" | |
| password+="${char}" | |
| fi | |
| done | |
| } | |
| decrypt () { | |
| # Decrypt with a password. | |
| echo "${1}" | ${gpg} --armor --batch \ | |
| --decrypt --passphrase-fd 0 "${2}" 2>/dev/null | |
| } | |
| encrypt () { | |
| # Encrypt with a password. | |
| ${gpg} --armor --batch \ | |
| --symmetric --yes --passphrase-fd 3 \ | |
| --output "${2}" "${3}" 3< <(echo "${1}") | |
| } | |
| read_pass () { | |
| # Read a password from safe. | |
| if [[ ! -s ${safe} ]] ; then fail "No password safe found" ; fi | |
| if [[ -z "${2+x}" ]] ; then read -r -p " | |
| Username (Enter for all): " username | |
| else | |
| username="${2}" | |
| fi | |
| if [[ -z "${username}" || "${username}" == "all" ]] ; then username="" ; fi | |
| while [[ -z "${password}" ]] ; do get_pass " | |
| Password to unlock ${safe}: " ; done | |
| printf "\n\n" | |
| decrypt ${password} ${safe} | grep -F " ${username}" \ | |
| || fail "Decryption failed" | |
| } | |
| gen_pass () { | |
| # Generate a password. | |
| len=50 | |
| max=100 | |
| if [[ -z "${3+x}" ]] ; then read -p " | |
| Password length (default: ${len}, max: ${max}): " length | |
| 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 | |
| get_pass " | |
| Password to unlock ${safe}: " ; echo | |
| # 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 ${password} ${safe} | \ | |
| ${filter} " ${username}$" || return | |
| fi ; \ | |
| echo "${entry}") | \ | |
| (${filter} "^[[:space:]]*$|^mtime:[[:digit:]]+$";echo mtime:$(date +%s)) | \ | |
| encrypt ${password} ${safe}.new - || fail "Write to safe failed" | |
| mv ${safe}{.new,} | |
| } | |
| new_entry () { | |
| # Prompt for new username and/or password. | |
| if [[ -z "${2+x}" ]] ; then read -r -p " | |
| Username: " username | |
| else | |
| username="${2}" | |
| fi | |
| if [[ -z "${3+x}" ]] ; then get_pass " | |
| Password for \"${username}\" (Enter to generate): " | |
| userpass="${password}" | |
| fi | |
| if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@") | |
| if [[ -z "${4+x}" || ! "${4}" =~ ^([qQ])$ ]] ; then | |
| echo " | |
| Password: ${userpass}" | |
| fi | |
| fi | |
| } | |
| print_help () { | |
| # Print help text. | |
| echo " | |
| pwd.sh is a shell script to manage passwords with GnuPG symmetric encryption. | |
| The script can be run interactively as './pwd.sh' or with the following args: | |
| * 'r' to read a password | |
| * 'w' to write a password | |
| * 'd' to delete a password | |
| * 'h' to see this help text | |
| A username can be supplied as an additional argument or 'all' for all entries. | |
| For writing, a password length can be appended. Append 'q' to suppress output. | |
| Examples: | |
| * Read all passwords: | |
| ./pwd.sh r all | |
| * Write a password for 'github': | |
| ./pwd.sh w github | |
| * Generate a 50 character password for 'github' and write: | |
| ./pwd.sh w github 50 | |
| * To suppress the generated password: | |
| ./pwd.sh w github 50 q | |
| * Delete password for 'mail': | |
| ./pwd.sh d mail | |
| A password cannot be supplied as an argument, nor is used as one throughout | |
| the script, to prevent it from appearing in process listing or logs. | |
| To report a bug, visit https://github.com/drduh/pwd.sh | |
| " | |
| } | |
| if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi | |
| password="" | |
| action="" | |
| if [[ -n "${1+x}" ]] ; then | |
| action="${1}" | |
| fi | |
| while [[ -z "${action}" ]] ; | |
| do read -n 1 -p " | |
| Read, Write, or Delete password (or Help): " action | |
| printf "\n" | |
| done | |
| if [[ "${action}" =~ ^([hH])$ ]] ; then | |
| print_help | |
| 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 |