From 639f221a162561679dfd619f44d88836c6843870 Mon Sep 17 00:00:00 2001 From: Dylan Araps Date: Tue, 26 Nov 2019 12:21:45 +0000 Subject: [PATCH] docs: update --- Makefile | 6 +- README.md | 17 +++-- pash | 154 ++++++++++++++++++++++++++++-------------- pash-posix | 191 ----------------------------------------------------- pash.bash | 139 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 247 deletions(-) delete mode 100755 pash-posix create mode 100755 pash.bash diff --git a/Makefile b/Makefile index 707c670..5256b08 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ PREFIX ?= /usr all: - @echo Run \'make install\' to install pash. + @echo Run \'make install\' to install pash \(POSIX sh\). + @echo Run \'make install-bash\' to install pash \(bash\). install: @install -Dm755 pash $(DESTDIR)$(PREFIX)/bin/pash +install-bash: + @install -Dm755 pash.bash $(DESTDIR)$(PREFIX)/bin/pash + uninstall: @rm -f $(DESTDIR)$(PREFIX)/bin/pash diff --git a/README.md b/README.md index eda4167..1bfdf08 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pash -A simple password manager using GPG. +A simple password manager using GPG written in POSIX `sh`. ``` pash @@ -18,6 +18,7 @@ pash * [Dependencies](#dependencies) +* [Installation](#installation) * [Usage](#usage) * [FAQ](#faq) * [How does this differ from `pass` or etc?](#how-does-this-differ-from-pass-or-etc) @@ -32,7 +33,6 @@ pash ## Dependencies -- `bash 4+` - `gpg` or `gpg2` **Clipboard Support**: @@ -40,6 +40,15 @@ pash - `xclip` or `tmux` +## Installation + +Two versions of `pash` are available, one written in POSIX `sh` and the other written in `bash`. They are both functionally identical and the `Makefile` gives the choice of which version you would like to install. + +- `make install` (POSIX `sh`) +- `make install-bash` (`bash`) +- Or just `cp` the desired version to your `$PATH`. + + ## Usage Examples: `pash add web/gmail`, `pash list`, `pash del google`, `pash show github`, `pash copy github`. @@ -62,9 +71,9 @@ COMMANDS ### How does this differ from `pass` or etc? -I was looking for a CLI password manager (*written in `bash`*) and wasn't happy with the options I had found. They either had multiple instances of `eval` (*on user inputted data*), lots of unsafe `bash` (*nowhere near being `shellcheck` compliant.*) or they were overly complex. The opposites for what I'd want in a password manager. +I was looking for a CLI password manager (*written in shell*) and wasn't happy with the options I had found. They either had multiple instances of `eval` (*on user inputted data*), lots of unsafe `bash` (*nowhere near being `shellcheck` compliant.*) or they were overly complex. The opposites for what I'd want in a password manager. -I decided to write my own. `pash` is written in pure `bash` (*minus `gpg`, `mkdir` and optionally `xclip`.*) and the codebase is minimal (*100~ lines*). `gpg` is used to generate passwords and store them in encrypted files. +I decided to write my own. `pash` is written in POSIX `sh` and the codebase is minimal (*100~ lines*). `gpg` is used to generate passwords and store them in encrypted files. ### Where are passwords stored? diff --git a/pash b/pash index 0b1c352..1b6453f 100755 --- a/pash +++ b/pash @@ -1,75 +1,120 @@ -#!/usr/bin/env bash +#!/bin/sh # # pash - simple password manager. pw_add() { - yn "Generate a password?" + name=$1 - case $REPLY in - [yY]) - pass=$("${gpg[0]}" --armor --gen-random 0 "${PASH_LENGTH:-50}") - pass=${pass:0:${PASH_LENGTH:-50}} - ;; + if yn "Generate a password?"; then + # Use 'gpg' to generate the password. This could have + # been 'openssl', '/dev/[u]random' or another utility, + # however sticking to 'gpg' removes the need for another + # dependency. + # + # The '-a' flag outputs the random bytes as a 'base64' + # encoded string to allow for the password to be used as + # well, a password. + # + # The 'cut' is required to actually truncate the password + # to the set length as the 'base64' encoding makes the + # resulting string longer than the given length. + pass=$("$gpg" -a --gen-random 1 "${PASH_LENGTH:-50}" |\ + cut -c -"${PASH_LENGTH:-50}") - *) read -rsp "Enter password: " pass ;; - esac + else + printf 'Enter password: ' - [[ $pass ]] || - die "Failed to generate a password." + stty -echo + read -r pass + stty echo - [[ $PASH_KEYID ]] && - flags=(--trust-model always -aer "$PASH_KEYID") + printf '\n' + fi - echo "$pass" | GPG_TTY=$(tty) "${gpg[0]}" "${flags[@]:--c}" -o "$1.gpg" + [ "$pass" ] || die "Failed to generate a password." + + # Mimic the use of an array for storing arguments by... using + # the function's argument list. This is very apt isn't it? + if [ "$PASH_KEYID" ]; then + set -- --trust-model always -aer "$PASH_KEYID" + else + set -- -c + fi + + # Use 'gpg' to store the password in an encrypted file. The + # 'GPG_TTY' environment variable is set to workaround cases + # where 'gpg' cannot find an attached terminal. + echo "$pass" | GPG_TTY=$(tty) "$gpg" "$@" -o "$name.gpg" && + printf '%s\n' "Saved '$name' to the store." } pw_del() { - yn "Delete pass file '$1'?" - - [[ $REPLY == [yY] ]] && { + yn "Delete pass file '$1'?" && { rm -f "$1.gpg" rmdir -p "${1%/*}" 2>/dev/null } } pw_show() { - read -r pass < <("${gpg[0]}" -dq "$1.gpg") + pass=$("$gpg" -dq "$1.gpg") - [[ ${FUNCNAME[1]} != pw_copy ]] && - printf '%s\n' "$pass" + # If '$2' is defined, don't print the password to the + # terminal. For example, this is used when the password is + # copied to the clipboard. + [ "$2" ] || printf '%s\n' "$pass" } pw_copy() { - pw_show "$1" + pw_show "$1" copy - if [[ $TMUX ]]; then + if [ "$TMUX" ]; then tmux load-buffer "$pass" - else - hash xclip && echo "$pass" | xclip -selection clipboard + + elif hash xclip; then + echo "$pass" | xclip -selection clipboard fi } pw_list() { - shopt -s globstar nullglob - - printf '%s\n' "pash" - - for pwrd in **; do - [[ -d $pwrd ]] && dir=/ || dir= - - nest=${pwrd//[^\/]} - pwrd=${pwrd//[^[:print:]]/^[} - pwrd=${pwrd//.gpg} - - printf '%s\n' "${nest//\//│ }├─ ${pwrd##*/}${dir}" - done - - printf '└%s\b┘\n' "${nest//\//──┴}" + if hash tree 2>/dev/null; then + tree --noreport + else + find . -mindepth 1 + fi } yn() { - read -rn 1 -p "$1 [y/n]: " + printf '%s [y/n]: ' "$1" + + # Enable raw input to allow for a single byte to be read from + # stdin without needing to wait for the user to press Return. + stty -icanon + + # Read a single byte from stdin using 'dd'. POSIX 'read' has + # no support for single/'N' byte based input from the user. + answer=$(dd ibs=1 count=1 2>/dev/null) + + # Disable raw input, leaving the terminal how we *should* + # have found it. + stty icanon + printf '\n' + + # Handle the answer here directly, enabling this function's + # return status to be used in place of checking for '[yY]' + # throughout this program. + glob "$answer" '[yY]' || return 1 && return 0 +} + +glob() { + # This is a simple wrapper around a case statement to allow + # for simple string comparisons against globs. + # + # Example: if glob "Hello World" '* World'; then + # + # Disable this warning as it is the intended behavior. + # shellcheck disable=2254 + case $1 in $2) return 0; esac; return 1 } die() { @@ -94,40 +139,47 @@ exit 1 } main() { - [[ $1 == -? || -z $1 ]] && + : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" + + [ "$1" = '-?' ] || [ -z "$1" ] && usage - mapfile -t gpg < <(type -p gpg gpg2) && [[ ! -x ${gpg[0]} ]] && + # Look for both 'gpg' and 'gpg2', + # preferring 'gpg2' if it is available. + hash gpg 2>/dev/null && gpg=gpg + hash gpg2 2>/dev/null && gpg=gpg2 + + [ "$gpg" ] || die "GPG not found." - mkdir -p "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" || + mkdir -p "$PASH_DIR" || die "Couldn't create password directory." cd "$PASH_DIR" || die "Can't access password directory." - [[ $1 == [acds]* && -z $2 ]] && + glob "$1" '[acds]*' && [ -z "$2" ] && die "Missing [name] argument." - [[ $1 == [cds]* && ! -f $2.gpg ]] && + glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] && die "Pass file '$2' doesn't exist." - [[ $1 == a* && -f $2.gpg ]] && + glob "$1" 'a*' && [ -f "$2.gpg" ] && die "Pass file '$2' already exists." - [[ $2 == */* && $2 == *../* ]] && + glob "$2" '*/*' && glob "$2" '*../*' && die "Category went out of bounds." - [[ $2 == /* ]] && + glob "$2" '/*' && die "Category can't start with '/'." - [[ $2 == */* ]] && - { mkdir -p "${2%/*}" || die "Couldn't create category '${2%/*}'."; } + glob "$2" '*/*' && { mkdir -p "${2%/*}" || + die "Couldn't create category '${2%/*}'."; } umask 077 case $1 in - a*) pw_add "$2" && printf '%s\n' "Saved '$2' to store." ;; + a*) pw_add "$2" ;; c*) pw_copy "$2" ;; d*) pw_del "$2" ;; s*) pw_show "$2" ;; diff --git a/pash-posix b/pash-posix deleted file mode 100755 index 1b6453f..0000000 --- a/pash-posix +++ /dev/null @@ -1,191 +0,0 @@ -#!/bin/sh -# -# pash - simple password manager. - -pw_add() { - name=$1 - - if yn "Generate a password?"; then - # Use 'gpg' to generate the password. This could have - # been 'openssl', '/dev/[u]random' or another utility, - # however sticking to 'gpg' removes the need for another - # dependency. - # - # The '-a' flag outputs the random bytes as a 'base64' - # encoded string to allow for the password to be used as - # well, a password. - # - # The 'cut' is required to actually truncate the password - # to the set length as the 'base64' encoding makes the - # resulting string longer than the given length. - pass=$("$gpg" -a --gen-random 1 "${PASH_LENGTH:-50}" |\ - cut -c -"${PASH_LENGTH:-50}") - - else - printf 'Enter password: ' - - stty -echo - read -r pass - stty echo - - printf '\n' - fi - - [ "$pass" ] || die "Failed to generate a password." - - # Mimic the use of an array for storing arguments by... using - # the function's argument list. This is very apt isn't it? - if [ "$PASH_KEYID" ]; then - set -- --trust-model always -aer "$PASH_KEYID" - else - set -- -c - fi - - # Use 'gpg' to store the password in an encrypted file. The - # 'GPG_TTY' environment variable is set to workaround cases - # where 'gpg' cannot find an attached terminal. - echo "$pass" | GPG_TTY=$(tty) "$gpg" "$@" -o "$name.gpg" && - printf '%s\n' "Saved '$name' to the store." -} - -pw_del() { - yn "Delete pass file '$1'?" && { - rm -f "$1.gpg" - rmdir -p "${1%/*}" 2>/dev/null - } -} - -pw_show() { - pass=$("$gpg" -dq "$1.gpg") - - # If '$2' is defined, don't print the password to the - # terminal. For example, this is used when the password is - # copied to the clipboard. - [ "$2" ] || printf '%s\n' "$pass" -} - -pw_copy() { - pw_show "$1" copy - - if [ "$TMUX" ]; then - tmux load-buffer "$pass" - - elif hash xclip; then - echo "$pass" | xclip -selection clipboard - fi -} - -pw_list() { - if hash tree 2>/dev/null; then - tree --noreport - else - find . -mindepth 1 - fi -} - -yn() { - printf '%s [y/n]: ' "$1" - - # Enable raw input to allow for a single byte to be read from - # stdin without needing to wait for the user to press Return. - stty -icanon - - # Read a single byte from stdin using 'dd'. POSIX 'read' has - # no support for single/'N' byte based input from the user. - answer=$(dd ibs=1 count=1 2>/dev/null) - - # Disable raw input, leaving the terminal how we *should* - # have found it. - stty icanon - - printf '\n' - - # Handle the answer here directly, enabling this function's - # return status to be used in place of checking for '[yY]' - # throughout this program. - glob "$answer" '[yY]' || return 1 && return 0 -} - -glob() { - # This is a simple wrapper around a case statement to allow - # for simple string comparisons against globs. - # - # Example: if glob "Hello World" '* World'; then - # - # Disable this warning as it is the intended behavior. - # shellcheck disable=2254 - case $1 in $2) return 0; esac; return 1 -} - -die() { - printf 'error: %s\n' "$1" >&2 - exit 1 -} - -usage() { printf %s "\ -pash 1.0.0 - simple password manager. - -=> [a]dd [name] - Create a new password entry. -=> [c]opy [name] - Copy entry to the clipboard. -=> [d]el [name] - Delete a password entry. -=> [l]ist - List all entries. -=> [s]how [name] - Show password for an entry. - -Using a key pair: export PASH_KEYID=XXXXXXXX -Password length: export PASH_LENGTH=50 -Store location: export PASH_DIR=~/.local/share/pash -" -exit 1 -} - -main() { - : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" - - [ "$1" = '-?' ] || [ -z "$1" ] && - usage - - # Look for both 'gpg' and 'gpg2', - # preferring 'gpg2' if it is available. - hash gpg 2>/dev/null && gpg=gpg - hash gpg2 2>/dev/null && gpg=gpg2 - - [ "$gpg" ] || - die "GPG not found." - - mkdir -p "$PASH_DIR" || - die "Couldn't create password directory." - - cd "$PASH_DIR" || - die "Can't access password directory." - - glob "$1" '[acds]*' && [ -z "$2" ] && - die "Missing [name] argument." - - glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] && - die "Pass file '$2' doesn't exist." - - glob "$1" 'a*' && [ -f "$2.gpg" ] && - die "Pass file '$2' already exists." - - glob "$2" '*/*' && glob "$2" '*../*' && - die "Category went out of bounds." - - glob "$2" '/*' && - die "Category can't start with '/'." - - glob "$2" '*/*' && { mkdir -p "${2%/*}" || - die "Couldn't create category '${2%/*}'."; } - - umask 077 - - case $1 in - a*) pw_add "$2" ;; - c*) pw_copy "$2" ;; - d*) pw_del "$2" ;; - s*) pw_show "$2" ;; - l*) pw_list ;; - *) usage - esac -} - -main "$@" diff --git a/pash.bash b/pash.bash new file mode 100755 index 0000000..0b1c352 --- /dev/null +++ b/pash.bash @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# +# pash - simple password manager. + +pw_add() { + yn "Generate a password?" + + case $REPLY in + [yY]) + pass=$("${gpg[0]}" --armor --gen-random 0 "${PASH_LENGTH:-50}") + pass=${pass:0:${PASH_LENGTH:-50}} + ;; + + *) read -rsp "Enter password: " pass ;; + esac + + [[ $pass ]] || + die "Failed to generate a password." + + [[ $PASH_KEYID ]] && + flags=(--trust-model always -aer "$PASH_KEYID") + + echo "$pass" | GPG_TTY=$(tty) "${gpg[0]}" "${flags[@]:--c}" -o "$1.gpg" +} + +pw_del() { + yn "Delete pass file '$1'?" + + [[ $REPLY == [yY] ]] && { + rm -f "$1.gpg" + rmdir -p "${1%/*}" 2>/dev/null + } +} + +pw_show() { + read -r pass < <("${gpg[0]}" -dq "$1.gpg") + + [[ ${FUNCNAME[1]} != pw_copy ]] && + printf '%s\n' "$pass" +} + +pw_copy() { + pw_show "$1" + + if [[ $TMUX ]]; then + tmux load-buffer "$pass" + else + hash xclip && echo "$pass" | xclip -selection clipboard + fi +} + +pw_list() { + shopt -s globstar nullglob + + printf '%s\n' "pash" + + for pwrd in **; do + [[ -d $pwrd ]] && dir=/ || dir= + + nest=${pwrd//[^\/]} + pwrd=${pwrd//[^[:print:]]/^[} + pwrd=${pwrd//.gpg} + + printf '%s\n' "${nest//\//│ }├─ ${pwrd##*/}${dir}" + done + + printf '└%s\b┘\n' "${nest//\//──┴}" +} + +yn() { + read -rn 1 -p "$1 [y/n]: " + printf '\n' +} + +die() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} + +usage() { printf %s "\ +pash 1.0.0 - simple password manager. + +=> [a]dd [name] - Create a new password entry. +=> [c]opy [name] - Copy entry to the clipboard. +=> [d]el [name] - Delete a password entry. +=> [l]ist - List all entries. +=> [s]how [name] - Show password for an entry. + +Using a key pair: export PASH_KEYID=XXXXXXXX +Password length: export PASH_LENGTH=50 +Store location: export PASH_DIR=~/.local/share/pash +" +exit 1 +} + +main() { + [[ $1 == -? || -z $1 ]] && + usage + + mapfile -t gpg < <(type -p gpg gpg2) && [[ ! -x ${gpg[0]} ]] && + die "GPG not found." + + mkdir -p "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" || + die "Couldn't create password directory." + + cd "$PASH_DIR" || + die "Can't access password directory." + + [[ $1 == [acds]* && -z $2 ]] && + die "Missing [name] argument." + + [[ $1 == [cds]* && ! -f $2.gpg ]] && + die "Pass file '$2' doesn't exist." + + [[ $1 == a* && -f $2.gpg ]] && + die "Pass file '$2' already exists." + + [[ $2 == */* && $2 == *../* ]] && + die "Category went out of bounds." + + [[ $2 == /* ]] && + die "Category can't start with '/'." + + [[ $2 == */* ]] && + { mkdir -p "${2%/*}" || die "Couldn't create category '${2%/*}'."; } + + umask 077 + + case $1 in + a*) pw_add "$2" && printf '%s\n' "Saved '$2' to store." ;; + c*) pw_copy "$2" ;; + d*) pw_del "$2" ;; + s*) pw_show "$2" ;; + l*) pw_list ;; + *) usage + esac +} + +main "$@"