docs: update
This commit is contained in:
parent
f480e29ee0
commit
639f221a16
6
Makefile
6
Makefile
|
@ -1,10 +1,14 @@
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
|
|
||||||
all:
|
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:
|
||||||
@install -Dm755 pash $(DESTDIR)$(PREFIX)/bin/pash
|
@install -Dm755 pash $(DESTDIR)$(PREFIX)/bin/pash
|
||||||
|
|
||||||
|
install-bash:
|
||||||
|
@install -Dm755 pash.bash $(DESTDIR)$(PREFIX)/bin/pash
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@rm -f $(DESTDIR)$(PREFIX)/bin/pash
|
@rm -f $(DESTDIR)$(PREFIX)/bin/pash
|
||||||
|
|
17
README.md
17
README.md
|
@ -1,6 +1,6 @@
|
||||||
# pash
|
# pash
|
||||||
|
|
||||||
A simple password manager using GPG.
|
A simple password manager using GPG written in POSIX `sh`.
|
||||||
|
|
||||||
```
|
```
|
||||||
pash
|
pash
|
||||||
|
@ -18,6 +18,7 @@ pash
|
||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Dependencies](#dependencies)
|
* [Dependencies](#dependencies)
|
||||||
|
* [Installation](#installation)
|
||||||
* [Usage](#usage)
|
* [Usage](#usage)
|
||||||
* [FAQ](#faq)
|
* [FAQ](#faq)
|
||||||
* [How does this differ from `pass` or etc?](#how-does-this-differ-from-pass-or-etc)
|
* [How does this differ from `pass` or etc?](#how-does-this-differ-from-pass-or-etc)
|
||||||
|
@ -32,7 +33,6 @@ pash
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- `bash 4+`
|
|
||||||
- `gpg` or `gpg2`
|
- `gpg` or `gpg2`
|
||||||
|
|
||||||
**Clipboard Support**:
|
**Clipboard Support**:
|
||||||
|
@ -40,6 +40,15 @@ pash
|
||||||
- `xclip` or `tmux`
|
- `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
|
## Usage
|
||||||
|
|
||||||
Examples: `pash add web/gmail`, `pash list`, `pash del google`, `pash show github`, `pash copy github`.
|
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?
|
### 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?
|
### Where are passwords stored?
|
||||||
|
|
||||||
|
|
154
pash
154
pash
|
@ -1,75 +1,120 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# pash - simple password manager.
|
# pash - simple password manager.
|
||||||
|
|
||||||
pw_add() {
|
pw_add() {
|
||||||
yn "Generate a password?"
|
name=$1
|
||||||
|
|
||||||
case $REPLY in
|
if yn "Generate a password?"; then
|
||||||
[yY])
|
# Use 'gpg' to generate the password. This could have
|
||||||
pass=$("${gpg[0]}" --armor --gen-random 0 "${PASH_LENGTH:-50}")
|
# been 'openssl', '/dev/[u]random' or another utility,
|
||||||
pass=${pass:0:${PASH_LENGTH:-50}}
|
# 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 ;;
|
else
|
||||||
esac
|
printf 'Enter password: '
|
||||||
|
|
||||||
[[ $pass ]] ||
|
stty -echo
|
||||||
die "Failed to generate a password."
|
read -r pass
|
||||||
|
stty echo
|
||||||
|
|
||||||
[[ $PASH_KEYID ]] &&
|
printf '\n'
|
||||||
flags=(--trust-model always -aer "$PASH_KEYID")
|
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() {
|
pw_del() {
|
||||||
yn "Delete pass file '$1'?"
|
yn "Delete pass file '$1'?" && {
|
||||||
|
|
||||||
[[ $REPLY == [yY] ]] && {
|
|
||||||
rm -f "$1.gpg"
|
rm -f "$1.gpg"
|
||||||
rmdir -p "${1%/*}" 2>/dev/null
|
rmdir -p "${1%/*}" 2>/dev/null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_show() {
|
pw_show() {
|
||||||
read -r pass < <("${gpg[0]}" -dq "$1.gpg")
|
pass=$("$gpg" -dq "$1.gpg")
|
||||||
|
|
||||||
[[ ${FUNCNAME[1]} != pw_copy ]] &&
|
# If '$2' is defined, don't print the password to the
|
||||||
printf '%s\n' "$pass"
|
# terminal. For example, this is used when the password is
|
||||||
|
# copied to the clipboard.
|
||||||
|
[ "$2" ] || printf '%s\n' "$pass"
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_copy() {
|
pw_copy() {
|
||||||
pw_show "$1"
|
pw_show "$1" copy
|
||||||
|
|
||||||
if [[ $TMUX ]]; then
|
if [ "$TMUX" ]; then
|
||||||
tmux load-buffer "$pass"
|
tmux load-buffer "$pass"
|
||||||
else
|
|
||||||
hash xclip && echo "$pass" | xclip -selection clipboard
|
elif hash xclip; then
|
||||||
|
echo "$pass" | xclip -selection clipboard
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_list() {
|
pw_list() {
|
||||||
shopt -s globstar nullglob
|
if hash tree 2>/dev/null; then
|
||||||
|
tree --noreport
|
||||||
printf '%s\n' "pash"
|
else
|
||||||
|
find . -mindepth 1
|
||||||
for pwrd in **; do
|
fi
|
||||||
[[ -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() {
|
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'
|
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() {
|
die() {
|
||||||
|
@ -94,40 +139,47 @@ exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
[[ $1 == -? || -z $1 ]] &&
|
: "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
|
||||||
|
|
||||||
|
[ "$1" = '-?' ] || [ -z "$1" ] &&
|
||||||
usage
|
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."
|
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."
|
die "Couldn't create password directory."
|
||||||
|
|
||||||
cd "$PASH_DIR" ||
|
cd "$PASH_DIR" ||
|
||||||
die "Can't access password directory."
|
die "Can't access password directory."
|
||||||
|
|
||||||
[[ $1 == [acds]* && -z $2 ]] &&
|
glob "$1" '[acds]*' && [ -z "$2" ] &&
|
||||||
die "Missing [name] argument."
|
die "Missing [name] argument."
|
||||||
|
|
||||||
[[ $1 == [cds]* && ! -f $2.gpg ]] &&
|
glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] &&
|
||||||
die "Pass file '$2' doesn't exist."
|
die "Pass file '$2' doesn't exist."
|
||||||
|
|
||||||
[[ $1 == a* && -f $2.gpg ]] &&
|
glob "$1" 'a*' && [ -f "$2.gpg" ] &&
|
||||||
die "Pass file '$2' already exists."
|
die "Pass file '$2' already exists."
|
||||||
|
|
||||||
[[ $2 == */* && $2 == *../* ]] &&
|
glob "$2" '*/*' && glob "$2" '*../*' &&
|
||||||
die "Category went out of bounds."
|
die "Category went out of bounds."
|
||||||
|
|
||||||
[[ $2 == /* ]] &&
|
glob "$2" '/*' &&
|
||||||
die "Category can't start with '/'."
|
die "Category can't start with '/'."
|
||||||
|
|
||||||
[[ $2 == */* ]] &&
|
glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
|
||||||
{ mkdir -p "${2%/*}" || die "Couldn't create category '${2%/*}'."; }
|
die "Couldn't create category '${2%/*}'."; }
|
||||||
|
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
a*) pw_add "$2" && printf '%s\n' "Saved '$2' to store." ;;
|
a*) pw_add "$2" ;;
|
||||||
c*) pw_copy "$2" ;;
|
c*) pw_copy "$2" ;;
|
||||||
d*) pw_del "$2" ;;
|
d*) pw_del "$2" ;;
|
||||||
s*) pw_show "$2" ;;
|
s*) pw_show "$2" ;;
|
||||||
|
|
191
pash-posix
191
pash-posix
|
@ -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 "$@"
|
|
|
@ -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 "$@"
|
Loading…
Reference in New Issue