password-store.sh (26050B)
1 #!/usr/bin/env bash 2 3 # Copyright (C) 2012 - 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. 4 # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 6 umask "${PASSWORD_STORE_UMASK:-077}" 7 set -o pipefail 8 9 GPG_OPTS=( $PASSWORD_STORE_GPG_OPTS "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) 10 GPG="gpg" 11 export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}" 12 command -v gpg2 &>/dev/null && GPG="gpg2" 13 [[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" ) 14 15 PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" 16 EXTENSIONS="${PASSWORD_STORE_EXTENSIONS_DIR:-$PREFIX/.extensions}" 17 X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}" 18 CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}" 19 GENERATED_LENGTH="${PASSWORD_STORE_GENERATED_LENGTH:-25}" 20 CHARACTER_SET="${PASSWORD_STORE_CHARACTER_SET:-[:punct:][:alnum:]}" 21 CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}" 22 23 unset GIT_DIR GIT_WORK_TREE GIT_NAMESPACE GIT_INDEX_FILE GIT_INDEX_VERSION GIT_OBJECT_DIRECTORY GIT_COMMON_DIR 24 export GIT_CEILING_DIRECTORIES="$PREFIX/.." 25 26 # 27 # BEGIN helper functions 28 # 29 30 set_git() { 31 INNER_GIT_DIR="${1%/*}" 32 while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do 33 INNER_GIT_DIR="${INNER_GIT_DIR%/*}" 34 done 35 [[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR="" 36 } 37 git_add_file() { 38 [[ -n $INNER_GIT_DIR ]] || return 39 git -C "$INNER_GIT_DIR" add "$1" || return 40 [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$1") ]] || return 41 git_commit "$2" 42 } 43 git_commit() { 44 local sign="" 45 [[ -n $INNER_GIT_DIR ]] || return 46 [[ $(git -C "$INNER_GIT_DIR" config --bool --get pass.signcommits) == "true" ]] && sign="-S" 47 git -C "$INNER_GIT_DIR" commit $sign -m "$1" 48 } 49 yesno() { 50 [[ -t 0 ]] || return 0 51 local response 52 read -r -p "$1 [y/N] " response 53 [[ $response == [yY] ]] || exit 1 54 } 55 die() { 56 echo "$@" >&2 57 exit 1 58 } 59 verify_file() { 60 [[ -n $PASSWORD_STORE_SIGNING_KEY ]] || return 0 61 [[ -f $1.sig ]] || die "Signature for $1 does not exist." 62 local fingerprints="$($GPG $PASSWORD_STORE_GPG_OPTS --verify --status-fd=1 "$1.sig" "$1" 2>/dev/null | sed -n 's/^\[GNUPG:\] VALIDSIG \([A-F0-9]\{40\}\) .* \([A-F0-9]\{40\}\)$/\1\n\2/p')" 63 local fingerprint found=0 64 for fingerprint in $PASSWORD_STORE_SIGNING_KEY; do 65 [[ $fingerprint =~ ^[A-F0-9]{40}$ ]] || continue 66 [[ $fingerprints == *$fingerprint* ]] && { found=1; break; } 67 done 68 [[ $found -eq 1 ]] || die "Signature for $1 is invalid." 69 } 70 set_gpg_recipients() { 71 GPG_RECIPIENT_ARGS=( ) 72 GPG_RECIPIENTS=( ) 73 local gpg_id 74 75 if [[ -n $PASSWORD_STORE_KEY ]]; then 76 for gpg_id in $PASSWORD_STORE_KEY; do 77 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" ) 78 GPG_RECIPIENTS+=( "$gpg_id" ) 79 done 80 return 81 fi 82 83 local current="$PREFIX/$1" 84 while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do 85 current="${current%/*}" 86 done 87 current="$current/.gpg-id" 88 89 if [[ ! -f $current ]]; then 90 cat >&2 <<-_EOF 91 Error: You must run: 92 $PROGRAM init your-gpg-id 93 before you may use the password store. 94 95 _EOF 96 cmd_usage 97 exit 1 98 fi 99 100 verify_file "$current" 101 102 while read -r gpg_id; do 103 gpg_id="${gpg_id%%#*}" # strip comment 104 [[ -n $gpg_id ]] || continue 105 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" ) 106 GPG_RECIPIENTS+=( "$gpg_id" ) 107 done < "$current" 108 } 109 110 reencrypt_path() { 111 local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile 112 local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")" 113 while read -r -d "" passfile; do 114 [[ -L $passfile ]] && continue 115 local passfile_dir="${passfile%/*}" 116 passfile_dir="${passfile_dir#$PREFIX}" 117 passfile_dir="${passfile_dir#/}" 118 local passfile_display="${passfile#$PREFIX/}" 119 passfile_display="${passfile_display%.gpg}" 120 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--" 121 122 set_gpg_recipients "$passfile_dir" 123 if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then 124 for index in "${!GPG_RECIPIENTS[@]}"; do 125 local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)" 126 [[ -z $group ]] && continue 127 IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190 128 unset "GPG_RECIPIENTS[$index]" 129 done 130 gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/^sub:[^idr:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)" 131 fi 132 current_keys="$(LC_ALL=C $GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$passfile" 2>&1 | sed -nE 's/^gpg: public key is ([A-F0-9]+)$/\1/p' | LC_ALL=C sort -u)" 133 134 if [[ $gpg_keys != "$current_keys" ]]; then 135 echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }" 136 $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" && 137 mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp" 138 fi 139 prev_gpg_recipients="${GPG_RECIPIENTS[*]}" 140 done < <(find "$1" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0) 141 } 142 check_sneaky_paths() { 143 local path 144 for path in "$@"; do 145 [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home." 146 done 147 } 148 149 # 150 # END helper functions 151 # 152 153 # 154 # BEGIN platform definable 155 # 156 157 clip() { 158 if [[ -n $WAYLAND_DISPLAY ]] && command -v wl-copy &> /dev/null; then 159 local copy_cmd=( wl-copy ) 160 local paste_cmd=( wl-paste -n ) 161 if [[ $X_SELECTION == primary ]]; then 162 copy_cmd+=( --primary ) 163 paste_cmd+=( --primary ) 164 fi 165 local display_name="$WAYLAND_DISPLAY" 166 elif [[ -n $DISPLAY ]] && command -v xclip &> /dev/null; then 167 local copy_cmd=( xclip -selection "$X_SELECTION" ) 168 local paste_cmd=( xclip -o -selection "$X_SELECTION" ) 169 local display_name="$DISPLAY" 170 else 171 die "Error: No X11 or Wayland display and clipper detected" 172 fi 173 local sleep_argv0="password store sleep on display $display_name" 174 175 # This base64 business is because bash cannot store binary data in a shell 176 # variable. Specifically, it cannot store nulls nor (non-trivally) store 177 # trailing new lines. 178 pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 179 local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)" 180 echo -n "$1" | "${copy_cmd[@]}" || die "Error: Could not copy data to the clipboard" 181 ( 182 ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" ) 183 local now="$("${paste_cmd[@]}" | $BASE64)" 184 [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now" 185 186 # It might be nice to programatically check to see if klipper exists, 187 # as well as checking for other common clipboard managers. But for now, 188 # this works fine -- if qdbus isn't there or if klipper isn't running, 189 # this essentially becomes a no-op. 190 # 191 # Clipboard managers frequently write their history out in plaintext, 192 # so we axe it here: 193 qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null 194 195 echo "$before" | $BASE64 -d | "${copy_cmd[@]}" 196 ) >/dev/null 2>&1 & disown 197 echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds." 198 } 199 200 qrcode() { 201 if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then 202 if type feh >/dev/null 2>&1; then 203 echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 - 204 return 205 elif type gm >/dev/null 2>&1; then 206 echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 - 207 return 208 elif type display >/dev/null 2>&1; then 209 echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 - 210 return 211 fi 212 fi 213 echo -n "$1" | qrencode -t utf8 214 } 215 216 tmpdir() { 217 [[ -n $SECURE_TMPDIR ]] && return 218 local warn=1 219 [[ $1 == "nowarn" ]] && warn=0 220 local template="$PROGRAM.XXXXXXXXXXXXX" 221 if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then 222 SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")" 223 remove_tmpfile() { 224 rm -rf "$SECURE_TMPDIR" 225 } 226 trap remove_tmpfile EXIT 227 else 228 [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF 229 Your system does not have /dev/shm, which means that it may 230 be difficult to entirely erase the temporary non-encrypted 231 password file after editing. 232 233 Are you sure you would like to continue? 234 _EOF 235 )" 236 SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")" 237 shred_tmpfile() { 238 find "$SECURE_TMPDIR" -type f -exec $SHRED {} + 239 rm -rf "$SECURE_TMPDIR" 240 } 241 trap shred_tmpfile EXIT 242 fi 243 244 } 245 GETOPT="getopt" 246 SHRED="shred -f -z" 247 BASE64="base64" 248 249 source "$(dirname "$0")/platform/$(uname | cut -d _ -f 1 | tr '[:upper:]' '[:lower:]').sh" 2>/dev/null # PLATFORM_FUNCTION_FILE 250 251 # 252 # END platform definable 253 # 254 255 256 # 257 # BEGIN subcommand functions 258 # 259 260 cmd_version() { 261 cat <<-_EOF 262 ============================================ 263 = pass: the standard unix password manager = 264 = = 265 = v1.7.4 = 266 = = 267 = Jason A. Donenfeld = 268 = Jason@zx2c4.com = 269 = = 270 = http://www.passwordstore.org/ = 271 ============================================ 272 _EOF 273 } 274 275 cmd_usage() { 276 cmd_version 277 echo 278 cat <<-_EOF 279 Usage: 280 $PROGRAM init [--path=subfolder,-p subfolder] gpg-id... 281 Initialize new password storage and use gpg-id for encryption. 282 Selectively reencrypt existing passwords using new gpg-id. 283 $PROGRAM [ls] [subfolder] 284 List passwords. 285 $PROGRAM find pass-names... 286 List passwords that match pass-names. 287 $PROGRAM [show] [--clip[=line-number],-c[line-number]] pass-name 288 Show existing password and optionally put it on the clipboard. 289 If put on the clipboard, it will be cleared in $CLIP_TIME seconds. 290 $PROGRAM grep [GREPOPTIONS] search-string 291 Search for password files containing search-string when decrypted. 292 $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name 293 Insert new password. Optionally, echo the password back to the console 294 during entry. Or, optionally, the entry may be multiline. Prompt before 295 overwriting existing password unless forced. 296 $PROGRAM edit pass-name 297 Insert a new password or edit an existing password using ${EDITOR:-vi}. 298 $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length] 299 Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols. 300 Optionally put it on the clipboard and clear board after $CLIP_TIME seconds. 301 Prompt before overwriting existing password unless forced. 302 Optionally replace only the first line of an existing file with a new password. 303 $PROGRAM rm [--recursive,-r] [--force,-f] pass-name 304 Remove existing password or directory, optionally forcefully. 305 $PROGRAM mv [--force,-f] old-path new-path 306 Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting. 307 $PROGRAM cp [--force,-f] old-path new-path 308 Copies old-path to new-path, optionally forcefully, selectively reencrypting. 309 $PROGRAM git git-command-args... 310 If the password store is a git repository, execute a git command 311 specified by git-command-args. 312 $PROGRAM help 313 Show this text. 314 $PROGRAM version 315 Show version information. 316 317 More information may be found in the pass(1) man page. 318 _EOF 319 } 320 321 cmd_init() { 322 local opts id_path="" 323 opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")" 324 local err=$? 325 eval set -- "$opts" 326 while true; do case $1 in 327 -p|--path) id_path="$2"; shift 2 ;; 328 --) shift; break ;; 329 esac done 330 331 [[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..." 332 [[ -n $id_path ]] && check_sneaky_paths "$id_path" 333 [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory." 334 335 local gpg_id="$PREFIX/$id_path/.gpg-id" 336 set_git "$gpg_id" 337 338 if [[ $# -eq 1 && -z $1 ]]; then 339 [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed." 340 rm -v -f "$gpg_id" || exit 1 341 if [[ -n $INNER_GIT_DIR ]]; then 342 git -C "$INNER_GIT_DIR" rm -qr "$gpg_id" 343 git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}." 344 fi 345 rmdir -p "${gpg_id%/*}" 2>/dev/null 346 else 347 mkdir -v -p "$PREFIX/$id_path" 348 printf "%s\n" "$@" > "$gpg_id" 349 local id_print="$(printf "%s, " "$@")" 350 echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}" 351 git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}." 352 if [[ -n $PASSWORD_STORE_SIGNING_KEY ]]; then 353 local signing_keys=( ) key 354 for key in $PASSWORD_STORE_SIGNING_KEY; do 355 signing_keys+=( --default-key $key ) 356 done 357 $GPG "${GPG_OPTS[@]}" "${signing_keys[@]}" --detach-sign "$gpg_id" || die "Could not sign .gpg_id." 358 key="$($GPG "${GPG_OPTS[@]}" --verify --status-fd=1 "$gpg_id.sig" "$gpg_id" 2>/dev/null | sed -n 's/^\[GNUPG:\] VALIDSIG [A-F0-9]\{40\} .* \([A-F0-9]\{40\}\)$/\1/p')" 359 [[ -n $key ]] || die "Signing of .gpg_id unsuccessful." 360 git_add_file "$gpg_id.sig" "Signing new GPG id with ${key//[$IFS]/,}." 361 fi 362 fi 363 364 reencrypt_path "$PREFIX/$id_path" 365 git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}." 366 } 367 368 cmd_show() { 369 local opts selected_line clip=0 qrcode=0 370 opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")" 371 local err=$? 372 eval set -- "$opts" 373 while true; do case $1 in 374 -q|--qrcode) qrcode=1; selected_line="${2:-1}"; shift 2 ;; 375 -c|--clip) clip=1; selected_line="${2:-1}"; shift 2 ;; 376 --) shift; break ;; 377 esac done 378 379 [[ $err -ne 0 || ( $qrcode -eq 1 && $clip -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--clip[=line-number],-c[line-number]] [--qrcode[=line-number],-q[line-number]] [pass-name]" 380 381 local pass 382 local path="$1" 383 local passfile="$PREFIX/$path.gpg" 384 check_sneaky_paths "$path" 385 if [[ -f $passfile ]]; then 386 if [[ $clip -eq 0 && $qrcode -eq 0 ]]; then 387 pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | $BASE64)" || exit $? 388 echo "$pass" | $BASE64 -d 389 else 390 [[ $selected_line =~ ^[0-9]+$ ]] || die "Clip location '$selected_line' is not a number." 391 pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +${selected_line} | head -n 1)" || exit $? 392 [[ -n $pass ]] || die "There is no password to put on the clipboard at line ${selected_line}." 393 if [[ $clip -eq 1 ]]; then 394 clip "$pass" "$path" 395 elif [[ $qrcode -eq 1 ]]; then 396 qrcode "$pass" "$path" 397 fi 398 fi 399 elif [[ -d $PREFIX/$path ]]; then 400 if [[ -z $path ]]; then 401 echo "Password Store" 402 else 403 echo "${path%\/}" 404 fi 405 tree -N -C -l --noreport "$PREFIX/$path" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' # remove .gpg at end of line, but keep colors 406 elif [[ -z $path ]]; then 407 die "Error: password store is empty. Try \"pass init\"." 408 else 409 die "Error: $path is not in the password store." 410 fi 411 } 412 413 cmd_find() { 414 [[ $# -eq 0 ]] && die "Usage: $PROGRAM $COMMAND pass-names..." 415 IFS="," eval 'echo "Search Terms: $*"' 416 local terms="*$(printf '%s*|*' "$@")" 417 tree -N -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' 418 } 419 420 cmd_grep() { 421 [[ $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [GREPOPTIONS] search-string" 422 local passfile grepresults 423 while read -r -d "" passfile; do 424 grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$@")" 425 [[ $? -ne 0 ]] && continue 426 passfile="${passfile%.gpg}" 427 passfile="${passfile#$PREFIX/}" 428 local passfile_dir="${passfile%/*}/" 429 [[ $passfile_dir == "${passfile}/" ]] && passfile_dir="" 430 passfile="${passfile##*/}" 431 printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile" 432 echo "$grepresults" 433 done < <(find -L "$PREFIX" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0) 434 } 435 436 cmd_insert() { 437 local opts multiline=0 noecho=1 force=0 438 opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")" 439 local err=$? 440 eval set -- "$opts" 441 while true; do case $1 in 442 -m|--multiline) multiline=1; shift ;; 443 -e|--echo) noecho=0; shift ;; 444 -f|--force) force=1; shift ;; 445 --) shift; break ;; 446 esac done 447 448 [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name" 449 local path="${1%/}" 450 local passfile="$PREFIX/$path.gpg" 451 check_sneaky_paths "$path" 452 set_git "$passfile" 453 454 [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" 455 456 mkdir -p -v "$PREFIX/$(dirname -- "$path")" 457 set_gpg_recipients "$(dirname -- "$path")" 458 459 if [[ $multiline -eq 1 ]]; then 460 echo "Enter contents of $path and press Ctrl+D when finished:" 461 echo 462 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 463 elif [[ $noecho -eq 1 ]]; then 464 local password password_again 465 while true; do 466 read -r -p "Enter password for $path: " -s password || exit 1 467 echo 468 read -r -p "Retype password for $path: " -s password_again || exit 1 469 echo 470 if [[ $password == "$password_again" ]]; then 471 echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 472 break 473 else 474 die "Error: the entered passwords do not match." 475 fi 476 done 477 else 478 local password 479 read -r -p "Enter password for $path: " -e password 480 echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 481 fi 482 git_add_file "$passfile" "Add given password for $path to store." 483 } 484 485 cmd_edit() { 486 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name" 487 488 local path="${1%/}" 489 check_sneaky_paths "$path" 490 mkdir -p -v "$PREFIX/$(dirname -- "$path")" 491 set_gpg_recipients "$(dirname -- "$path")" 492 local passfile="$PREFIX/$path.gpg" 493 set_git "$passfile" 494 495 tmpdir #Defines $SECURE_TMPDIR 496 local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt" 497 498 local action="Add" 499 if [[ -f $passfile ]]; then 500 $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1 501 action="Edit" 502 fi 503 ${EDITOR:-vi} "$tmp_file" 504 [[ -f $tmp_file ]] || die "New password not saved." 505 $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged." 506 while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do 507 yesno "GPG encryption failed. Would you like to try again?" 508 done 509 git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}." 510 } 511 512 cmd_generate() { 513 local opts qrcode=0 clip=0 force=0 characters="$CHARACTER_SET" inplace=0 pass 514 opts="$($GETOPT -o nqcif -l no-symbols,qrcode,clip,in-place,force -n "$PROGRAM" -- "$@")" 515 local err=$? 516 eval set -- "$opts" 517 while true; do case $1 in 518 -n|--no-symbols) characters="$CHARACTER_SET_NO_SYMBOLS"; shift ;; 519 -q|--qrcode) qrcode=1; shift ;; 520 -c|--clip) clip=1; shift ;; 521 -f|--force) force=1; shift ;; 522 -i|--in-place) inplace=1; shift ;; 523 --) shift; break ;; 524 esac done 525 526 [[ $err -ne 0 || ( $# -ne 2 && $# -ne 1 ) || ( $force -eq 1 && $inplace -eq 1 ) || ( $qrcode -eq 1 && $clip -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--qrcode,-q] [--in-place,-i | --force,-f] pass-name [pass-length]" 527 local path="$1" 528 local length="${2:-$GENERATED_LENGTH}" 529 check_sneaky_paths "$path" 530 [[ $length =~ ^[0-9]+$ ]] || die "Error: pass-length \"$length\" must be a number." 531 [[ $length -gt 0 ]] || die "Error: pass-length must be greater than zero." 532 mkdir -p -v "$PREFIX/$(dirname -- "$path")" 533 set_gpg_recipients "$(dirname -- "$path")" 534 local passfile="$PREFIX/$path.gpg" 535 set_git "$passfile" 536 537 [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" 538 539 read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom) 540 [[ ${#pass} -eq $length ]] || die "Could not generate password from /dev/urandom." 541 if [[ $inplace -eq 0 ]]; then 542 echo "$pass" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 543 else 544 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--" 545 if { echo "$pass"; $GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +2; } | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then 546 mv "$passfile_temp" "$passfile" 547 else 548 rm -f "$passfile_temp" 549 die "Could not reencrypt new password." 550 fi 551 fi 552 local verb="Add" 553 [[ $inplace -eq 1 ]] && verb="Replace" 554 git_add_file "$passfile" "$verb generated password for ${path}." 555 556 if [[ $clip -eq 1 ]]; then 557 clip "$pass" "$path" 558 elif [[ $qrcode -eq 1 ]]; then 559 qrcode "$pass" "$path" 560 else 561 printf "\e[1mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass" 562 fi 563 } 564 565 cmd_delete() { 566 local opts recursive="" force=0 567 opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")" 568 local err=$? 569 eval set -- "$opts" 570 while true; do case $1 in 571 -r|--recursive) recursive="-r"; shift ;; 572 -f|--force) force=1; shift ;; 573 --) shift; break ;; 574 esac done 575 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name" 576 local path="$1" 577 check_sneaky_paths "$path" 578 579 local passdir="$PREFIX/${path%/}" 580 local passfile="$PREFIX/$path.gpg" 581 [[ -f $passfile && -d $passdir && $path == */ || ! -f $passfile ]] && passfile="${passdir%/}/" 582 [[ -e $passfile ]] || die "Error: $path is not in the password store." 583 set_git "$passfile" 584 585 [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?" 586 587 rm $recursive -f -v "$passfile" 588 set_git "$passfile" 589 if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then 590 git -C "$INNER_GIT_DIR" rm -qr "$passfile" 591 set_git "$passfile" 592 git_commit "Remove $path from store." 593 fi 594 rmdir -p "${passfile%/*}" 2>/dev/null 595 } 596 597 cmd_copy_move() { 598 local opts move=1 force=0 599 [[ $1 == "copy" ]] && move=0 600 shift 601 opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" 602 local err=$? 603 eval set -- "$opts" 604 while true; do case $1 in 605 -f|--force) force=1; shift ;; 606 --) shift; break ;; 607 esac done 608 [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path" 609 check_sneaky_paths "$@" 610 local old_path="$PREFIX/${1%/}" 611 local old_dir="$old_path" 612 local new_path="$PREFIX/$2" 613 614 if ! [[ -f $old_path.gpg && -d $old_path && $1 == */ || ! -f $old_path.gpg ]]; then 615 old_dir="${old_path%/*}" 616 old_path="${old_path}.gpg" 617 fi 618 echo "$old_path" 619 [[ -e $old_path ]] || die "Error: $1 is not in the password store." 620 621 mkdir -p -v "${new_path%/*}" 622 [[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg" 623 624 local interactive="-i" 625 [[ ! -t 0 || $force -eq 1 ]] && interactive="-f" 626 627 set_git "$new_path" 628 if [[ $move -eq 1 ]]; then 629 mv $interactive -v "$old_path" "$new_path" || exit 1 630 [[ -e "$new_path" ]] && reencrypt_path "$new_path" 631 632 set_git "$new_path" 633 if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then 634 git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null 635 set_git "$new_path" 636 git_add_file "$new_path" "Rename ${1} to ${2}." 637 fi 638 set_git "$old_path" 639 if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then 640 git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null 641 set_git "$old_path" 642 [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}." 643 fi 644 rmdir -p "$old_dir" 2>/dev/null 645 else 646 cp $interactive -r -v "$old_path" "$new_path" || exit 1 647 [[ -e "$new_path" ]] && reencrypt_path "$new_path" 648 git_add_file "$new_path" "Copy ${1} to ${2}." 649 fi 650 } 651 652 cmd_git() { 653 set_git "$PREFIX/" 654 if [[ $1 == "init" ]]; then 655 INNER_GIT_DIR="$PREFIX" 656 git -C "$INNER_GIT_DIR" "$@" || exit 1 657 git_add_file "$PREFIX" "Add current contents of password store." 658 659 echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes" 660 git_add_file .gitattributes "Configure git repository for gpg file diff." 661 git -C "$INNER_GIT_DIR" config --local diff.gpg.binary true 662 git -C "$INNER_GIT_DIR" config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}" 663 elif [[ -n $INNER_GIT_DIR ]]; then 664 tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files. 665 export TMPDIR="$SECURE_TMPDIR" 666 git -C "$INNER_GIT_DIR" "$@" 667 else 668 die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"." 669 fi 670 } 671 672 cmd_extension_or_show() { 673 if ! cmd_extension "$@"; then 674 COMMAND="show" 675 cmd_show "$@" 676 fi 677 } 678 679 SYSTEM_EXTENSION_DIR="" 680 cmd_extension() { 681 check_sneaky_paths "$1" 682 local user_extension system_extension extension 683 [[ -n $SYSTEM_EXTENSION_DIR ]] && system_extension="$SYSTEM_EXTENSION_DIR/$1.bash" 684 [[ $PASSWORD_STORE_ENABLE_EXTENSIONS == true ]] && user_extension="$EXTENSIONS/$1.bash" 685 if [[ -n $user_extension && -f $user_extension && -x $user_extension ]]; then 686 verify_file "$user_extension" 687 extension="$user_extension" 688 elif [[ -n $system_extension && -f $system_extension && -x $system_extension ]]; then 689 extension="$system_extension" 690 else 691 return 1 692 fi 693 shift 694 source "$extension" "$@" 695 return 0 696 } 697 698 # 699 # END subcommand functions 700 # 701 702 PROGRAM="${0##*/}" 703 COMMAND="$1" 704 705 case "$1" in 706 init) shift; cmd_init "$@" ;; 707 help|--help) shift; cmd_usage "$@" ;; 708 version|--version) shift; cmd_version "$@" ;; 709 show|ls|list) shift; cmd_show "$@" ;; 710 find|search) shift; cmd_find "$@" ;; 711 grep) shift; cmd_grep "$@" ;; 712 insert|add) shift; cmd_insert "$@" ;; 713 edit) shift; cmd_edit "$@" ;; 714 generate) shift; cmd_generate "$@" ;; 715 delete|rm|remove) shift; cmd_delete "$@" ;; 716 rename|mv) shift; cmd_copy_move "move" "$@" ;; 717 copy|cp) shift; cmd_copy_move "copy" "$@" ;; 718 git) shift; cmd_git "$@" ;; 719 *) cmd_extension_or_show "$@" ;; 720 esac 721 exit 0