mirror of
https://github.com/octoleo/Purse.git
synced 2024-12-28 03:45:04 +00:00
Use keygroup, make encrypted index optional
This commit is contained in:
parent
8d9ca6c14d
commit
883e68a95f
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 drduh
|
||||
Copyright (c) 2018-2020 drduh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
22
README.md
22
README.md
@ -10,18 +10,26 @@ By using Purse with YubiKey, the risk of master password theft or keylogging is
|
||||
|
||||
# Release notes
|
||||
|
||||
## Version 1 (2018)
|
||||
## Version 2b1 (2020)
|
||||
|
||||
The original release which has been available for general use and review since June 2018 (forked from pwd.sh which dates to 2015). There are no known bugs nor security vulnerabilities identified in this stable version of purse.sh. Compatible on Linux, OpenBSD, macOS.
|
||||
Minor update to the second release. Currently in beta testing. Compatible on Linux, OpenBSD, macOS.
|
||||
|
||||
Changelist:
|
||||
|
||||
* Purse now uses a GPG keygroup to encrypt secrets to multiple recipients for improved reliability. The program will prompt for key IDs to define the keygroup; a single key ID can still be used.
|
||||
* Encrypted index is now optional and off by default, allowing a single touch to encrypt and decrypt secrets instead of two.
|
||||
* GPG configuration file is now included in Purse backup archives.
|
||||
|
||||
## Version 2b (2019)
|
||||
|
||||
The second release of purse.sh features several security and reliability improvements, and is an optional upgrade. Currently in beta testing. Compatible on Linux, OpenBSD, macOS.
|
||||
|
||||
Known issues:
|
||||
|
||||
* Read actions now require two Yubikey touches, if touch to decrypt is enabled - once for the index and twice for the encrypted password file.
|
||||
|
||||
Changelist:
|
||||
|
||||
* Passwords are now encrypted as individual files, rather than all encrypted as a single flat file.
|
||||
* Individual password filenames are random, mapped to usernames in an encrypted index file.
|
||||
* Index and password files are now "immutable" using chmod while purse.sh is not running.
|
||||
@ -33,15 +41,19 @@ Changelist:
|
||||
* Removed option: read all passwords; no use case for having a single command.
|
||||
* Removed option: suppress generated password output; should be read from safe to verify save.
|
||||
|
||||
## Version 1 (2018)
|
||||
|
||||
The original release which has been available for general use and review since June 2018 (forked from pwd.sh which dates to 2015). There are no known bugs nor security vulnerabilities identified in this stable version of purse.sh. Compatible on Linux, OpenBSD, macOS.
|
||||
|
||||
# Use
|
||||
|
||||
This script requires a GPG identity - see [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) to set one up.
|
||||
This script requires a GPG identity - see [drduh/YubiKey-Guide](https://github.com/drduh/YubiKey-Guide) to set one up. Multiple identities stored on several YubiKeys are recommended for reliability.
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/drduh/Purse
|
||||
```
|
||||
|
||||
Set your GPG key ID with `export PURSE_KEYID=0xFF3E7D88647EBCDB` or by editing `purse.sh`.
|
||||
(Version 2b and older) Set your GPG key ID with `export PURSE_KEYID=0xFF3E7D88647EBCDB` or by editing `purse.sh`.
|
||||
|
||||
`cd purse.sh` and run the script interactively using `./purse.sh` or symlink to a directory in `PATH`:
|
||||
|
||||
@ -87,7 +99,7 @@ Restore an archive from backup:
|
||||
$ tar xvf purse*tar
|
||||
```
|
||||
|
||||
The backup contains only encrypted files and can be publicly shared for use on trusted computers. For additional privacy, the recipient key ID is **not** included in GPG metadata (`throw-keyids` option).
|
||||
The backup contains only encrypted passwords and can be publicly shared for use on trusted computers. For additional privacy, the recipient key ID is **not** included in GPG metadata (`throw-keyids` option). The password index file can also be encrypted by changing the `encrypt_index` variable to `true` in the script.
|
||||
|
||||
See [drduh/config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GPG configuration options.
|
||||
|
||||
|
113
purse.sh
113
purse.sh
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# https://github.com/drduh/Purse
|
||||
# https://github.com/drduh/Purse/blob/master/purse.sh
|
||||
|
||||
set -o errtrace
|
||||
set -o nounset
|
||||
@ -9,19 +9,20 @@ set -o pipefail
|
||||
|
||||
umask 077
|
||||
|
||||
encrypt_index="false"
|
||||
now=$(date +%s)
|
||||
copy="$(command -v xclip || command -v pbcopy)"
|
||||
gpg="$(command -v gpg || command -v gpg2)"
|
||||
gpgconf="${HOME}/.gnupg/gpg.conf"
|
||||
backuptar="${PURSE_BACKUP:=purse.$(hostname).$(date +%F).tar}"
|
||||
keyid="${PURSE_KEYID:=0xFF3E7D88647EBCDB}"
|
||||
safeix="${PURSE_INDEX:=purse.index}"
|
||||
safedir="${PURSE_SAFE:=safe}"
|
||||
timeout=9
|
||||
timeout=10
|
||||
|
||||
fail () {
|
||||
# Print an error message and exit.
|
||||
|
||||
tput setaf 1 1 1 ; printf "\nError: ${1}\n" ; tput sgr0
|
||||
tput setaf 1 1 1 ; printf "\nError: %s\n" "${1}" ; tput sgr0
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -55,10 +56,11 @@ decrypt () {
|
||||
}
|
||||
|
||||
encrypt () {
|
||||
# Encrypt to a recipient.
|
||||
# Encrypt to a group of hidden recipients.
|
||||
|
||||
${gpg} --encrypt --armor --batch --yes --throw-keyids \
|
||||
--recipient ${keyid} --output "${1}" "${2}"
|
||||
--hidden-recipient "purse_keygroup" \
|
||||
--output "${1}" "${2}"
|
||||
}
|
||||
|
||||
read_pass () {
|
||||
@ -73,15 +75,20 @@ read_pass () {
|
||||
else username="${2}" ; fi
|
||||
done
|
||||
|
||||
prompt_key "index"
|
||||
if [[ "${encrypt_index}" = "true" ]] ; then
|
||||
prompt_key "index"
|
||||
|
||||
spath=$(decrypt ${safeix} | \
|
||||
grep -F "${username}" | tail -n1 | cut -d : -f2) || \
|
||||
fail "Decryption failed"
|
||||
spath=$(decrypt "${safeix}" | \
|
||||
grep -F "${username}" | tail -n1 | cut -d ":" -f2) || \
|
||||
fail "Decryption failed"
|
||||
else
|
||||
spath=$(grep -F "${username}" "${safeix}" | \
|
||||
tail -n1 | cut -d ":" -f2)
|
||||
fi
|
||||
|
||||
prompt_key "password"
|
||||
|
||||
clip <(decrypt ${spath} | head -n1) || \
|
||||
clip <(decrypt "${spath}" | head -n1) || \
|
||||
fail "Decryption failed"
|
||||
}
|
||||
|
||||
@ -89,7 +96,7 @@ prompt_key () {
|
||||
# Print a message if safe file exists.
|
||||
|
||||
if [[ -f "${safeix}" ]] ; then
|
||||
printf "\n Touch key to access ${1} ...\n\n"
|
||||
printf "\n Touch key to access %s ...\n" "${1}"
|
||||
fi
|
||||
}
|
||||
|
||||
@ -99,7 +106,7 @@ gen_pass () {
|
||||
len=20
|
||||
max=80
|
||||
|
||||
if [[ -z "${3+x}" ]] ; then read -p "
|
||||
if [[ -z "${3+x}" ]] ; then read -r -p "
|
||||
|
||||
Password length (default: ${len}, max: ${max}): " length
|
||||
else length="${3}" ; fi
|
||||
@ -107,7 +114,7 @@ gen_pass () {
|
||||
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}"
|
||||
${gpg} --armor --gen-random 0 "$((max * 3 / 4))" | cut -c -"${len}"
|
||||
}
|
||||
|
||||
write_pass () {
|
||||
@ -116,18 +123,22 @@ write_pass () {
|
||||
fpath=$(tr -dc "[:lower:]" < /dev/urandom | fold -w8 | head -n1)
|
||||
spath=${safedir}/${fpath}
|
||||
printf '%s\n' "${userpass}" | \
|
||||
encrypt ${spath} - || \
|
||||
encrypt "${spath}" - || \
|
||||
fail "Failed to put ${spath}"
|
||||
|
||||
prompt_key "index"
|
||||
if [[ "${encrypt_index}" = "true" ]] ; then
|
||||
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}"
|
||||
( 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
|
||||
|
||||
mv ${safeix}{.${now},}
|
||||
}
|
||||
|
||||
list_entry () {
|
||||
@ -135,24 +146,30 @@ list_entry () {
|
||||
|
||||
if [[ ! -s ${safeix} ]] ; then fail "${safeix} not found" ; fi
|
||||
|
||||
prompt_key "index"
|
||||
|
||||
decrypt ${safeix} || fail "Decryption failed"
|
||||
if [[ "${encrypt_index}" = "true" ]] ; then
|
||||
prompt_key "index"
|
||||
decrypt "${safeix}" || fail "Decryption failed"
|
||||
else
|
||||
cat "${safeix}"
|
||||
fi
|
||||
}
|
||||
|
||||
backup () {
|
||||
# Archive encrypted index and safe directory.
|
||||
|
||||
if [[ -f ${safeix} && -d ${safedir} ]] ; then \
|
||||
tar cfv ${backuptar} ${safeix} ${safedir}
|
||||
if [[ -f "${safeix}" ]] ; then
|
||||
cp "${gpgconf}" "gpg.conf.${now}"
|
||||
tar cfv "${backuptar}" "${safeix}" "${safedir}" "gpg.conf.${now}"
|
||||
rm "gpg.conf.${now}"
|
||||
else fail "Nothing to archive" ; fi
|
||||
printf "\nArchived ${backuptar}\n" ; \
|
||||
|
||||
printf "\nArchived %s \n" "${backuptar}"
|
||||
}
|
||||
|
||||
clip () {
|
||||
# Use clipboard and clear after timeout.
|
||||
|
||||
${copy} < ${1}
|
||||
${copy} < "${1}"
|
||||
|
||||
printf "\n"
|
||||
shift
|
||||
@ -164,6 +181,34 @@ clip () {
|
||||
printf "" | ${copy}
|
||||
}
|
||||
|
||||
|
||||
setup_keygroup() {
|
||||
# Configure GPG keygroup setting.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
new_entry () {
|
||||
# Prompt for new username and/or password.
|
||||
|
||||
@ -216,6 +261,10 @@ print_help () {
|
||||
|
||||
if [[ -z ${gpg} && ! -x ${gpg} ]] ; then fail "GnuPG is not available" ; fi
|
||||
|
||||
if [[ ! -f ${gpgconf} ]] ; then fail "GnuPG config is not available" ; fi
|
||||
|
||||
if [[ -z ${copy} && ! -x ${copy} ]] ; then fail "Clipboard is not available" ; fi
|
||||
|
||||
if [[ ! -d ${safedir} ]] ; then mkdir -p ${safedir} ; fi
|
||||
|
||||
chmod -R 0600 ${safeix} 2>/dev/null
|
||||
@ -241,6 +290,12 @@ elif [[ "${action}" =~ ^([lL])$ ]] ; then
|
||||
list_entry
|
||||
|
||||
elif [[ "${action}" =~ ^([wW])$ ]] ; then
|
||||
purse_keygroup=$(grep "group purse_keygroup" "${gpgconf}")
|
||||
if [[ -z "${purse_keygroup}" ]] ; then
|
||||
setup_keygroup
|
||||
fi
|
||||
printf "\n %s\n" "${purse_keygroup}"
|
||||
|
||||
new_entry "$@"
|
||||
write_pass
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user