3 # Copyright (C) 2012 - 2017 Jason A. Donenfeld <
[email protected]>. All Rights Reserved.
4 # This file is licensed under the GPLv2+. Please see COPYING for more information.
6 umask "${PASSWORD_STORE_UMASK:-077}"
9 GPG_OPTS=( $PASSWORD_STORE_GPG_OPTS "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
11 export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
12 which gpg2 &>/dev/null && GPG="gpg2"
13 [[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
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:-[:graph:]}"
21 CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}"
23 export GIT_CEILING_DIRECTORIES="$PREFIX/.."
26 # BEGIN helper functions
30 INNER_GIT_DIR="${1%/*}"
31 while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do
32 INNER_GIT_DIR="${INNER_GIT_DIR%/*}"
34 [[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR=""
37 [[ -n $INNER_GIT_DIR ]] || return
38 git -C "$INNER_GIT_DIR" add "$1" || return
39 [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$1") ]] || return
44 [[ -n $INNER_GIT_DIR ]] || return
45 [[ $(git -C "$INNER_GIT_DIR" config --bool --get pass.signcommits) == "true" ]] && sign="-S"
46 git -C "$INNER_GIT_DIR" commit $sign -m "$1"
49 [[ -t 0 ]] || return 0
51 read -r -p "$1 [y/N] " response
52 [[ $response == [yY] ]] || exit 1
59 [[ -n $PASSWORD_STORE_SIGNING_KEY ]] || return 0
60 [[ -f $1.sig ]] || die "Signature for $1 does not exist."
61 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')"
62 local fingerprint found=0
63 for fingerprint in $PASSWORD_STORE_SIGNING_KEY; do
64 [[ $fingerprint =~ ^[A-F0-9]{40}$ ]] || continue
65 [[ $fingerprints == *$fingerprint* ]] && { found=1; break; }
67 [[ $found -eq 1 ]] || die "Signature for $1 is invalid."
69 set_gpg_recipients() {
70 GPG_RECIPIENT_ARGS=( )
73 if [[ -n $PASSWORD_STORE_KEY ]]; then
74 for gpg_id in $PASSWORD_STORE_KEY; do
75 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
76 GPG_RECIPIENTS+=( "$gpg_id" )
81 local current="$PREFIX/$1"
82 while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
83 current="${current%/*}"
85 current="$current/.gpg-id"
87 if [[ ! -f $current ]]; then
90 $PROGRAM init your-gpg-id
91 before you may use the password store.
98 verify_file "$current"
101 while read -r gpg_id; do
102 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
103 GPG_RECIPIENTS+=( "$gpg_id" )
108 local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile
109 local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")"
110 while read -r -d "" passfile; do
111 local passfile_dir="${passfile%/*}"
112 passfile_dir="${passfile_dir#$PREFIX}"
113 passfile_dir="${passfile_dir#/}"
114 local passfile_display="${passfile#$PREFIX/}"
115 passfile_display="${passfile_display%.gpg}"
116 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
118 set_gpg_recipients "$passfile_dir"
119 if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
120 for index in "${!GPG_RECIPIENTS[@]}"; do
121 local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)"
122 [[ -z $group ]] && continue
123 IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
124 unset GPG_RECIPIENTS[$index]
126 gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/sub:[^:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)"
128 current_keys="$($GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --list-only --keyid-format long "$passfile" 2>&1 | cut -d ' ' -f 5 | LC_ALL=C sort -u)"
130 if [[ $gpg_keys != "$current_keys" ]]; then
131 echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
132 $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
133 mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
135 prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
136 done < <(find "$1" -path '*/.git' -prune -o -iname '*.gpg' -print0)
138 check_sneaky_paths() {
141 [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
146 # END helper functions
150 # BEGIN platform definable
154 # This base64 business is because bash cannot store binary data in a shell
155 # variable. Specifically, it cannot store nulls nor (non-trivally) store
156 # trailing new lines.
157 local sleep_argv0="password store sleep on display $DISPLAY"
158 pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
159 local before="$(xclip -o -selection "$X_SELECTION" 2>/dev/null | base64)"
160 echo -n "$1" | xclip -selection "$X_SELECTION" || die "Error: Could not copy data to the clipboard"
162 ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" )
163 local now="$(xclip -o -selection "$X_SELECTION" | base64)"
164 [[ $now != $(echo -n "$1" | base64) ]] && before="$now"
166 # It might be nice to programatically check to see if klipper exists,
167 # as well as checking for other common clipboard managers. But for now,
168 # this works fine -- if qdbus isn't there or if klipper isn't running,
169 # this essentially becomes a no-op.
171 # Clipboard managers frequently write their history out in plaintext,
173 qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null
175 echo "$before" | base64 -d | xclip -selection "$X_SELECTION"
176 ) 2>/dev/null & disown
177 echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
181 if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then
182 if type feh >/dev/null 2>&1; then
183 echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 -
185 elif type gm >/dev/null 2>&1; then
186 echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 -
188 elif type display >/dev/null 2>&1; then
189 echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 -
193 echo -n "$1" | qrencode -t utf8
197 [[ -n $SECURE_TMPDIR ]] && return
199 [[ $1 == "nowarn" ]] && warn=0
200 local template="$PROGRAM.XXXXXXXXXXXXX"
201 if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
202 SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")"
204 rm -rf "$SECURE_TMPDIR"
206 trap remove_tmpfile INT TERM EXIT
208 [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF
209 Your system does not have /dev/shm, which means that it may
210 be difficult to entirely erase the temporary non-encrypted
211 password file after editing.
213 Are you sure you would like to continue?
216 SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
218 find "$SECURE_TMPDIR" -type f -exec $SHRED {} +
219 rm -rf "$SECURE_TMPDIR"
221 trap shred_tmpfile INT TERM EXIT
230 # END platform definable
235 # BEGIN subcommand functions
240 ============================================
241 = pass: the standard unix password manager =
245 = Jason A. Donenfeld =
248 = http://www.passwordstore.org/ =
249 ============================================
258 $PROGRAM init [--path=subfolder,-p subfolder] gpg-id...
259 Initialize new password storage and use gpg-id for encryption.
260 Selectively reencrypt existing passwords using new gpg-id.
261 $PROGRAM [ls] [subfolder]
263 $PROGRAM find pass-names...
264 List passwords that match pass-names.
265 $PROGRAM [show] [--clip[=line-number],-c[line-number]] pass-name
266 Show existing password and optionally put it on the clipboard.
267 If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
268 $PROGRAM grep search-string
269 Search for password files containing search-string when decrypted.
270 $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
271 Insert new password. Optionally, echo the password back to the console
272 during entry. Or, optionally, the entry may be multiline. Prompt before
273 overwriting existing password unless forced.
274 $PROGRAM edit pass-name
275 Insert a new password or edit an existing password using ${EDITOR:-vi}.
276 $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]
277 Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols.
278 Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
279 Prompt before overwriting existing password unless forced.
280 Optionally replace only the first line of an existing file with a new password.
281 $PROGRAM rm [--recursive,-r] [--force,-f] pass-name
282 Remove existing password or directory, optionally forcefully.
283 $PROGRAM mv [--force,-f] old-path new-path
284 Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
285 $PROGRAM cp [--force,-f] old-path new-path
286 Copies old-path to new-path, optionally forcefully, selectively reencrypting.
287 $PROGRAM git git-command-args...
288 If the password store is a git repository, execute a git command
289 specified by git-command-args.
293 Show version information.
295 More information may be found in the pass(1) man page.
300 local opts id_path=""
301 opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")"
304 while true; do case $1 in
305 -p|--path) id_path="$2"; shift 2 ;;
309 [[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..."
310 [[ -n $id_path ]] && check_sneaky_paths "$id_path"
311 [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory."
313 local gpg_id="$PREFIX/$id_path/.gpg-id"
316 if [[ $# -eq 1 && -z $1 ]]; then
317 [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
318 rm -v -f "$gpg_id" || exit 1
319 if [[ -n $INNER_GIT_DIR ]]; then
320 git -C "$INNER_GIT_DIR" rm -qr "$gpg_id"
321 git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}."
323 rmdir -p "${gpg_id%/*}" 2>/dev/null
325 mkdir -v -p "$PREFIX/$id_path"
326 printf "%s\n" "$@" > "$gpg_id"
327 local id_print="$(printf "%s, " "$@")"
328 echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}"
329 git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}."
330 if [[ -n $PASSWORD_STORE_SIGNING_KEY ]]; then
331 local signing_keys=( ) key
332 for key in $PASSWORD_STORE_SIGNING_KEY; do
333 signing_keys+=( --default-key $key )
335 gpg "${GPG_OPTS[@]}" "${signing_keys[@]}" --detach-sign "$gpg_id" || die "Could not sign .gpg_id."
336 key="$(gpg --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')"
337 [[ -n $key ]] || die "Signing of .gpg_id unsuccessful."
338 git_add_file "$gpg_id.sig" "Signing new GPG id with ${key//[$IFS]/,}."
342 reencrypt_path "$PREFIX/$id_path"
343 git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}."
347 local opts selected_line clip=0 qrcode=0
348 opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")"
351 while true; do case $1 in
352 -q|--qrcode) qrcode=1; selected_line="${2:-1}"; shift 2 ;;
353 -c|--clip) clip=1; selected_line="${2:-1}"; shift 2 ;;
357 [[ $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]"
360 local passfile="$PREFIX/$path.gpg"
361 check_sneaky_paths "$path"
362 if [[ -f $passfile ]]; then
363 if [[ $clip -eq 0 && $qrcode -eq 0 ]]; then
364 $GPG -d "${GPG_OPTS[@]}" "$passfile" || exit $?
366 [[ $selected_line =~ ^[0-9]+$ ]] || die "Clip location '$selected_line' is not a number."
367 local pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +${selected_line} | head -n 1)"
368 [[ -n $pass ]] || die "There is no password to put on the clipboard at line ${selected_line}."
369 if [[ $clip -eq 1 ]]; then
371 elif [[ $qrcode -eq 1 ]]; then
372 qrcode "$pass" "$path"
375 elif [[ -d $PREFIX/$path ]]; then
376 if [[ -z $path ]]; then
377 echo "Password Store"
381 tree -C -l --noreport "$PREFIX/$path" | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' # remove .gpg at end of line, but keep colors
382 elif [[ -z $path ]]; then
383 die "Error: password store is empty. Try \"pass init\"."
385 die "Error: $path is not in the password store."
390 [[ $# -eq 0 ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
391 IFS="," eval 'echo "Search Terms: $*"'
392 local terms="*$(printf '%s*|*' "$@")"
393 tree -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g'
397 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND search-string"
398 local search="$1" passfile grepresults
399 while read -r -d "" passfile; do
400 grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$search")"
401 [[ $? -ne 0 ]] && continue
402 passfile="${passfile%.gpg}"
403 passfile="${passfile#$PREFIX/}"
404 local passfile_dir="${passfile%/*}/"
405 [[ $passfile_dir == "${passfile}/" ]] && passfile_dir=""
406 passfile="${passfile##*/}"
407 printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
409 done < <(find -L "$PREFIX" -path '*/.git' -prune -o -iname '*.gpg' -print0)
413 local opts multiline=0 noecho=1 force=0
414 opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")"
417 while true; do case $1 in
418 -m|--multiline) multiline=1; shift ;;
419 -e|--echo) noecho=0; shift ;;
420 -f|--force) force=1; shift ;;
424 [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
426 local passfile="$PREFIX/$path.gpg"
427 check_sneaky_paths "$path"
430 [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
432 mkdir -p -v "$PREFIX/$(dirname "$path")"
433 set_gpg_recipients "$(dirname "$path")"
435 if [[ $multiline -eq 1 ]]; then
436 echo "Enter contents of $path and press Ctrl+D when finished:"
438 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
439 elif [[ $noecho -eq 1 ]]; then
440 local password password_again
442 read -r -p "Enter password for $path: " -s password || exit 1
444 read -r -p "Retype password for $path: " -s password_again || exit 1
446 if [[ $password == "$password_again" ]]; then
447 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password" || die "Password encryption aborted."
450 die "Error: the entered passwords do not match."
455 read -r -p "Enter password for $path: " -e password
456 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password" || die "Password encryption aborted."
458 git_add_file "$passfile" "Add given password for $path to store."
462 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
465 check_sneaky_paths "$path"
466 mkdir -p -v "$PREFIX/$(dirname "$path")"
467 set_gpg_recipients "$(dirname "$path")"
468 local passfile="$PREFIX/$path.gpg"
471 tmpdir #Defines $SECURE_TMPDIR
472 local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt"
476 if [[ -f $passfile ]]; then
477 $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1
480 ${EDITOR:-vi} "$tmp_file"
481 [[ -f $tmp_file ]] || die "New password not saved."
482 $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged."
483 while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do
484 yesno "GPG encryption failed. Would you like to try again?"
486 git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
490 local opts qrcode=0 clip=0 force=0 characters="$CHARACTER_SET" inplace=0 pass
491 opts="$($GETOPT -o nqcif -l no-symbols,qrcode,clip,in-place,force -n "$PROGRAM" -- "$@")"
494 while true; do case $1 in
495 -n|--no-symbols) characters="$CHARACTER_SET_NO_SYMBOLS"; shift ;;
496 -q|--qrcode) qrcode=1; shift ;;
497 -c|--clip) clip=1; shift ;;
498 -f|--force) force=1; shift ;;
499 -i|--in-place) inplace=1; shift ;;
503 [[ $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]"
505 local length="${2:-$GENERATED_LENGTH}"
506 check_sneaky_paths "$path"
507 [[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
508 mkdir -p -v "$PREFIX/$(dirname "$path")"
509 set_gpg_recipients "$(dirname "$path")"
510 local passfile="$PREFIX/$path.gpg"
513 [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
515 read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom)
516 [[ ${#pass} -eq $length ]] || die "Could not generate password from /dev/urandom."
517 if [[ $inplace -eq 0 ]]; then
518 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass" || die "Password encryption aborted."
520 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
521 if $GPG -d "${GPG_OPTS[@]}" "$passfile" | sed $'1c \\\n'"$(sed 's/[\/&]/\\&/g' <<<"$pass")"$'\n' | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then
522 mv "$passfile_temp" "$passfile"
524 rm -f "$passfile_temp"
525 die "Could not reencrypt new password."
529 [[ $inplace -eq 1 ]] && verb="Replace"
530 git_add_file "$passfile" "$verb generated password for ${path}."
532 if [[ $clip -eq 1 ]]; then
534 elif [[ $qrcode -eq 1 ]]; then
535 qrcode "$pass" "$path"
537 printf "\e[1m\e[37mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass"
542 local opts recursive="" force=0
543 opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")"
546 while true; do case $1 in
547 -r|--recursive) recursive="-r"; shift ;;
548 -f|--force) force=1; shift ;;
551 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
553 check_sneaky_paths "$path"
555 local passdir="$PREFIX/${path%/}"
556 local passfile="$PREFIX/$path.gpg"
557 [[ -f $passfile && -d $passdir && $path == */ || ! -f $passfile ]] && passfile="${passdir%/}/"
558 [[ -e $passfile ]] || die "Error: $path is not in the password store."
561 [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
563 rm $recursive -f -v "$passfile"
565 if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then
566 git -C "$INNER_GIT_DIR" rm -qr "$passfile"
568 git_commit "Remove $path from store."
570 rmdir -p "${passfile%/*}" 2>/dev/null
574 local opts move=1 force=0
575 [[ $1 == "copy" ]] && move=0
577 opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
580 while true; do case $1 in
581 -f|--force) force=1; shift ;;
584 [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
585 check_sneaky_paths "$@"
586 local old_path="$PREFIX/${1%/}"
587 local old_dir="$old_path"
588 local new_path="$PREFIX/$2"
590 if ! [[ -f $old_path.gpg && -d $old_path && $1 == */ || ! -f $old_path.gpg ]]; then
591 old_dir="${old_path%/*}"
592 old_path="${old_path}.gpg"
595 [[ -e $old_path ]] || die "Error: $1 is not in the password store."
597 mkdir -p -v "${new_path%/*}"
598 [[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg"
600 local interactive="-i"
601 [[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
604 if [[ $move -eq 1 ]]; then
605 mv $interactive -v "$old_path" "$new_path" || exit 1
606 [[ -e "$new_path" ]] && reencrypt_path "$new_path"
609 if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
610 git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
612 git_add_file "$new_path" "Rename ${1} to ${2}."
615 if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
616 git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
618 [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}."
620 rmdir -p "$old_dir" 2>/dev/null
622 cp $interactive -r -v "$old_path" "$new_path" || exit 1
623 [[ -e "$new_path" ]] && reencrypt_path "$new_path"
624 git_add_file "$new_path" "Copy ${1} to ${2}."
630 if [[ $1 == "init" ]]; then
631 INNER_GIT_DIR="$PREFIX"
632 git -C "$INNER_GIT_DIR" "$@" || exit 1
633 git_add_file "$PREFIX" "Add current contents of password store."
635 echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
636 git_add_file .gitattributes "Configure git repository for gpg file diff."
637 git -C "$INNER_GIT_DIR" config --local diff.gpg.binary true
638 git -C "$INNER_GIT_DIR" config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
639 elif [[ -n $INNER_GIT_DIR ]]; then
640 tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
641 export TMPDIR="$SECURE_TMPDIR"
642 git -C "$INNER_GIT_DIR" "$@"
644 die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
648 cmd_extension_or_show() {
649 if ! cmd_extension "$@"; then
655 SYSTEM_EXTENSION_DIR="/pass/lib/password-store/extensions"
657 check_sneaky_paths "$1"
658 local user_extension system_extension extension
659 [[ -n $SYSTEM_EXTENSION_DIR ]] && system_extension="$SYSTEM_EXTENSION_DIR/$1.bash"
660 [[ $PASSWORD_STORE_ENABLE_EXTENSIONS == true ]] && user_extension="$EXTENSIONS/$1.bash"
661 if [[ -n $user_extension && -f $user_extension && -x $user_extension ]]; then
662 verify_file "$user_extension"
663 extension="$user_extension"
664 elif [[ -n $system_extension && -f $system_extension && -x $system_extension ]]; then
665 extension="$system_extension"
670 source "$extension" "$@"
675 # END subcommand functions
682 init) shift; cmd_init "$@" ;;
683 help|--help) shift; cmd_usage "$@" ;;
684 version|--version) shift; cmd_version "$@" ;;
685 show|ls|list) shift; cmd_show "$@" ;;
686 find|search) shift; cmd_find "$@" ;;
687 grep) shift; cmd_grep "$@" ;;
688 insert|add) shift; cmd_insert "$@" ;;
689 edit) shift; cmd_edit "$@" ;;
690 generate) shift; cmd_generate "$@" ;;
691 delete|rm|remove) shift; cmd_delete "$@" ;;
692 rename|mv) shift; cmd_copy_move "move" "$@" ;;
693 copy|cp) shift; cmd_copy_move "copy" "$@" ;;
694 git) shift; cmd_git "$@" ;;
695 *) cmd_extension_or_show "$@" ;;