dotfiles

Mahdi's dotfiles
git clone git://mahdi.pw/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

pash (7726B)


      1 #!/bin/sh
      2 #
      3 # pash - simple password manager.
      4 # https://github.com/dylanaraps/pash
      5 
      6 pw_add() {
      7     name=$1
      8 
      9     if yn "Generate a password?"; then
     10         # Generate a password by reading '/dev/urandom' with the
     11         # 'tr' command to translate the random bytes into a
     12         # configurable character set.
     13         #
     14         # The 'dd' command is then used to read only the desired
     15         # password length.
     16         #
     17         # Regarding usage of '/dev/urandom' instead of '/dev/random'.
     18         # See: https://www.2uo.de/myths-about-urandom
     19         pass=$(LC_ALL=C tr -dc "${PASH_PATTERN:-_A-Z-a-z-0-9}" < /dev/urandom |
     20             dd ibs=1 obs=1 count="${PASH_LENGTH:-50}" 2>/dev/null)
     21 
     22     else
     23         # 'sread()' is a simple wrapper function around 'read'
     24         # to prevent user input from being printed to the terminal.
     25         sread pass  "Enter password"
     26         sread pass2 "Enter password (again)"
     27 
     28         # Disable this check as we dynamically populate the two
     29         # passwords using the 'sread()' function.
     30         # shellcheck disable=2154
     31         [ "$pass" = "$pass2" ] || die "Passwords do not match"
     32     fi
     33 
     34     [ "$pass" ] || die "Failed to generate a password"
     35 
     36     # Mimic the use of an array for storing arguments by... using
     37     # the function's argument list. This is very apt isn't it?
     38     if [ "$PASH_KEYID" ]; then
     39         set -- --trust-model always -aer "$PASH_KEYID"
     40     else
     41         set -- -c
     42     fi
     43 
     44     # Use 'gpg' to store the password in an encrypted file.
     45     # A heredoc is used here instead of a 'printf' to avoid
     46     # leaking the password through the '/proc' filesystem.
     47     #
     48     # Heredocs are sometimes implemented via temporary files,
     49     # however this is typically done using 'mkstemp()' which
     50     # is more secure than a leak in '/proc'.
     51     "$gpg" "$@" -o "$name.gpg" <<-EOF &&
     52 		$pass
     53 	EOF
     54     printf '%s\n' "Saved '$name' to the store."
     55 }
     56 
     57 pw_del() {
     58     yn "Delete pass file '$1'?" && {
     59         rm -f "$1.gpg"
     60 
     61         # Remove empty parent directories of a password
     62         # entry. It's fine if this fails as it means that
     63         # another entry also lives in the same directory.
     64         rmdir -p "${1%/*}" 2>/dev/null || :
     65     }
     66 }
     67 
     68 pw_show() {
     69     "$gpg" -dq "$1.gpg"
     70 }
     71 
     72 pw_copy() {
     73     # Disable warning against word-splitting as it is safe
     74     # and intentional (globbing is disabled).
     75     # shellcheck disable=2086
     76     : "${PASH_CLIP:=xclip -sel c}"
     77 
     78     # Wait in the background for the password timeout and
     79     # clear the clipboard when the timer runs out.
     80     #
     81     # If the 'sleep' fails, kill the script. This is the
     82     # simplest method of aborting from a subshell.
     83     [ "$PASH_TIMEOUT" != off ] && {
     84         printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}"
     85 
     86         sleep "$PASH_TIMEOUT" || kill 0
     87         $PASH_CLIP </dev/null
     88     } &
     89 
     90     pw_show "$1" | $PASH_CLIP
     91 }
     92 
     93 pw_list() {
     94     find . -type f -name \*.gpg | sed 's/..//;s/\.gpg$//'
     95 }
     96 
     97 pw_tree() {
     98 	# This is a simple 'tree' implementation based around
     99 	# Minus the 'find' and 'sort' calls to grab the recursive
    100 	# file list, this is entirely shell!
    101 	items=$(find . -mindepth 1 | sort)
    102 	items_count=$(echo "$items" | wc -l)
    103 	count=1
    104 	echo "$items" | while read -r item; do
    105 		glob "$item" './.*' && continue
    106 
    107 		path=${item##./}
    108 		path=${path%%.gpg}
    109 
    110 
    111 		[ -d "$item" ] && suffix=/ || suffix=
    112 
    113 		# Split the string on each forward slash to count the
    114 		# "nest level" of each file and directory.
    115 		#
    116 		# The shift is added as the resulting count will always
    117 		# be one higher than intended.
    118 		#
    119 		# Disable warning about word splitting as it is
    120 		# intentional here and globbing is disabled.
    121 		# shellcheck disable=2086
    122 		{
    123 			IFS=/
    124 			set -- $path
    125 			shift
    126 		}
    127 
    128 		# Print the dividers from 1-nest_level. This is a simple
    129 		# iteration over the shifted argument list.
    130 		for _; do printf '│  '; done
    131 
    132 		[ "$count" -eq "$items_count" ] && {
    133 			printf '└── %s\n' "${path##*/}${suffix}"
    134 			break
    135 		}
    136 		printf '├── %s\n' "${path##*/}${suffix}"
    137 		count=$((count+1))
    138 	done
    139 }
    140 
    141 yn() {
    142     printf '%s [y/n]: ' "$1"
    143 
    144     # Enable raw input to allow for a single byte to be read from
    145     # stdin without needing to wait for the user to press Return.
    146     stty -icanon
    147 
    148     # Read a single byte from stdin using 'dd'. POSIX 'read' has
    149     # no support for single/'N' byte based input from the user.
    150     answer=$(dd ibs=1 count=1 2>/dev/null)
    151 
    152     # Disable raw input, leaving the terminal how we *should*
    153     # have found it.
    154     stty icanon
    155 
    156     printf '\n'
    157 
    158     # Handle the answer here directly, enabling this function's
    159     # return status to be used in place of checking for '[yY]'
    160     # throughout this program.
    161     glob "$answer" '[yY]'
    162 }
    163 
    164 sread() {
    165     printf '%s: ' "$2"
    166 
    167     # Disable terminal printing while the user inputs their
    168     # password. POSIX 'read' has no '-s' flag which would
    169     # effectively do the same thing.
    170     stty -echo
    171     read -r "$1"
    172     stty echo
    173 
    174     printf '\n'
    175 }
    176 
    177 glob() {
    178     # This is a simple wrapper around a case statement to allow
    179     # for simple string comparisons against globs.
    180     #
    181     # Example: if glob "Hello World" '* World'; then
    182     #
    183     # Disable this warning as it is the intended behavior.
    184     # shellcheck disable=2254
    185     case $1 in $2) return 0; esac; return 1
    186 }
    187 
    188 die() {
    189     printf 'error: %s.\n' "$1" >&2
    190     exit 1
    191 }
    192 
    193 usage() { printf %s "\
    194 pash 2.3.0 - simple password manager.
    195 
    196 => [a]dd  [name] - Create a new password entry.
    197 => [c]opy [name] - Copy entry to the clipboard.
    198 => [d]el  [name] - Delete a password entry.
    199 => [l]ist        - List all entries.
    200 => [s]how [name] - Show password for an entry.
    201 => [t]ree        - List all entries in a tree.
    202 
    203 Using a key pair:  export PASH_KEYID=XXXXXXXX
    204 Password length:   export PASH_LENGTH=50
    205 Password pattern:  export PASH_PATTERN=_A-Z-a-z-0-9
    206 Store location:    export PASH_DIR=~/.local/share/pash
    207 Clipboard tool:    export PASH_CLIP='xclip -sel c'
    208 Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable)
    209 "
    210 exit 0
    211 }
    212 
    213 main() {
    214     : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
    215 
    216     # Look for both 'gpg' and 'gpg2',
    217     # preferring 'gpg2' if it is available.
    218     command -v gpg  >/dev/null 2>&1 && gpg=gpg
    219     command -v gpg2 >/dev/null 2>&1 && gpg=gpg2
    220 
    221     [ "$gpg" ] ||
    222         die "GPG not found"
    223 
    224     mkdir -p "$PASH_DIR" ||
    225         die "Couldn't create password directory"
    226 
    227     cd "$PASH_DIR" ||
    228         die "Can't access password directory"
    229 
    230     glob "$1" '[acds]*' && [ -z "$2" ] &&
    231         die "Missing [name] argument"
    232 
    233     glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] &&
    234         die "Pass file '$2' doesn't exist"
    235 
    236     glob "$1" 'a*' && [ -f "$2.gpg" ] &&
    237         die "Pass file '$2' already exists"
    238 
    239     glob "$2" '*/*' && glob "$2" '*../*' &&
    240         die "Category went out of bounds"
    241 
    242     glob "$2" '/*' &&
    243         die "Category can't start with '/'"
    244 
    245     glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
    246         die "Couldn't create category '${2%/*}'"; }
    247 
    248     # Set 'GPG_TTY' to the current 'TTY' if it
    249     # is unset. Fixes a somewhat rare `gpg` issue.
    250     export GPG_TTY=${GPG_TTY:-$(tty)}
    251 
    252     # Restrict permissions of any new files to
    253     # only the current user.
    254     umask 077
    255 
    256     # Ensure that we leave the terminal in a usable
    257     # state on exit or Ctrl+C.
    258     [ -t 1 ] && trap 'stty echo icanon' INT EXIT
    259 
    260     case $1 in
    261         a*) pw_add  "$2" ;;
    262         c*) pw_copy "$2" ;;
    263         d*) pw_del  "$2" ;;
    264         s*) pw_show "$2" ;;
    265         l*) pw_list ;;
    266         t*) pw_tree ;;
    267         *)  usage
    268     esac
    269 }
    270 
    271 # Ensure that debug mode is never enabled to
    272 # prevent the password from leaking.
    273 set +x
    274 
    275 # Ensure that globbing is globally disabled
    276 # to avoid insecurities with word-splitting.
    277 set -f
    278 
    279 [ "$1" ] || usage && main "$@"