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-11-28 23:18:48 +00:00
#set -x # uncomment to debug
2019-01-31 01:50:11 +00:00
umask 077
2019-11-28 23:18:48 +00:00
now = $( date +%s)
copy = " $( command -v xclip || command -v pbcopy) "
2015-10-31 04:08:51 +00:00
gpg = " $( command -v gpg || command -v gpg2) "
2019-11-28 23:18:48 +00:00
backuptar = " ${ PURSE_BACKUP : =purse. $( hostname) . $( date +%F) .tar } "
keyid = " ${ PURSE_KEYID : =0xFF3E7D88647EBCDB } "
safeix = " ${ PURSE_INDEX : =purse.index } "
safedir = " ${ PURSE_SAFE : =safe } "
timeout = 9
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
2019-11-28 23:18:48 +00:00
tput setaf 1 1 1 ; printf " \nError: ${ 1 } \n " ; 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
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-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
2019-11-28 23:18:48 +00:00
prompt_key "index"
2015-07-03 04:16:37 +00:00
2019-11-28 23:18:48 +00:00
spath = $( decrypt ${ safeix } | \
grep -F " ${ username } " | tail -n1 | cut -d : -f2) || \
fail "Decryption failed"
2018-06-02 20:33:18 +00:00
2019-11-28 23:18:48 +00:00
prompt_key "password"
clip <( decrypt ${ spath } | head -n1) || \
fail "Decryption failed"
}
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
printf " \n Touch key to access ${ 1 } ...\n\n "
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
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 "
2019-11-28 23:18:48 +00:00
2017-03-31 20:35:15 +00:00
Password length ( default: ${ len } , max: ${ max } ) : " length
2019-11-28 23:18:48 +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 } "
2019-11-28 23:18:48 +00:00
}
2015-07-02 02:31:38 +00:00
write_pass ( ) {
2019-11-28 23:18:48 +00:00
# Write a password and update index file.
2015-07-02 02:03:55 +00:00
2019-11-28 23:18:48 +00:00
fpath = $( tr -dc "[:lower:]" < /dev/urandom | fold -w8 | head -n1)
spath = ${ safedir } /${ fpath }
printf '%s\n' " ${ userpass } " | \
encrypt ${ spath } - || \
fail " Failed to put ${ spath } "
prompt_key "index"
( if [ [ -f " ${ safeix } " ] ] ; then
decrypt ${ safeix } || return ; fi
printf " ${ username } @ ${ now } : ${ spath } \n " ) | \
encrypt ${ safeix } .${ now } - || \
fail " Failed to put ${ safeix } . ${ now } "
2015-07-02 02:31:38 +00:00
2019-11-28 23:18:48 +00:00
mv ${ safeix } { .${ now } ,}
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
prompt_key "index"
decrypt ${ safeix } || fail "Decryption failed"
}
backup ( ) {
# Archive encrypted index and safe directory.
if [ [ -f ${ safeix } && -d ${ safedir } ] ] ; then \
tar cfv ${ backuptar } ${ safeix } ${ safedir }
else fail "Nothing to archive" ; fi
printf " \nArchived ${ backuptar } \n " ; \
}
clip ( ) {
# Use clipboard and clear after timeout.
${ copy } < ${ 1 }
printf "\n"
shift
while [ $timeout -gt 0 ] ; do
printf "\r\033[KPassword on clipboard! Clearing in %.d" $(( timeout--))
sleep 1
done
printf "" | ${ copy }
}
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
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
2019-11-28 23:18:48 +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
2019-11-28 23:18:48 +00:00
* List stored passwords and copy a previous version:
./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
2019-11-28 23:18:48 +00:00
if [ [ ! -d ${ safedir } ] ] ; then mkdir -p ${ safedir } ; fi
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
read -n 1 -p "
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
new_entry " $@ "
write_pass
2015-07-02 02:03:55 +00:00
2019-11-28 23:18:48 +00:00
else read_pass " $@ " ; fi
chmod -R 0400 ${ safeix } ${ safedir } 2>/dev/null
tput setaf 2 2 2 ; printf "\nDone\n" ; tput sgr0