2015-07-03 18:59:27 +00:00
#!/usr/bin/env bash
2019-01-31 01:50:11 +00:00
# https://github.com/drduh/Purse
2015-07-02 02:03:55 +00:00
2015-07-03 02:56:16 +00:00
set -o errtrace
2015-07-02 02:03:55 +00:00
set -o nounset
2015-07-03 02:56:16 +00:00
set -o pipefail
2015-07-02 02:03:55 +00:00
2019-01-31 01:50:11 +00:00
umask 077
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) "
2019-01-31 01:50:11 +00:00
safe = " ${ PURSE_SAFE : =purse.enc } "
2018-06-02 20:33:18 +00:00
keyid = "0xFF3E7D88647EBCDB"
2015-07-02 02:03:55 +00:00
2015-07-03 02:56:16 +00:00
fail ( ) {
# Print an error message and exit.
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
printf "\n\n"
2016-05-18 19:04:16 +00:00
tput setaf 1 1 1 ; echo " Error: ${ 1 } " ; tput sgr0
2015-07-03 02:56:16 +00:00
exit 1
}
2015-07-02 02:03:55 +00:00
get_pass ( ) {
2015-07-03 02:56:16 +00:00
# Prompt for a password.
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
password = ""
2015-07-03 02:56:16 +00:00
prompt = " ${ 1 } "
2015-07-31 04:53:28 +00:00
2015-07-03 02:56:16 +00:00
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 = "*"
2015-07-03 02:56:16 +00:00
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 ( ) {
2015-07-03 02:56:16 +00:00
# Read a password from safe.
2015-07-02 02:03:55 +00:00
2019-01-31 01:50:11 +00:00
if [ [ ! -s ${ safe } ] ] ; then fail " ${ safe } not found " ; fi
2015-08-07 18:11:11 +00:00
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ 2 +x } " ] ] ; then read -r -p "
Username ( Enter for all) : " username
2015-07-31 04:08:43 +00:00
else
2015-08-07 18:11:11 +00:00
username = " ${ 2 } "
2015-07-31 04:08:43 +00:00
fi
2017-03-31 20:35:15 +00:00
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"
}
2017-03-31 20:35:15 +00:00
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
}
2015-07-03 02:56:16 +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
2015-07-31 04:35:35 +00:00
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ 3 +x } " ] ] ; then read -p "
Password length ( default: ${ len } , max: ${ max } ) : " length
2015-07-31 04:35:35 +00:00
else
length = " ${ 3 } "
fi
2015-07-02 02:31:38 +00:00
2017-03-31 20:35:15 +00:00
if [ [ ${ length } = ~ ^[ 0-9] +$ ] ] ; then len = ${ length } ; fi
2015-07-03 02:56:16 +00:00
2015-07-03 04:29:12 +00:00
# base64: 4 characters for every 3 bytes
2017-03-31 20:35:15 +00:00
${ 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 ( ) {
2015-07-03 02:56:16 +00:00
# Write a password in safe.
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +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 = " "
2015-07-03 02:56:16 +00:00
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
2015-07-03 02:56:16 +00:00
# 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.
2015-07-03 02:56:16 +00:00
# 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
2015-07-03 02:56:16 +00:00
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"
2017-03-31 20:35:15 +00:00
mv ${ safe } { .new,}
2015-07-02 02:03:55 +00:00
}
2017-03-31 20:35:15 +00:00
new_entry ( ) {
# Prompt for new username and/or password.
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ 2 +x } " ] ] ; then read -r -p "
2015-07-03 21:03:26 +00:00
Username: " username
2015-07-31 04:35:35 +00:00
else
2015-07-31 04:53:28 +00:00
username = " ${ 2 } "
2015-07-31 04:35:35 +00:00
fi
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ 3 +x } " ] ] ; then get_pass "
Password for \" ${ username } \" ( Enter to generate) : "
userpass = " ${ password } "
2015-07-31 04:08:43 +00:00
fi
2015-07-03 21:03:26 +00:00
2018-06-02 20:33:18 +00:00
printf "\n"
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ password } " ] ] ; then userpass = $( gen_pass " $@ " )
2015-08-07 18:20:21 +00:00
if [ [ -z " ${ 4 +x } " || ! " ${ 4 } " = ~ ^( [ qQ] ) $ ] ] ; then
2018-06-02 20:33:18 +00:00
printf " \n Password: ${ userpass } \n "
2015-08-07 18:20:21 +00:00
fi
2015-07-02 02:03:55 +00:00
fi
}
2017-03-31 20:35:15 +00:00
print_help ( ) {
# Print help text.
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
echo "
2019-01-31 01:50:11 +00:00
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:
2017-03-31 20:35:15 +00:00
* 'r' to read a password
* 'w' to write a password
* 'd' to delete a password
2019-01-31 01:50:11 +00:00
* 'h' to print this help text
2017-03-31 20:35:15 +00:00
2019-01-31 01:50:11 +00:00
A username, password length and 'q' options can also be used.
2017-03-31 20:35:15 +00:00
Examples:
* Read all passwords:
2018-06-02 20:33:18 +00:00
./purse.sh r all
2017-03-31 20:35:15 +00:00
* Write a password for 'github' :
2018-06-02 20:33:18 +00:00
./purse.sh w github
2017-03-31 20:35:15 +00:00
* Generate a 50 character password for 'github' and write:
2018-06-02 20:33:18 +00:00
./purse.sh w github 50
2017-03-31 20:35:15 +00:00
2019-01-31 01:50:11 +00:00
* Generate a password and write without displaying it:
2017-03-31 20:35:15 +00:00
2018-06-02 20:33:18 +00:00
./purse.sh w github 50 q
2017-03-31 20:35:15 +00:00
* Delete password for 'mail' :
2018-06-02 20:33:18 +00:00
./purse.sh d mail
2017-03-31 20:35:15 +00:00
2019-01-31 01:50:11 +00:00
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."
2015-07-02 02:03:55 +00:00
}
2017-03-31 20:35:15 +00:00
if [ [ -z ${ gpg } && ! -x ${ gpg } ] ] ; then fail "GnuPG is not available" ; fi
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
password = ""
action = ""
if [ [ -n " ${ 1 +x } " ] ] ; then
2015-07-31 04:53:28 +00:00
action = " ${ 1 } "
2017-03-31 20:35:15 +00:00
fi
2015-07-03 02:56:16 +00:00
2017-03-31 20:35:15 +00:00
while [ [ -z " ${ action } " ] ] ;
do read -n 1 -p "
Read, Write, or Delete password ( or Help) : " action
printf "\n"
done
2015-07-31 04:08:43 +00:00
2017-03-31 20:35:15 +00:00
if [ [ " ${ action } " = ~ ^( [ hH] ) $ ] ] ; then
print_help
elif [ [ " ${ action } " = ~ ^( [ wW] ) $ ] ] ; then
new_entry " $@ "
write_pass
2015-07-08 08:19:04 +00:00
elif [ [ " ${ action } " = ~ ^( [ dD] ) $ ] ] ; then
2017-03-31 20:35:15 +00:00
if [ [ -z " ${ 2 +x } " ] ] ; then read -p "
Username: " username
2015-07-31 04:08:43 +00:00
else
2015-07-31 04:53:28 +00:00
username = " ${ 2 } "
2015-07-31 04:08:43 +00:00
fi
write_pass
2015-07-02 21:47:32 +00:00
else
2015-07-31 04:08:43 +00:00
read_pass " $@ "
2015-07-02 21:47:32 +00:00
fi
2015-07-02 02:03:55 +00:00
2017-03-31 20:35:15 +00:00
printf "\n" ; tput setaf 2 2 2 ; echo "Done" ; tput sgr0