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
2024-03-26 21:18:57 +00:00
#set -x # uncomment to debug
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
2019-01-31 01:50:11 +00:00
umask 077
2024-03-26 21:18:57 +00:00
export LC_ALL = "C"
2024-03-10 21:59:33 +00:00
now = " $( date +%s) "
2024-03-26 21:18:57 +00:00
today = " $( date +%F) "
2024-03-10 21:59:33 +00:00
gpg = " $( command -v gpg || command -v gpg2) "
2024-07-04 22:06:17 +00:00
gpg_conf = " ${ HOME } /.gnupg/gpg.conf "
2024-03-26 21:18:57 +00:00
2024-07-04 23:28:27 +00:00
clip = " ${ PURSE_CLIP : =xclip } " # clipboard, 'pbcopy' on macOS
clip_args = ${ PURSE_CLIP_ARGS : = } # args to pass to copy command
2024-03-26 21:18:57 +00:00
clip_dest = " ${ PURSE_DEST : =clipboard } " # set to 'screen' to print to stdout
clip_timeout = " ${ PURSE_TIME : =10 } " # seconds to clear clipboard/screen
comment = " ${ PURSE_COMMENT : = } " # *unencrypted* comment in files
daily_backup = " ${ PURSE_DAILY : = } " # daily backup archive on write
2024-03-26 23:56:11 +00:00
encrypt_index = " ${ PURSE_ENCIX : = } " # also keep index encrypted
2024-03-26 21:18:57 +00:00
pass_copy = " ${ PURSE_COPY : = } " # copy password before write
2024-03-26 23:36:25 +00:00
pass_echo = " ${ PURSE_ECHO : =* } " # show "*" when typing passwords
2024-03-26 21:52:56 +00:00
pass_len = " ${ PURSE_LEN : =14 } " # default password length
2024-03-26 21:18:57 +00:00
safe_dir = " ${ PURSE_SAFE : =safe } " # safe directory name
safe_ix = " ${ PURSE_INDEX : =purse.index } " # index file name
safe_backup = " ${ PURSE_BACKUP : =purse. $( hostname) . ${ today } .tar } "
2024-03-26 23:36:25 +00:00
pass_chars = " ${ PURSE_CHARS : = '[:alnum:]!?@#$%^&*();:+=' } "
2024-03-26 21:18:57 +00:00
trap cleanup EXIT INT TERM
cleanup ( ) {
2024-03-26 21:52:56 +00:00
# "Lock" files on trapped exits.
2024-03-26 21:18:57 +00:00
ret = $?
2024-06-30 21:39:57 +00:00
chmod -R 0000 " ${ safe_dir } " " ${ safe_ix } " 2>/dev/null
2024-03-26 21:18:57 +00:00
exit ${ ret }
}
2015-07-02 02:03:55 +00:00
2015-07-03 02:56:16 +00:00
fail ( ) {
2024-03-26 21:18:57 +00:00
# Print an error in red and exit.
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +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
2024-03-26 21:18:57 +00:00
warn ( ) {
# Print a warning in yellow.
tput setaf 3 ; printf "\nWARNING: %s\n" " ${ 1 } " ; tput sgr0
}
2024-06-30 21:39:57 +00:00
setup_keygroup( ) {
# Configure one or more recipients.
purse_keygroup = "group purse_keygroup ="
keyid = ""
recommend = " $( ${ gpg } -K | grep "sec#" | \
awk -F "/" '{print $2}' | cut -c-18 | tr "\n" " " ) "
printf " \n Setting up keygroup ...\n
Found recommended key IDs: %s\n
Enter one or more key IDs, preferred one last\n " " ${ recommend } "
while [ [ -z " ${ keyid } " ] ] ; do read -r -p "
Key ID or Enter to continue : " keyid
if [ [ -z " ${ keyid } " ] ] ; then
printf "%s\n" " ${ purse_keygroup } " >> " ${ gpg_conf } "
break
fi
purse_keygroup = " ${ purse_keygroup } ${ keyid } "
keyid = ""
done
}
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
2024-06-30 21:39:57 +00:00
password = ""
2024-03-26 21:18:57 +00:00
prompt = " ${ 1 } "
printf "\n"
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
2024-03-26 21:52:56 +00:00
if [ [ ${ char } = = $'\0' ] ] ; then break
2015-07-03 17:05:06 +00:00
elif [ [ ${ char } = = $'\177' ] ] ; then
2024-03-26 21:52:56 +00:00
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
2024-03-26 23:36:25 +00:00
prompt = " ${ pass_echo } "
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
2024-03-26 21:18:57 +00:00
${ gpg } --encrypt --armor --batch --yes \
2020-05-25 21:22:21 +00:00
--hidden-recipient "purse_keygroup" \
2024-03-26 21:18:57 +00:00
--throw-keyids --comment " ${ comment } " \
--output " ${ 1 } " " ${ 2 } " 2>/dev/null
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
2024-03-26 21:18:57 +00:00
if [ [ ! -s " ${ safe_ix } " ] ] ; then fail " ${ safe_ix } not found " ; fi
2015-08-07 18:11:11 +00:00
2019-11-28 23:18:48 +00:00
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
2024-03-26 21:18:57 +00:00
if [ [ -n " ${ encrypt_index } " ] ] ; then prompt_key "index"
spath = $( decrypt " ${ safe_ix } " | \
2024-03-10 21:59:33 +00:00
grep -F " ${ username } " | tail -1 | cut -d ":" -f2) || \
2024-03-26 21:18:57 +00:00
fail "Secret not available"
else spath = $( grep -F " ${ username } " " ${ safe_ix } " | \
tail -1 | cut -d ":" -f2)
2020-05-25 21:22:21 +00:00
fi
2018-06-02 20:33:18 +00:00
2024-06-30 21:39:57 +00:00
if [ [ ! -s " ${ spath } " ] ] ; then
fail "Secret not available" ; fi
2019-11-28 23:18:48 +00:00
prompt_key "password"
2024-07-04 22:59:36 +00:00
emit_pass <( decrypt " ${ spath } " | head -1) || \
2024-06-30 21:39:57 +00:00
fail " Failed to decrypt ${ spath } "
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
2024-03-26 21:18:57 +00:00
if [ [ -f " ${ safe_ix } " ] ] ; then
2024-06-30 21:39:57 +00:00
printf "\n Touch key to access %s ...\n" " ${ 1 } " ; fi
2015-07-02 02:03:55 +00:00
}
2024-06-30 23:16:26 +00:00
generate_pass ( ) {
2024-03-26 21:18:57 +00:00
# Generate a password from urandom.
2015-07-02 02:03:55 +00:00
2020-05-25 21:22:21 +00:00
if [ [ -z " ${ 3 +x } " ] ] ; then read -r -p "
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-26 21:52:56 +00:00
if [ [ " ${ length } " = ~ ^[ 0-9] +$ ] ] ; then
2024-07-04 22:59:36 +00:00
pass_len = " ${ length } "
fi
2015-07-03 02:56:16 +00:00
2024-03-26 21:18:57 +00:00
tr -dc " ${ pass_chars } " < /dev/urandom | \
2024-03-10 21:59:33 +00:00
fold -w " ${ pass_len } " | head -1
2019-11-28 23:18:48 +00:00
}
2015-07-02 02:31:38 +00:00
2024-06-30 23:16:26 +00:00
generate_user ( ) {
2024-06-30 21:39:57 +00:00
# Generate a username.
printf "%s%s\n" \
" $( awk 'length > 2 && length < 12 {print(tolower($0))}' \
/usr/share/dict/words | grep -v "'" | sort -R | head -n2 | \
tr "\n" "_" | iconv -f utf-8 -t ascii//TRANSLIT) " \
" $( tr -dc "[:digit:]" < /dev/urandom | fold -w 4 | head -1) "
}
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.
2024-03-26 21:18:57 +00:00
spath = " ${ safe_dir } / $( tr -dc "[:lower:]" < /dev/urandom | \
fold -w10 | head -1) "
if [ [ -n " ${ pass_copy } " ] ] ; then
2024-07-04 22:59:36 +00:00
emit_pass <( printf '%s' " ${ userpass } " ) ; fi
2015-07-02 02:03:55 +00:00
2024-06-30 21:39:57 +00:00
printf '%s\n' " ${ userpass } " | encrypt " ${ spath } " - || \
2024-03-26 21:18:57 +00:00
fail " Failed saving ${ spath } "
2019-11-28 23:18:48 +00:00
2024-03-26 21:18:57 +00:00
if [ [ -n " ${ encrypt_index } " ] ] ; then
2020-05-25 21:22:21 +00:00
prompt_key "index"
2024-03-26 21:18:57 +00:00
( if [ [ -f " ${ safe_ix } " ] ] ; then
decrypt " ${ safe_ix } " || return ; fi
2020-05-25 21:22:21 +00:00
printf "%s@%s:%s\n" " ${ username } " " ${ now } " " ${ spath } " ) | \
2024-03-26 21:18:57 +00:00
encrypt " ${ safe_ix } . ${ now } " - && \
mv " ${ safe_ix } . ${ now } " " ${ safe_ix } " || \
fail " Failed saving ${ safe_ix } . ${ now } "
2020-05-25 21:22:21 +00:00
else
2024-03-26 21:52:56 +00:00
printf "%s@%s:%s\n" \
" ${ username } " " ${ now } " " ${ spath } " >> " ${ safe_ix } "
2020-05-25 21:22:21 +00:00
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.
2024-03-26 21:18:57 +00:00
if [ [ ! -s " ${ safe_ix } " ] ] ; then fail " ${ safe_ix } not found " ; fi
2019-11-28 23:18:48 +00:00
2024-03-26 21:18:57 +00:00
if [ [ -n " ${ encrypt_index } " ] ] ; then prompt_key "index"
decrypt " ${ safe_ix } " || fail " ${ safe_ix } not available "
else printf "\n" ; cat " ${ safe_ix } "
2020-05-25 21:22:21 +00:00
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-07-04 23:28:27 +00:00
if [ [ ! -f " ${ safe_backup } " ] ] ; then
2024-03-26 21:32:02 +00:00
if [ [ -f " ${ safe_ix } " && -d " ${ safe_dir } " ] ] ; then
cp " ${ gpg_conf } " " gpg.conf. ${ today } "
2024-03-26 21:52:56 +00:00
tar cf " ${ safe_backup } " " ${ safe_dir } " " ${ safe_ix } " \
2024-03-26 21:32:02 +00:00
" ${ BASH_SOURCE } " " gpg.conf. ${ today } " && \
printf "\nArchived %s\n" " ${ safe_backup } "
rm -f " gpg.conf. ${ today } "
else fail "Nothing to archive" ; fi
else warn " ${ safe_backup } exists, skipping archive " ; fi
2019-11-28 23:18:48 +00:00
}
2024-07-04 22:59:36 +00:00
emit_pass ( ) {
2024-03-26 21:18:57 +00:00
# Use clipboard or stdout and clear after timeout.
2019-11-28 23:18:48 +00:00
2024-03-26 21:18:57 +00:00
if [ [ " ${ clip_dest } " = "screen" ] ] ; then
printf '\n%s\n' " $( cat ${ 1 } ) "
2024-06-30 23:16:26 +00:00
else ${ clip } < " ${ 1 } " ; fi
2019-11-28 23:18:48 +00:00
printf "\n"
2024-03-26 21:32:02 +00:00
while [ [ " ${ clip_timeout } " -gt 0 ] ] ; do
2024-03-26 21:18:57 +00:00
printf "\r\033[K Password on %s! Clearing in %.d" \
" ${ clip_dest } " " $(( clip_timeout--)) " ; sleep 1
2019-11-28 23:18:48 +00:00
done
2024-03-26 21:18:57 +00:00
printf "\r\033[K Clearing password from %s ..." " ${ clip_dest } "
2019-11-28 23:18:48 +00:00
2024-03-26 21:32:02 +00:00
if [ [ " ${ clip_dest } " = "screen" ] ] ; then clear
2024-06-30 23:16:26 +00:00
else printf "\n" ; printf "" | ${ clip } ; fi
2019-11-28 23:18:48 +00:00
}
2015-07-02 02:03:55 +00:00
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
2024-06-30 21:39:57 +00:00
if [ [ -z " ${ 2 +x } " ] ] ; then read -r -p "
Username ( Enter to generate) : " username
else username = " ${ 2 } " ; fi
if [ [ -z " ${ username } " ] ] ; then
2024-07-04 23:28:27 +00:00
username = $( generate_user " $@ " ) ; fi
2015-07-31 04:35:35 +00:00
2024-03-26 21:18:57 +00:00
if [ [ -z " ${ 3 +x } " ] ] ; then
get_pass " Password for \" ${ username } \" (Enter to generate): "
2024-07-04 23:28:27 +00:00
userpass = " ${ password } " ; fi
2015-07-03 21:03:26 +00:00
2024-03-26 21:18:57 +00:00
printf "\n"
2024-03-10 21:59:33 +00:00
if [ [ -z " ${ password } " ] ] ; then
2024-07-04 23:28:27 +00:00
userpass = $( generate_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
2024-03-26 21:52:56 +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.\n
2024-03-26 21:18:57 +00:00
Purse can be used interactively or by passing one of the following options:\n
2019-11-28 23:18:48 +00:00
* 'w' to write a password
* 'r' to read a password
* 'l' to list passwords
2024-03-26 21:18:57 +00:00
* 'b' to create an archive for backup\n
Options can also be passed on the command line.\n
* Create a 20-character password for userName:
./purse.sh w userName 20\n
* Read password for userName:
./purse.sh r userName\n
* Passwords are stored with an epoch timestamp for revision control. The most recent version is copied to clipboard on read. To list all passwords or read a specific version of a password:
./purse.sh l
./purse.sh r userName@1574723625\n
* Create an archive for backup:
./purse.sh b\n
* Restore an archive from backup:
tar xvf purse*tar\n "" "
2015-07-02 02:03:55 +00:00
}
2024-03-26 21:18:57 +00:00
if [ [ -z " ${ gpg } " || ! -x " ${ gpg } " ] ] ; then fail "GnuPG is not available" ; fi
2015-07-02 02:03:55 +00:00
2024-03-26 21:18:57 +00:00
if [ [ ! -f " ${ gpg_conf } " ] ] ; then fail "GnuPG config is not available" ; fi
2020-05-25 21:22:21 +00:00
2024-03-26 21:18:57 +00:00
if [ [ ! -d " ${ safe_dir } " ] ] ; then mkdir -p " ${ safe_dir } " ; fi
2024-03-10 21:59:33 +00:00
2024-03-26 21:18:57 +00:00
chmod -R 0700 " ${ safe_dir } " " ${ safe_ix } " 2>/dev/null
2019-11-28 23:18:48 +00:00
2024-06-30 23:16:26 +00:00
if [ [ -z " $( command -v ${ clip } ) " ] ] ; then
2024-03-26 21:18:57 +00:00
warn "Clipboard not available, passwords will print to screen/stdout!"
clip_dest = "screen"
2024-06-30 23:16:26 +00:00
elif [ [ -n " ${ clip_args } " ] ] ; then
clip += " ${ clip_args } "
2024-03-26 21:18:57 +00:00
fi
2017-03-31 20:35:15 +00:00
2024-03-26 21:18:57 +00:00
username = ""
2019-11-28 23:18:48 +00:00
password = ""
2017-03-31 20:35:15 +00:00
action = ""
2024-03-26 21:18:57 +00:00
2019-11-28 23:18:48 +00:00
if [ [ -n " ${ 1 +x } " ] ] ; then action = " ${ 1 } " ; fi
2015-07-03 02:56:16 +00:00
2024-03-26 21:18:57 +00:00
while [ [ -z " ${ action } " ] ] ; do 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
2024-03-26 21:52:56 +00:00
if [ [ " ${ action } " = ~ ^( [ rR] ) $ ] ] ; then read_pass " $@ "
2017-03-31 20:35:15 +00:00
elif [ [ " ${ action } " = ~ ^( [ wW] ) $ ] ] ; then
2024-03-26 21:18:57 +00:00
purse_keygroup = " $( grep "group purse_keygroup" " ${ gpg_conf } " ) "
2020-05-25 21:22:21 +00:00
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
2024-03-26 21:52:56 +00:00
if [ [ -n " ${ daily_backup } " ] ] ; then backup ; fi
2024-03-26 21:18:57 +00:00
elif [ [ " ${ action } " = ~ ^( [ lL] ) $ ] ] ; then list_entry
elif [ [ " ${ action } " = ~ ^( [ bB] ) $ ] ] ; then backup
else print_help ; fi
2019-11-28 23:18:48 +00:00
2024-03-10 21:59:33 +00:00
tput setaf 2 ; printf "\nDone\n" ; tput sgr0