2015-07-03 18:59:27 +00:00
#!/usr/bin/env bash
2020-05-25 21:22:21 +00:00
# https://github.com/drduh/Purse/blob/master/purse.sh
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
2024-03-10 21:59:33 +00:00
#set -x # uncomment to debug
2019-11-28 23:18:48 +00:00
2019-01-31 01:50:11 +00:00
umask 077
2024-03-10 21:59:33 +00:00
cb_timeout = 10 # seconds to keep password on clipboard
daily_backup = "false" # if true, create daily archive on write
encrypt_index = "false" # if true, requires 2 touches to decrypt
pass_copy = "false" # if true, keep password on clipboard before write
pass_len = 14 # default password length
pass_chars = " [:alnum:]!@# $%^&*();:+= "
2020-05-25 21:22:21 +00:00
gpgconf = " ${ HOME } /.gnupg/gpg.conf "
2019-11-28 23:18:48 +00:00
backuptar = " ${ PURSE_BACKUP : =purse. $( hostname) . $( date +%F) .tar } "
safeix = " ${ PURSE_INDEX : =purse.index } "
safedir = " ${ PURSE_SAFE : =safe } "
2024-03-10 21:59:33 +00:00
now = " $( date +%s) "
copy = " $( command -v xclip || command -v pbcopy) "
gpg = " $( command -v gpg || command -v gpg2) "
script = " $( basename " ${ BASH_SOURCE } " ) "
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
2024-03-10 21:59:33 +00:00
tput setaf 1 ; printf "\nError: %s\n" " ${ 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 ( ) {
2019-11-28 23:18:48 +00:00
# Decrypt with GPG.
2015-07-02 02:03:55 +00:00
2024-03-10 21:59:33 +00:00
cat " ${ 1 } " | \
${ gpg } --armor --batch --decrypt 2>/dev/null
2015-07-02 02:03:55 +00:00
}
encrypt ( ) {
2020-05-25 21:22:21 +00:00
# Encrypt to a group of hidden recipients.
2015-07-02 02:03:55 +00:00
2018-06-02 20:33:18 +00:00
${ gpg } --encrypt --armor --batch --yes --throw-keyids \
2020-05-25 21:22:21 +00:00
--hidden-recipient "purse_keygroup" \
--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-11-28 23:18:48 +00:00
if [ [ ! -s ${ safeix } ] ] ; then fail " ${ safeix } not found " ; fi
2015-08-07 18:11:11 +00:00
2019-11-28 23:18:48 +00:00
username = ""
while [ [ -z " ${ username } " ] ] ; do
if [ [ -z " ${ 2 +x } " ] ] ; then read -r -p "
Username: " username
else username = " ${ 2 } " ; fi
done
2015-07-31 04:08:43 +00:00
2020-05-25 21:22:21 +00:00
if [ [ " ${ encrypt_index } " = "true" ] ] ; then
prompt_key "index"
2015-07-03 04:16:37 +00:00
2020-05-25 21:22:21 +00:00
spath = $( decrypt " ${ safeix } " | \
2024-03-10 21:59:33 +00:00
grep -F " ${ username } " | tail -1 | cut -d ":" -f2) || \
2020-05-25 21:22:21 +00:00
fail "Decryption failed"
else
spath = $( grep -F " ${ username } " " ${ safeix } " | \
2024-03-10 21:59:33 +00:00
tail -1 | cut -d ":" -f2)
2020-05-25 21:22:21 +00:00
fi
2018-06-02 20:33:18 +00:00
2019-11-28 23:18:48 +00:00
prompt_key "password"
2024-03-10 21:59:33 +00:00
if [ [ -s " ${ spath } " ] ] ; then
clip <( decrypt " ${ spath } " | head -1) || \
fail "Decryption failed"
else fail "Secret not available"
fi
2019-11-28 23:18:48 +00:00
}
2017-03-31 20:35:15 +00:00
2018-06-02 20:33:18 +00:00
prompt_key ( ) {
2019-11-28 23:18:48 +00:00
# Print a message if safe file exists.
2018-06-02 20:33:18 +00:00
2019-11-28 23:18:48 +00:00
if [ [ -f " ${ safeix } " ] ] ; then
2020-05-25 21:22:21 +00:00
printf "\n Touch key to access %s ...\n" " ${ 1 } "
2018-06-02 20:33:18 +00:00
fi
2015-07-02 02:03:55 +00:00
}
2015-07-03 02:56:16 +00:00
gen_pass ( ) {
2019-11-28 23:18:48 +00:00
# Generate a password using GPG.
2015-07-02 02:03:55 +00:00
2020-05-25 21:22:21 +00:00
if [ [ -z " ${ 3 +x } " ] ] ; then read -r -p "
2019-11-28 23:18:48 +00:00
2024-03-10 21:59:33 +00:00
Password length ( default: ${ pass_len } ) : " length
2019-11-28 23:18:48 +00:00
else length = " ${ 3 } " ; fi
2015-07-02 02:31:38 +00:00
2024-03-10 21:59:33 +00:00
if [ [ ${ length } = ~ ^[ 0-9] +$ ] ] ; then pass_len = ${ length } ; fi
2015-07-03 02:56:16 +00:00
2024-03-10 21:59:33 +00:00
LC_LANG = C tr -dc " ${ pass_chars } " < /dev/urandom | \
fold -w " ${ pass_len } " | head -1
2019-11-28 23:18:48 +00:00
}
2015-07-02 02:31:38 +00:00
write_pass ( ) {
2024-03-10 21:59:33 +00:00
# Write a password and update the index.
if [ [ " ${ pass_copy } " = "true" ] ] ; then
clip <( printf '%s' " ${ userpass } " )
fi
2015-07-02 02:03:55 +00:00
2024-03-10 21:59:33 +00:00
fpath = " $( LC_LANG = C tr -dc '[:lower:]' < /dev/urandom | fold -w10 | head -1) "
spath = " ${ safedir } / ${ fpath } "
2019-11-28 23:18:48 +00:00
printf '%s\n' " ${ userpass } " | \
2020-05-25 21:22:21 +00:00
encrypt " ${ spath } " - || \
2019-11-28 23:18:48 +00:00
fail " Failed to put ${ spath } "
2024-03-10 21:59:33 +00:00
userpass = ""
2019-11-28 23:18:48 +00:00
2020-05-25 21:22:21 +00:00
if [ [ " ${ encrypt_index } " = "true" ] ] ; then
prompt_key "index"
( if [ [ -f " ${ safeix } " ] ] ; then
decrypt " ${ safeix } " || return ; fi
printf "%s@%s:%s\n" " ${ username } " " ${ now } " " ${ spath } " ) | \
encrypt " ${ safeix } . ${ now } " - || \
fail " Failed to put ${ safeix } . ${ now } "
mv " ${ safeix } . ${ now } " " ${ safeix } "
else
printf "%s@%s:%s\n" " ${ username } " " ${ now } " " ${ spath } " >> " ${ safeix } "
fi
2015-07-02 02:03:55 +00:00
}
2019-11-28 23:18:48 +00:00
list_entry ( ) {
# Decrypt the index to list entries.
if [ [ ! -s ${ safeix } ] ] ; then fail " ${ safeix } not found " ; fi
2020-05-25 21:22:21 +00:00
if [ [ " ${ encrypt_index } " = "true" ] ] ; then
prompt_key "index"
decrypt " ${ safeix } " || fail "Decryption failed"
else
2024-03-10 21:59:33 +00:00
printf "\n"
2020-05-25 21:22:21 +00:00
cat " ${ safeix } "
fi
2019-11-28 23:18:48 +00:00
}
backup ( ) {
2024-03-10 21:59:33 +00:00
# Archive index, safe and configuration.
2019-11-28 23:18:48 +00:00
2024-03-10 21:59:33 +00:00
if [ [ -f " ${ safeix } " && -d " ${ safedir } " ] ] ; then
2020-05-25 21:22:21 +00:00
cp " ${ gpgconf } " " gpg.conf. ${ now } "
2024-03-10 21:59:33 +00:00
tar --create --file " ${ backuptar } " \
2020-05-25 21:58:56 +00:00
" ${ safeix } " " ${ safedir } " " gpg.conf. ${ now } " " ${ script } "
2020-05-25 21:22:21 +00:00
rm " gpg.conf. ${ now } "
2019-11-28 23:18:48 +00:00
else fail "Nothing to archive" ; fi
2020-05-25 21:22:21 +00:00
2024-03-10 21:59:33 +00:00
printf "\nArchived %s\n" " ${ backuptar } "
2019-11-28 23:18:48 +00:00
}
clip ( ) {
# Use clipboard and clear after timeout.
2020-05-25 21:22:21 +00:00
${ copy } < " ${ 1 } "
2019-11-28 23:18:48 +00:00
printf "\n"
shift
2024-03-10 21:59:33 +00:00
while [ $cb_timeout -gt 0 ] ; do
printf "\r\033[KPassword on clipboard! Clearing in %.d" $(( cb_timeout--))
2019-11-28 23:18:48 +00:00
sleep 1
done
2024-03-10 21:59:33 +00:00
printf "\n"
2019-11-28 23:18:48 +00:00
printf "" | ${ copy }
}
2015-07-02 02:03:55 +00:00
2020-05-25 21:22:21 +00:00
setup_keygroup( ) {
2024-03-10 21:59:33 +00:00
# Configure GPG keygroup.
2020-05-25 21:22:21 +00:00
purse_keygroup = "group purse_keygroup ="
keyid = ""
recommend = " $( ${ gpg } -K | grep "sec#" | \
awk -F "/" '{print $2}' | cut -c-18 | tr "\n" " " ) "
printf " \n Setting up GPG key group ...
Found key IDs: %s
Enter backup key IDs first, preferred key IDs last.
" " ${ recommend } "
while [ [ -z " ${ keyid } " ] ] ; do
read -r -p "
Key ID or Enter to continue : " keyid
if [ [ -z " ${ keyid } " ] ] ; then
printf "%s\n" " $purse_keygroup " >> " ${ gpgconf } "
break
fi
purse_keygroup = " ${ purse_keygroup } ${ keyid } "
keyid = ""
done
}
2017-03-31 20:35:15 +00:00
new_entry ( ) {
2024-03-10 21:59:33 +00:00
# Prompt for username and password.
2015-07-02 02:03:55 +00:00
2019-11-28 23:18:48 +00:00
username = ""
while [ [ -z " ${ username } " ] ] ; do
if [ [ -z " ${ 2 +x } " ] ] ; then read -r -p "
2015-07-03 21:03:26 +00:00
Username: " username
2019-11-28 23:18:48 +00:00
else username = " ${ 2 } " ; fi
done
2015-07-31 04:35:35 +00:00
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
2024-03-10 21:59:33 +00:00
if [ [ -z " ${ password } " ] ] ; then
userpass = $( gen_pass " $@ " )
fi
2015-07-02 02:03:55 +00:00
}
2017-03-31 20:35:15 +00:00
print_help ( ) {
# Print help text.
2015-07-02 02:03:55 +00:00
2019-11-28 23:18:48 +00:00
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.
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
Purse can be used interactively or by passing one of the following options:
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
* 'w' to write a password
* 'r' to read a password
* 'l' to list passwords
* 'b' to create an archive for backup
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
Example usage:
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
* Generate a 30 character password for 'userName' :
./purse.sh w userName 30
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
* Copy the password for 'userName' to clipboard:
./purse.sh r userName
2017-03-31 20:35:15 +00:00
2024-03-10 21:59:33 +00:00
* List stored passwords and copy a specific version:
2019-11-28 23:18:48 +00:00
./purse.sh l
./purse.sh r userName@1574723625
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
* Create an archive for backup:
./purse.sh b
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
* Restore an archive from backup:
tar xvf purse*tar"" "
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
2020-05-25 21:22:21 +00:00
if [ [ -z ${ copy } && ! -x ${ copy } ] ] ; then fail "Clipboard is not available" ; fi
2024-03-10 21:59:33 +00:00
if [ [ ! -f ${ gpgconf } ] ] ; then fail "GnuPG config is not available" ; fi
if [ [ ! -d " ${ safedir } " ] ] ; then mkdir -p " ${ safedir } " ; fi
2019-11-28 23:18:48 +00:00
2024-03-10 21:59:33 +00:00
chmod -R 0600 " ${ safeix } " 2>/dev/null
chmod -R 0700 " ${ safedir } " 2>/dev/null
2017-03-31 20:35:15 +00:00
2019-11-28 23:18:48 +00:00
password = ""
2017-03-31 20:35:15 +00:00
action = ""
2019-11-28 23:18:48 +00:00
if [ [ -n " ${ 1 +x } " ] ] ; then action = " ${ 1 } " ; fi
2015-07-03 02:56:16 +00:00
2019-11-28 23:18:48 +00:00
while [ [ -z " ${ action } " ] ] ; do
2024-03-10 21:59:33 +00:00
read -r -n 1 -p "
2019-11-28 23:18:48 +00:00
Read or Write ( or Help for more options) : " action
2017-03-31 20:35:15 +00:00
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
2019-11-28 23:18:48 +00:00
elif [ [ " ${ action } " = ~ ^( [ bB] ) $ ] ] ; then
backup
elif [ [ " ${ action } " = ~ ^( [ lL] ) $ ] ] ; then
list_entry
2017-03-31 20:35:15 +00:00
elif [ [ " ${ action } " = ~ ^( [ wW] ) $ ] ] ; then
2020-05-25 21:22:21 +00:00
purse_keygroup = $( grep "group purse_keygroup" " ${ gpgconf } " )
if [ [ -z " ${ purse_keygroup } " ] ] ; then
setup_keygroup
fi
printf "\n %s\n" " ${ purse_keygroup } "
2017-03-31 20:35:15 +00:00
new_entry " $@ "
write_pass
2015-07-02 02:03:55 +00:00
2024-03-10 21:59:33 +00:00
if [ [ " ${ daily_backup } " = "true" ] ] ; then
if [ [ ! -f ${ backuptar } ] ] ; then
backup
fi
fi
2019-11-28 23:18:48 +00:00
else read_pass " $@ " ; fi
2024-03-10 21:59:33 +00:00
chmod -R 0400 " ${ safeix } " " ${ safedir } " 2>/dev/null
2019-11-28 23:18:48 +00:00
2024-03-10 21:59:33 +00:00
tput setaf 2 ; printf "\nDone\n" ; tput sgr0