mirror of https://github.com/octoleo/Purse.git
252 lines
5.4 KiB
Bash
Executable File
252 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# https://github.com/drduh/Purse
|
|
|
|
set -o errtrace
|
|
set -o nounset
|
|
set -o pipefail
|
|
|
|
#set -x # uncomment to debug
|
|
|
|
umask 077
|
|
|
|
now=$(date +%s)
|
|
copy="$(command -v xclip || command -v pbcopy)"
|
|
gpg="$(command -v gpg || command -v gpg2)"
|
|
backuptar="${PURSE_BACKUP:=purse.$(hostname).$(date +%F).tar}"
|
|
keyid="${PURSE_KEYID:=0xFF3E7D88647EBCDB}"
|
|
safeix="${PURSE_INDEX:=purse.index}"
|
|
safedir="${PURSE_SAFE:=safe}"
|
|
timeout=9
|
|
|
|
fail () {
|
|
# Print an error message and exit.
|
|
|
|
tput setaf 1 1 1 ; printf "\nError: ${1}\n" ; tput sgr0
|
|
exit 1
|
|
}
|
|
|
|
get_pass () {
|
|
# Prompt for a password.
|
|
|
|
password=""
|
|
prompt="${1}"
|
|
|
|
while IFS= read -p "${prompt}" -r -s -n 1 char ; do
|
|
if [[ ${char} == $'\0' ]] ; then
|
|
break
|
|
elif [[ ${char} == $'\177' ]] ; then
|
|
if [[ -z "${password}" ]] ; then
|
|
prompt=""
|
|
else
|
|
prompt=$'\b \b'
|
|
password="${password%?}"
|
|
fi
|
|
else
|
|
prompt="*"
|
|
password+="${char}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
decrypt () {
|
|
# Decrypt with GPG.
|
|
|
|
cat "${1}" | ${gpg} --armor --batch --decrypt 2>/dev/null
|
|
}
|
|
|
|
encrypt () {
|
|
# Encrypt to a recipient.
|
|
|
|
${gpg} --encrypt --armor --batch --yes --throw-keyids \
|
|
--recipient ${keyid} --output "${1}" "${2}"
|
|
}
|
|
|
|
read_pass () {
|
|
# Read a password from safe.
|
|
|
|
if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi
|
|
|
|
username=""
|
|
while [[ -z "${username}" ]] ; do
|
|
if [[ -z "${2+x}" ]] ; then read -r -p "
|
|
Username: " username
|
|
else username="${2}" ; fi
|
|
done
|
|
|
|
prompt_key "index"
|
|
|
|
spath=$(decrypt ${safeix} | \
|
|
grep -F "${username}" | tail -n1 | cut -d : -f2) || \
|
|
fail "Decryption failed"
|
|
|
|
prompt_key "password"
|
|
|
|
clip <(decrypt ${spath} | head -n1) || \
|
|
fail "Decryption failed"
|
|
}
|
|
|
|
prompt_key () {
|
|
# Print a message if safe file exists.
|
|
|
|
if [[ -f "${safeix}" ]] ; then
|
|
printf "\n Touch key to access ${1} ...\n\n"
|
|
fi
|
|
}
|
|
|
|
gen_pass () {
|
|
# Generate a password using GPG.
|
|
|
|
len=20
|
|
max=80
|
|
|
|
if [[ -z "${3+x}" ]] ; then read -p "
|
|
|
|
Password length (default: ${len}, max: ${max}): " length
|
|
else length="${3}" ; fi
|
|
|
|
if [[ ${length} =~ ^[0-9]+$ ]] ; then len=${length} ; fi
|
|
|
|
# base64: 4 characters for every 3 bytes
|
|
${gpg} --armor --gen-random 0 "$((${max} * 3/4))" | cut -c -"${len}"
|
|
}
|
|
|
|
write_pass () {
|
|
# Write a password and update index file.
|
|
|
|
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}"
|
|
|
|
mv ${safeix}{.${now},}
|
|
}
|
|
|
|
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}
|
|
}
|
|
|
|
new_entry () {
|
|
# Prompt for new username and/or password.
|
|
|
|
username=""
|
|
while [[ -z "${username}" ]] ; do
|
|
if [[ -z "${2+x}" ]] ; then read -r -p "
|
|
Username: " username
|
|
else username="${2}" ; fi
|
|
done
|
|
|
|
if [[ -z "${3+x}" ]] ; then get_pass "
|
|
Password for \"${username}\" (Enter to generate): "
|
|
userpass="${password}"
|
|
fi
|
|
|
|
if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@") ; fi
|
|
}
|
|
|
|
print_help () {
|
|
# Print help text.
|
|
|
|
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.
|
|
|
|
Purse can be used interactively or by passing one of the following options:
|
|
|
|
* 'w' to write a password
|
|
* 'r' to read a password
|
|
* 'l' to list passwords
|
|
* 'b' to create an archive for backup
|
|
|
|
Example usage:
|
|
|
|
* Generate a 30 character password for 'userName':
|
|
./purse.sh w userName 30
|
|
|
|
* Copy the password for 'userName' to clipboard:
|
|
./purse.sh r userName
|
|
|
|
* List stored passwords and copy a previous version:
|
|
./purse.sh l
|
|
./purse.sh r userName@1574723625
|
|
|
|
* Create an archive for backup:
|
|
./purse.sh b
|
|
|
|
* Restore an archive from backup:
|
|
tar xvf purse*tar"""
|
|
}
|
|
|
|
if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi
|
|
|
|
if [[ ! -d ${safedir} ]] ; then mkdir -p ${safedir} ; fi
|
|
|
|
chmod -R 0600 ${safeix} 2>/dev/null
|
|
chmod -R 0700 ${safedir} 2>/dev/null
|
|
|
|
password=""
|
|
action=""
|
|
if [[ -n "${1+x}" ]] ; then action="${1}" ; fi
|
|
|
|
while [[ -z "${action}" ]] ; do
|
|
read -n 1 -p "
|
|
Read or Write (or Help for more options): " action
|
|
printf "\n"
|
|
done
|
|
|
|
if [[ "${action}" =~ ^([hH])$ ]] ; then
|
|
print_help
|
|
|
|
elif [[ "${action}" =~ ^([bB])$ ]] ; then
|
|
backup
|
|
|
|
elif [[ "${action}" =~ ^([lL])$ ]] ; then
|
|
list_entry
|
|
|
|
elif [[ "${action}" =~ ^([wW])$ ]] ; then
|
|
new_entry "$@"
|
|
write_pass
|
|
|
|
else read_pass "$@" ; fi
|
|
|
|
chmod -R 0400 ${safeix} ${safedir} 2>/dev/null
|
|
|
|
tput setaf 2 2 2 ; printf "\nDone\n" ; tput sgr0
|