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