Skip to content

Commit

Permalink
Merge pull request #40 from drduh/wip-10mar24
Browse files Browse the repository at this point in the history
Version 3 beta
  • Loading branch information
drduh authored Mar 10, 2024
2 parents ae4fe07 + a313775 commit 18e6024
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 78 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pwd.*.tar
pwd.index
safe/
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2015-2020 drduh
Copyright (c) 2015 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
Expand Down
64 changes: 19 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,29 @@
pwd.sh is a Bash shell script to manage passwords and other secrets.

It uses GnuPG to symmetrically (i.e., using a master password) encrypt and decrypt plain text files.
It uses GnuPG to symmetrically (i.e., using a master password) encrypt and decrypt plaintext files.

[drduh/Purse](https://github.com/drduh/Purse) is a fork which uses public key authentication instead of a master password and can integrate with YubiKey.

# Release notes

## Version 2 (2020)
See [Releases](https://github.com/drduh/pwd.sh/releases)

The second release of pwd.sh features many security and reliability improvements, and is a recommended upgrade. Compatible on Linux, OpenBSD, macOS.

Known Issues:

* Newer versions of macOS error with `tr: Illegal byte sequence` - see [issue #36](https://github.com/drduh/pwd.sh/issues/36)
# Use

Changelist:
Clone the repository:

* 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 pwd.sh is not running.
* Read passwords are now copied to clipboard and cleared after a timeout, instead of printed to stdout.
* Use printf instead of echo for improved portability.
* New option: list passwords in the index.
* New option: create tar archive for backup.
* Removed option: delete password; the index is now a permanent ledger.
* 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.
```console
git clone https://github.com/drduh/pwd.sh

## Version 1 (2015)
```

The original release which has been available for general use and review since July 2015. There are no known bugs nor security vulnerabilities identified in this stable version of pwd.sh. Compatible on Linux, OpenBSD, macOS.

# Use
Or download the script directly:

```console
$ git clone https://github.com/drduh/pwd.sh
wget https://raw.githubusercontent.com/drduh/pwd.sh/master/pwd.sh
```

`cd pwd.sh` and run the script interactively using `./pwd.sh` or symlink to a directory in `PATH`:
Run the script interactively using `./pwd.sh` or symlink to a directory in `PATH`:

* Type `w` to write a password
* Type `r` to read a password
Expand All @@ -47,48 +33,36 @@ $ git clone https://github.com/drduh/pwd.sh

Options can also be passed on the command line.

Example usage:

Create a 30-character password for `userName`:
Create a 20-character password for `userName`:

```console
$ ./pwd.sh w userName 30
./pwd.sh w userName 20
```

Read password for `userName`:

```console
$ ./pwd.sh r userName
./pwd.sh r userName
```

Passwords are stored with a timestamp for revision control. The most recent version is copied to clipboard on read. To list all passwords or read a previous version of a password:
Passwords are stored with a 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:

```console
$ ./pwd.sh l
./pwd.sh l

$ ./pwd.sh r userName@1574723600
./pwd.sh r userName@1574723600
```

Create an archive for backup:

```console
$ ./pwd.sh b
./pwd.sh b
```

Restore an archive from backup:

```console
$ tar xvf pwd*tar
tar xvf pwd*tar
```

The backup contains only encrypted files and can be publicly shared for use on trusted computers.

See [drduh/config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional GPG configuration options.

# Similar software

* [drduh/Purse](https://github.com/drduh/Purse)
* [zx2c4/password-store](https://github.com/zx2c4/password-store)
* [caodonnell/passman.sh: a pwd.sh fork](https://github.com/caodonnell/passman.sh)
* [bndw/pick: command-line password manager for macOS and Linux](https://github.com/bndw/pick)
* [anders/pwgen: generate passwords using OS X Security framework](https://github.com/anders/pwgen)
See [config/gpg.conf](https://github.com/drduh/config/blob/master/gpg.conf) for additional configuration options.
81 changes: 49 additions & 32 deletions pwd.sh
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
#!/usr/bin/env bash
# https://github.com/drduh/pwd.sh

# https://github.com/drduh/pwd.sh/blob/master/pwd.sh
set -o errtrace
set -o nounset
set -o pipefail

#set -x # uncomment to debug
#set -x # uncomment to debug

umask 077

now=$(date +%s)
copy="$(command -v xclip || command -v pbcopy)"
gpg="$(command -v gpg || command -v gpg2)"
cb_timeout=10 # seconds to keep password on clipboard
daily_backup="false" # if true, create daily archive on write
pass_copy="false" # if true, keep password on clipboard before write
pass_len=14 # default password length
pass_chars="[:alnum:]!@#$%^&*();:+="

gpgconf="${HOME}/.gnupg/gpg.conf"
backuptar="${PWDSH_BACKUP:=pwd.$(hostname).$(date +%F).tar}"
safeix="${PWDSH_INDEX:=pwd.index}"
safedir="${PWDSH_SAFE:=safe}"
script="$(basename $BASH_SOURCE)"
timeout=10

now="$(date +%s)"
copy="$(command -v xclip || command -v pbcopy)"
gpg="$(command -v gpg || command -v gpg2)"
script="$(basename "${BASH_SOURCE}")"

fail () {
# Print an error message and exit.

tput setaf 1 1 1 ; printf "\nError: %s\n" "${1}" ; tput sgr0
tput setaf 1 ; printf "\nError: %s\n" "${1}" ; tput sgr0
exit 1
}

Expand Down Expand Up @@ -52,7 +56,8 @@ get_pass () {
decrypt () {
# Decrypt with GPG.

printf "%s\n" "${1}" | ${gpg} --armor --batch --no-symkey-cache \
printf "%s\n" "${1}" | \
${gpg} --armor --batch --no-symkey-cache \
--decrypt --passphrase-fd 0 "${2}" 2>/dev/null
}

Expand Down Expand Up @@ -91,17 +96,14 @@ read_pass () {
gen_pass () {
# Generate a password using GPG.

len=20
max=80

if [[ -z "${3+x}" ]] ; then read -r -p "
Password length (default: ${len}, max: ${max}): " length
Password length (default: ${pass_len}): " length
else length="${3}" ; fi

if [[ ${length} =~ ^[0-9]+$ ]] ; then len=${length} ; fi
if [[ ${length} =~ ^[0-9]+$ ]] ; then pass_len=${length} ; fi

# base64: 4 characters for every 3 bytes
${gpg} --armor --gen-random 0 "$((max * 3 / 4))" | cut -c -"${len}"
LC_LANG=C tr -dc "${pass_chars}" < /dev/urandom | \
fold -w "${pass_len}" | head -1
}

write_pass () {
Expand All @@ -112,11 +114,16 @@ write_pass () {
Password to unlock ${safeix}: " ; done
printf "\n"

fpath=$(LC_LANG=C tr -dc "[:lower:]" < /dev/urandom | fold -w8 | head -n1)
spath=${safedir}/${fpath}
if [[ "${pass_copy}" = "true" ]] ; then
clip <(printf '%s' "${userpass}")
fi

fpath="$(LC_LANG=C tr -dc '[:lower:]' < /dev/urandom | fold -w10 | head -1)"
spath="${safedir}/${fpath}"
printf '%s\n' "${userpass}" | \
encrypt "${password}" "${spath}" - || \
fail "Failed to put ${spath}"
userpass=""

( if [[ -f "${safeix}" ]] ; then
decrypt "${password}" "${safeix}" || return ; fi
Expand All @@ -136,20 +143,21 @@ list_entry () {
Password to unlock ${safeix}: " ; done
printf "\n\n"

decrypt ${password} "${safeix}" || fail "Decryption failed"
decrypt "${password}" "${safeix}" || \
fail "Decryption failed"
}

backup () {
# Archive encrypted index and safe directory.
# Archive index, safe and configuration.

if [[ -f "${safeix}" && -d "${safedir}" ]] ; then \
if [[ -f "${safeix}" && -d "${safedir}" ]] ; then
cp "${gpgconf}" "gpg.conf.${now}"
tar cfv "${backuptar}" \
tar --create --file "${backuptar}" \
"${safeix}" "${safedir}" "gpg.conf.${now}" "${script}"
rm "gpg.conf.${now}"
else fail "Nothing to archive" ; fi

printf "\nArchived %s\n" "${backuptar}" ; \
printf "\nArchived %s\n" "${backuptar}"
}

clip () {
Expand All @@ -159,16 +167,17 @@ clip () {

printf "\n"
shift
while [ $timeout -gt 0 ] ; do
printf "\r\033[KPassword on clipboard! Clearing in %.d" $((timeout--))
while [ $cb_timeout -gt 0 ] ; do
printf "\r\033[KPassword on clipboard! Clearing in %.d" $((cb_timeout--))
sleep 1
done

printf "\n"
printf "" | ${copy}
}

new_entry () {
# Prompt for new username and/or password.
# Prompt for username and password.

username=""
while [[ -z "${username}" ]] ; do
Expand All @@ -183,7 +192,9 @@ new_entry () {
fi

printf "\n"
if [[ -z "${password}" ]] ; then userpass=$(gen_pass "$@") ; fi
if [[ -z "${password}" ]] ; then
userpass=$(gen_pass "$@")
fi
}

print_help () {
Expand All @@ -207,7 +218,7 @@ print_help () {
* Copy the password for 'userName' to clipboard:
./pwd.sh r userName
* List stored passwords and copy a previous version:
* List stored passwords and copy a specific version:
./pwd.sh l
./pwd.sh r userName@1574723625
Expand All @@ -234,7 +245,7 @@ action=""
if [[ -n "${1+x}" ]] ; then action="${1}" ; fi

while [[ -z "${action}" ]] ; do
read -n 1 -p "
read -r -n 1 -p "
Read or Write (or Help for more options): " action
printf "\n"
done
Expand All @@ -252,8 +263,14 @@ elif [[ "${action}" =~ ^([wW])$ ]] ; then
new_entry "$@"
write_pass

if [[ "${daily_backup}" = "true" ]] ; then
if [[ ! -f ${backuptar} ]] ; then
backup
fi
fi

else read_pass "$@" ; fi

chmod -R 0400 "${safeix}" "${safedir}" 2>/dev/null

tput setaf 2 2 2 ; printf "\nDone\n" ; tput sgr0
tput setaf 2 ; printf "\nDone\n" ; tput sgr0

0 comments on commit 18e6024

Please # to comment.