Purse/purse.sh

237 lines
4.9 KiB
Bash
Raw Normal View History

2015-07-03 18:59:27 +00:00
#!/usr/bin/env bash
2015-07-02 02:03:55 +00:00
set -o errtrace
2015-07-02 02:03:55 +00:00
set -o nounset
set -o pipefail
2015-07-02 02:03:55 +00:00
2016-07-20 06:19:14 +00:00
filter="$(command -v grep) -v -E"
2015-10-31 04:08:51 +00:00
gpg="$(command -v gpg || command -v gpg2)"
2018-06-02 20:33:18 +00:00
safe="${PURSE_SAFE:=.purse}"
keyid="0xFF3E7D88647EBCDB"
2015-07-02 02:03:55 +00:00
fail () {
# Print an error message and exit.
2015-07-02 02:03:55 +00:00
printf "\n\n"
2016-05-18 19:04:16 +00:00
tput setaf 1 1 1 ; echo "Error: ${1}" ; tput sgr0
exit 1
}
2015-07-02 02:03:55 +00:00
get_pass () {
# Prompt for a password.
2015-07-02 02:03:55 +00:00
password=""
prompt="${1}"
while IFS= read -p "${prompt}" -r -s -n 1 char ; do
2015-07-03 13:07:54 +00:00
if [[ ${char} == $'\0' ]] ; then
break
2015-07-03 17:05:06 +00:00
elif [[ ${char} == $'\177' ]] ; then
if [[ -z "${password}" ]] ; then
prompt=""
2015-07-03 13:07:54 +00:00
else
prompt=$'\b \b'
password="${password%?}"
2015-07-02 02:03:55 +00:00
fi
2015-07-03 13:07:54 +00:00
else
2015-07-03 17:05:06 +00:00
prompt="*"
password+="${char}"
2015-07-03 13:07:54 +00:00
fi
2015-07-02 02:03:55 +00:00
done
}
decrypt () {
2018-06-02 20:33:18 +00:00
# Decrypt with authorized GPG key.
2015-07-02 02:03:55 +00:00
2018-06-02 20:33:18 +00:00
cat "${1}" | ${gpg} --armor --batch --decrypt 2>/dev/null
2015-07-02 02:03:55 +00:00
}
encrypt () {
2018-06-02 20:33:18 +00:00
# Encrypt to a recipient.
2015-07-02 02:03:55 +00:00
2018-06-02 20:33:18 +00:00
${gpg} --encrypt --armor --batch --yes --throw-keyids \
--recipient ${keyid} --output "${1}" "${2}"
2015-07-02 02:03:55 +00:00
}
read_pass () {
# Read a password from safe.
2015-07-02 02:03:55 +00:00
if [[ ! -s ${safe} ]] ; then fail "No password safe found" ; fi
2015-08-07 18:11:11 +00:00
if [[ -z "${2+x}" ]] ; then read -r -p "
Username (Enter for all): " username
else
2015-08-07 18:11:11 +00:00
username="${2}"
fi
if [[ -z "${username}" || "${username}" == "all" ]] ; then username="" ; fi
2015-07-03 04:16:37 +00:00
2018-06-02 20:33:18 +00:00
prompt_key
decrypt ${safe} | grep -F " ${username}" || fail "Decryption failed"
}
2018-06-02 20:33:18 +00:00
prompt_key () {
# Print a message when key touch is needed.
if [[ -f "${safe}" ]] ; then
printf "\n Touch key to decrypt safe ...\n\n"
fi
2015-07-02 02:03:55 +00:00
}
gen_pass () {
# Generate a password.
2015-07-02 02:03:55 +00:00
2018-06-02 20:33:18 +00:00
len=20
max=80
if [[ -z "${3+x}" ]] ; then read -p "
Password length (default: ${len}, max: ${max}): " length
else
length="${3}"
fi
2015-07-02 02:31:38 +00:00
if [[ ${length} =~ ^[0-9]+$ ]] ; then len=${length} ; fi
2015-07-03 04:29:12 +00:00
# base64: 4 characters for every 3 bytes
${gpg} --armor --gen-random 0 "$((${max} * 3/4))" | cut -c -"${len}"
2015-08-07 18:24:08 +00:00
}
2015-07-02 02:31:38 +00:00
write_pass () {
# Write a password in safe.
2015-07-02 02:03:55 +00:00
# If no password (delete action), clear the entry by writing an empty line.
2015-10-31 04:08:51 +00:00
if [[ -z "${userpass+x}" ]] ; then
entry=" "
else
2015-10-31 04:08:51 +00:00
entry="${userpass} ${username}"
2015-07-02 02:31:38 +00:00
fi
2018-06-02 20:33:18 +00:00
prompt_key
# If safe exists, decrypt it and filter out username, or bail on error.
2015-10-31 04:08:51 +00:00
# If successful, append entry, or blank line.
2016-07-20 06:19:14 +00:00
# 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.
2015-10-31 04:08:51 +00:00
( if [[ -f "${safe}" ]] ; then
2018-06-02 20:33:18 +00:00
decrypt ${safe} | ${filter} " ${username}$" || return
fi ; \
2015-10-31 04:08:51 +00:00
echo "${entry}") | \
2016-07-20 06:19:14 +00:00
(${filter} "^[[:space:]]*$|^mtime:[[:digit:]]+$";echo mtime:$(date +%s)) | \
2018-06-02 20:33:18 +00:00
encrypt ${safe}.new - || fail "Write to safe failed"
mv ${safe}{.new,}
2015-07-02 02:03:55 +00:00
}
new_entry () {
# Prompt for new username and/or password.
2015-07-02 02:03:55 +00:00
if [[ -z "${2+x}" ]] ; then read -r -p "
2015-07-03 21:03:26 +00:00
Username: " username
else
username="${2}"
fi
if [[ -z "${3+x}" ]] ; then get_pass "
Password for \"${username}\" (Enter to generate): "
userpass="${password}"
fi
2015-07-03 21:03:26 +00:00
2018-06-02 20:33:18 +00:00
printf "\n"
if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@")
if [[ -z "${4+x}" || ! "${4}" =~ ^([qQ])$ ]] ; then
2018-06-02 20:33:18 +00:00
printf "\n Password: ${userpass}\n"
fi
2015-07-02 02:03:55 +00:00
fi
}
print_help () {
# Print help text.
2015-07-02 02:03:55 +00:00
echo "
2018-06-02 20:33:18 +00:00
purse is a shell script to manage passwords with GnuPG asymmetric encryption.
It is recommended to be used with a Yubikey or other hardware token.
2015-07-02 02:03:55 +00:00
2018-06-02 20:33:18 +00:00
The script can run interactively as './purse.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:
2018-06-02 20:33:18 +00:00
./purse.sh r all
* Write a password for 'github':
2018-06-02 20:33:18 +00:00
./purse.sh w github
* Generate a 50 character password for 'github' and write:
2018-06-02 20:33:18 +00:00
./purse.sh w github 50
* To suppress the generated password:
2018-06-02 20:33:18 +00:00
./purse.sh w github 50 q
* Delete password for 'mail':
2018-06-02 20:33:18 +00:00
./purse.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.
2018-06-02 20:33:18 +00:00
To report a bug, visit https://github.com/drduh/purse"
2015-07-02 02:03:55 +00:00
}
if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi
2015-07-02 02:03:55 +00:00
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
2015-07-02 02:03:55 +00:00
printf "\n" ; tput setaf 2 2 2 ; echo "Done" ; tput sgr0