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 "$@"