rice

personal dot files and scripts for linux and macOS
Log | Files | Refs | README | LICENSE

pash (6309B)


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