1
0
mirror of https://github.com/octoleo/Purse.git synced 2024-12-29 12:32:39 +00:00

Version 2. Use symmetric crypto instead of keys. Don't write passwords in plaintext to disk. Choose passwords by character length. Add recommended gpg configuration.

This commit is contained in:
drduh 2015-07-02 22:56:16 -04:00
parent 288c4d112d
commit 72443fa6be
3 changed files with 109 additions and 150 deletions

View File

@ -8,8 +8,6 @@ This script uses GPG to manage an encrypted text file containing passwords.
# Requirements # Requirements
Requires `gpg`. Install with `brew install gpg` or `apt-get install gnupg` or build and install it from [source](https://www.gnupg.org/download/index.html). Requires `gpg`. Install with `brew install gpg` or `apt-get install gnupg` or build and install it from [source](https://www.gnupg.org/download/index.html).
Requires `srm`. Install with `apt-get install secure-delete`. Already included in OS X.
# Installation # Installation
git clone https://github.com/drduh/pwd.sh && cd pwd.sh git clone https://github.com/drduh/pwd.sh && cd pwd.sh
@ -24,5 +22,6 @@ Type `r` to print stored passwords. Can be piped to `grep` and `pbcopy` or `xsel
Type `d` to delete a password by Username/ID. Type `d` to delete a password by Username/ID.
To reset/erase keys and passwords, `rm pwd.sh.*`. The encrypted file `pwd.sh.safe` can be safely shared between machines over public channels (Google Drive, Dropbox, etc).
A sample `gpg.conf` configuration file is provided for your consideration.

27
gpg.conf Normal file
View File

@ -0,0 +1,27 @@
# ~/.gnupg/gpg.conf
#
# Suggested GnuPG configuration.
auto-key-locate keyserver
keyserver hkps://hkps.pool.sks-keyservers.net
keyserver-options no-honor-keyserver-url
keyserver-options debug
keyserver-options verbose
# sudo curl -o /etc/gpg.pem https://sks-keyservers.net/sks-keyservers.netCA.pem
keyserver-options ca-cert-file=/etc/gpg.pem
personal-cipher-preferences AES256 AES192 AES CAST5
personal-digest-preferences SHA512 SHA384 SHA256 SHA224
default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed
cert-digest-algo SHA512
charset utf-8
fixed-list-mode
no-comments
no-emit-version
keyid-format 0xlong
list-options show-uid-validity
verify-options show-uid-validity
with-fingerprint

227
pwd.sh
View File

@ -1,217 +1,150 @@
#!/bin/bash #!/bin/bash
# #
# pwd.sh # Script for managing passwords in a symmetrically encrypted file using GnuPG.
#
# An interface to gpg for managing passwords.
set -o pipefail set -o errtrace
set -o nounset set -o nounset
set -o pipefail
umask 077
safe=pwd.sh.safe
public=pwd.sh.pub
secret=pwd.sh.sec
del=$(which srm)
del_opts=("--force --zero")
gpg=$(which gpg) gpg=$(which gpg)
gpg_opts=("--no-default-keyring --keyring ./${public} --secret-keyring ./${secret}") safe=pwd.sh.safe
name="nobody@pwd.sh"
fail () {
# Print an error message and exit.
tput setaf 1 ; echo "Error: ${1}"
exit 1
}
get_pass () { get_pass () {
# Fancy prompt for fetching a password. # Prompt for a password.
unset password unset password
prompt="Password: " prompt="${1}"
while IFS= read -p "$prompt" -r -s -n 1 char while IFS= read -p "${prompt}" -r -s -n 1 char ; do
do if [[ ${char} == $'\0' ]] ; then
if [[ $char == $'\0' ]] ; then break
break
fi fi
prompt='*' prompt='*'
password+="$char" password+="${char}"
done done
if [ -z ${password+x} ] ; then
fail "No password provided"
fi
} }
decrypt () { decrypt () {
# Decrypt a gpg-encrypted file with a password. # Decrypt with a password.
${gpg} ${gpg_opts} \ ${gpg} \
--decrypt --armor --batch \ --decrypt --armor --batch \
--command-fd 0 --passphrase "${1}" "${2}" \ --command-fd 0 --passphrase "${1}" "${2}" 2>/dev/null
2>/dev/null
} }
encrypt () { encrypt () {
# Encrypt and sign a file with a password. # Encrypt with a password.
${gpg} ${gpg_opts} \ ${gpg} \
--encrypt --armor --sign --batch \ --symmetric --armor --batch --yes \
--hidden-recipient "${name}" \
--yes \
--command-fd 0 --passphrase "${1}" \ --command-fd 0 --passphrase "${1}" \
--output "${2}" "${3}" \ --output "${2}" "${3}" 2>/dev/null
2>/dev/null
} }
read_pass () { read_pass () {
# Reads a password. # Read a password from safe.
if [ ! -s ${safe} ] ; then if [ ! -s ${safe} ] ; then
echo "Empty safe, no passwords!" fail "No passwords found"
exit 3
else else
echo "Enter password to unlock ${safe}." get_pass "Enter password to unlock ${safe}: " ; echo
get_pass ; echo decrypt ${password} ${safe} || fail "Decryption failed"
decrypt ${password} ${safe}
fi fi
} }
create_id () {
# Creates a new Username/ID.
read -p "Username/ID: " id
read -p "Create random password? (y/n default: y) " rand_pass
if [ "${rand_pass}" == "n" ]; then
echo "Choose a password for '${id}'."
get_pass ; echo
user_pass=$password
else
user_pass=$(gen_pass)
fi
}
write_pass () {
# Writes a password to safe.
echo "Enter password to unlock ${safe}."
get_pass ; echo
# Create a temporary file to decrypt passwords to.
# TODO(any): can this be done without writing to disk?
tmp_secret=$(mktemp -q /tmp/pwd.sh.XXXXXX)
# Decrypt safe, exclude specified ID in case of update/removal.
if [ -s ${safe} ] ; then
decrypt ${password} ${safe} | grep -v " ${id}" > ${tmp_secret}
fi
# Append new password for ID, if one was provided.
if [ ! -z ${user_pass+x} ] ; then
echo "${user_pass} ${id}" >> ${tmp_secret}
fi
# Encrypt plaintext to safe.
encrypt ${password} ${safe} ${tmp_secret}
# Remove temporary plaintext.
${del} ${del_opts} ${tmp_secret}
echo "Updated password for '${id}' in ${safe}."
unset id
unset password
unset user_pass
}
gen_pass () { gen_pass () {
# Generate a random password. # Generate a password.
read -p "Password length? (min/avg/max default: max) " pass_length len=40
if [ "$pass_length" == "min" ]; then read -p "Password length? (default: 40, max: 80) " length
len=6
elif [ "$pass_length" == "avg" ]; then if [[ ${length} =~ ^[0-9]+$ ]] ; then
len=18 len=${length}
else
len=36
fi fi
${gpg} --gen-random -a 0 ${len} ${gpg} --gen-random -a 0 80 | cut -c -${len}
} }
create_keys () { write_pass () {
# Create public and private GnuPG keys. # Write a password in safe.
echo "Choose a strong master password." # If no password provided, clear the entry by writing an empty line.
get_pass ; echo if [ -z ${userpass+x} ] ; then
new_entry=" "
else
new_entry="${userpass} ${username}"
fi
${gpg} ${gpg_opts} \ get_pass "Enter password to unlock ${safe}: " ; echo
--gen-key --batch <(
cat <<EOF
Key-Type: RSA
Key-Length: 4096
Expire-Date: 0
Name-Real: ${name}
Passphrase: ${password}
%commit
EOF
) 2>/dev/null
echo "Created keys: ${public} and ${secret}." # If safe exists, decrypt it and filter out username, or bail on error.
# If successful, append new entry, or blank line.
unset password # Filter out any blank lines.
# Finally, encrypt it all to a new safe file, or fail.
# If successful, update to new safe file.
( if [ -f ${safe} ] ; then
decrypt ${password} ${safe} | \
grep -v -e " ${username}$" || return
fi ; \
echo "${new_entry}") | \
grep -v -e "^[[:space:]]*$" | \
encrypt ${password} ${safe}.new - || fail "Write to safe failed"
mv ${safe}.new ${safe}
} }
create_safe () { create_username () {
# Create an encrypted "safe" file to store passwords. # Create a new username and password.
touch ${safe} ; chmod 0600 ${safe} read -p "Username: " username
echo "Created encrypted safe file: ${safe}." read -p "Generate password? (y/n, default: y) " rand_pass
if [ "${rand_pass}" == "n" ]; then
get_pass "Enter password for \"${username}\": " ; echo
userpass=$password
else
userpass=$(gen_pass)
fi
} }
sanity_check () { sanity_check () {
# Make sure all necessary programs are installed and files exist. # Make sure required programs are installed and can be executed.
if [[ -z ${gpg} && ! -x ${gpg} ]] ; then if [[ -z ${gpg} && ! -x ${gpg} ]] ; then
echo "GnuPG is not available!" fail "GnuPG is not available"
exit 127
fi
if [[ -z ${del} && ! -x ${del} ]] ; then
echo "srm is not available!"
exit 127
fi
if [ ! -f ${secret} ] ; then
echo "No keys found, creating new keys ..."
create_keys
else
chmod 0600 ${secret}
fi
if [ ! -f ${safe} ] ; then
echo "No safe found, creating new safe file ..."
create_safe
else
chmod 0600 ${safe}
fi fi
} }
sanity_check sanity_check
read -p "Read, write, or delete a password? (r/w/d default: r) " action read -p "Read, write, or delete a password? (r/w/d, default: r) " action
if [ "${action}" == "w" ] ; then if [ "${action}" == "w" ] ; then
create_id create_username && write_pass
write_pass
elif [ "${action}" == "d" ] ; then elif [ "${action}" == "d" ] ; then
read -p "Which Username/ID to delete? " id read -p "Username to delete? " username
write_pass write_pass
else else
read_pass read_pass
fi fi
tput setaf 2 ; echo "Done"