pass: bump to latest release
[zanchey/uccpass.git] / pass / bin / pass
1 #!/usr/bin/env bash
2
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.
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 which 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:-[:graph:]}"
21 CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}"
22
23 export GIT_CEILING_DIRECTORIES="$PREFIX/.."
24
25 #
26 # BEGIN helper functions
27 #
28
29 set_git() {
30         INNER_GIT_DIR="${1%/*}"
31         while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do
32                 INNER_GIT_DIR="${INNER_GIT_DIR%/*}"
33         done
34         [[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR=""
35 }
36 git_add_file() {
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
40         git_commit "$2"
41 }
42 git_commit() {
43         local sign=""
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"
47 }
48 yesno() {
49         [[ -t 0 ]] || return 0
50         local response
51         read -r -p "$1 [y/N] " response
52         [[ $response == [yY] ]] || exit 1
53 }
54 die() {
55         echo "$@" >&2
56         exit 1
57 }
58 verify_file() {
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; }
66         done
67         [[ $found -eq 1 ]] || die "Signature for $1 is invalid."
68 }
69 set_gpg_recipients() {
70         GPG_RECIPIENT_ARGS=( )
71         GPG_RECIPIENTS=( )
72
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" )
77                 done
78                 return
79         fi
80
81         local current="$PREFIX/$1"
82         while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
83                 current="${current%/*}"
84         done
85         current="$current/.gpg-id"
86
87         if [[ ! -f $current ]]; then
88                 cat >&2 <<-_EOF
89                 Error: You must run:
90                     $PROGRAM init your-gpg-id
91                 before you may use the password store.
92
93                 _EOF
94                 cmd_usage
95                 exit 1
96         fi
97
98         verify_file "$current"
99
100         local gpg_id
101         while read -r gpg_id; do
102                 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
103                 GPG_RECIPIENTS+=( "$gpg_id" )
104         done < "$current"
105 }
106
107 reencrypt_path() {
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}.--"
117
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]
125                         done
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)"
127                 fi
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)"
129
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"
134                 fi
135                 prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
136         done < <(find "$1" -path '*/.git' -prune -o -iname '*.gpg' -print0)
137 }
138 check_sneaky_paths() {
139         local path
140         for path in "$@"; do
141                 [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
142         done
143 }
144
145 #
146 # END helper functions
147 #
148
149 #
150 # BEGIN platform definable
151 #
152
153 clip() {
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"
161         (
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"
165
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.
170                 #
171                 # Clipboard managers frequently write their history out in plaintext,
172                 # so we axe it here:
173                 qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null
174
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."
178 }
179
180 qrcode() {
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 -
184                         return
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 -
187                         return
188                 elif type display >/dev/null 2>&1; then
189                         echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 -
190                         return
191                 fi
192         fi
193         echo -n "$1" | qrencode -t utf8
194 }
195
196 tmpdir() {
197         [[ -n $SECURE_TMPDIR ]] && return
198         local warn=1
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")"
203                 remove_tmpfile() {
204                         rm -rf "$SECURE_TMPDIR"
205                 }
206                 trap remove_tmpfile INT TERM EXIT
207         else
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.
212
213                 Are you sure you would like to continue?
214                 _EOF
215                 )"
216                 SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
217                 shred_tmpfile() {
218                         find "$SECURE_TMPDIR" -type f -exec $SHRED {} +
219                         rm -rf "$SECURE_TMPDIR"
220                 }
221                 trap shred_tmpfile INT TERM EXIT
222         fi
223
224 }
225 GETOPT="getopt"
226 SHRED="shred -f -z"
227
228
229 #
230 # END platform definable
231 #
232
233
234 #
235 # BEGIN subcommand functions
236 #
237
238 cmd_version() {
239         cat <<-_EOF
240         ============================================
241         = pass: the standard unix password manager =
242         =                                          =
243         =                   v1.7                   =
244         =                                          =
245         =             Jason A. Donenfeld           =
246         =               [email protected]            =
247         =                                          =
248         =      http://www.passwordstore.org/       =
249         ============================================
250         _EOF
251 }
252
253 cmd_usage() {
254         cmd_version
255         echo
256         cat <<-_EOF
257         Usage:
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]
262                 List passwords.
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.
290             $PROGRAM help
291                 Show this text.
292             $PROGRAM version
293                 Show version information.
294
295         More information may be found in the pass(1) man page.
296         _EOF
297 }
298
299 cmd_init() {
300         local opts id_path=""
301         opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")"
302         local err=$?
303         eval set -- "$opts"
304         while true; do case $1 in
305                 -p|--path) id_path="$2"; shift 2 ;;
306                 --) shift; break ;;
307         esac done
308
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."
312
313         local gpg_id="$PREFIX/$id_path/.gpg-id"
314         set_git "$gpg_id"
315
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)}."
322                 fi
323                 rmdir -p "${gpg_id%/*}" 2>/dev/null
324         else
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 )
334                         done
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]/,}."
339                 fi
340         fi
341
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)}."
344 }
345
346 cmd_show() {
347         local opts selected_line clip=0 qrcode=0
348         opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")"
349         local err=$?
350         eval set -- "$opts"
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 ;;
354                 --) shift; break ;;
355         esac done
356
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]"
358
359         local path="$1"
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 $?
365                 else
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
370                                 clip "$pass" "$path"
371                         elif [[ $qrcode -eq 1 ]]; then
372                                 qrcode "$pass" "$path"
373                         fi
374                 fi
375         elif [[ -d $PREFIX/$path ]]; then
376                 if [[ -z $path ]]; then
377                         echo "Password Store"
378                 else
379                         echo "${path%\/}"
380                 fi
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\"."
384         else
385                 die "Error: $path is not in the password store."
386         fi
387 }
388
389 cmd_find() {
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'
394 }
395
396 cmd_grep() {
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"
408                 echo "$grepresults"
409         done < <(find -L "$PREFIX" -path '*/.git' -prune -o -iname '*.gpg' -print0)
410 }
411
412 cmd_insert() {
413         local opts multiline=0 noecho=1 force=0
414         opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")"
415         local err=$?
416         eval set -- "$opts"
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 ;;
421                 --) shift; break ;;
422         esac done
423
424         [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
425         local path="${1%/}"
426         local passfile="$PREFIX/$path.gpg"
427         check_sneaky_paths "$path"
428         set_git "$passfile"
429
430         [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
431
432         mkdir -p -v "$PREFIX/$(dirname "$path")"
433         set_gpg_recipients "$(dirname "$path")"
434
435         if [[ $multiline -eq 1 ]]; then
436                 echo "Enter contents of $path and press Ctrl+D when finished:"
437                 echo
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
441                 while true; do
442                         read -r -p "Enter password for $path: " -s password || exit 1
443                         echo
444                         read -r -p "Retype password for $path: " -s password_again || exit 1
445                         echo
446                         if [[ $password == "$password_again" ]]; then
447                                 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password" || die "Password encryption aborted."
448                                 break
449                         else
450                                 die "Error: the entered passwords do not match."
451                         fi
452                 done
453         else
454                 local password
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."
457         fi
458         git_add_file "$passfile" "Add given password for $path to store."
459 }
460
461 cmd_edit() {
462         [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
463
464         local path="${1%/}"
465         check_sneaky_paths "$path"
466         mkdir -p -v "$PREFIX/$(dirname "$path")"
467         set_gpg_recipients "$(dirname "$path")"
468         local passfile="$PREFIX/$path.gpg"
469         set_git "$passfile"
470
471         tmpdir #Defines $SECURE_TMPDIR
472         local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt"
473
474
475         local action="Add"
476         if [[ -f $passfile ]]; then
477                 $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1
478                 action="Edit"
479         fi
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?"
485         done
486         git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
487 }
488
489 cmd_generate() {
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" -- "$@")"
492         local err=$?
493         eval set -- "$opts"
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 ;;
500                 --) shift; break ;;
501         esac done
502
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]"
504         local path="$1"
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"
511         set_git "$passfile"
512
513         [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
514
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."
519         else
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"
523                 else
524                         rm -f "$passfile_temp"
525                         die "Could not reencrypt new password."
526                 fi
527         fi
528         local verb="Add"
529         [[ $inplace -eq 1 ]] && verb="Replace"
530         git_add_file "$passfile" "$verb generated password for ${path}."
531
532         if [[ $clip -eq 1 ]]; then
533                 clip "$pass" "$path"
534         elif [[ $qrcode -eq 1 ]]; then
535                 qrcode "$pass" "$path"
536         else
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"
538         fi
539 }
540
541 cmd_delete() {
542         local opts recursive="" force=0
543         opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")"
544         local err=$?
545         eval set -- "$opts"
546         while true; do case $1 in
547                 -r|--recursive) recursive="-r"; shift ;;
548                 -f|--force) force=1; shift ;;
549                 --) shift; break ;;
550         esac done
551         [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
552         local path="$1"
553         check_sneaky_paths "$path"
554
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."
559         set_git "$passfile"
560
561         [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
562
563         rm $recursive -f -v "$passfile"
564         set_git "$passfile"
565         if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then
566                 git -C "$INNER_GIT_DIR" rm -qr "$passfile"
567                 set_git "$passfile"
568                 git_commit "Remove $path from store."
569         fi
570         rmdir -p "${passfile%/*}" 2>/dev/null
571 }
572
573 cmd_copy_move() {
574         local opts move=1 force=0
575         [[ $1 == "copy" ]] && move=0
576         shift
577         opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
578         local err=$?
579         eval set -- "$opts"
580         while true; do case $1 in
581                 -f|--force) force=1; shift ;;
582                 --) shift; break ;;
583         esac done
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"
589
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"
593         fi
594         echo "$old_path"
595         [[ -e $old_path ]] || die "Error: $1 is not in the password store."
596
597         mkdir -p -v "${new_path%/*}"
598         [[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg"
599
600         local interactive="-i"
601         [[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
602
603         set_git "$new_path"
604         if [[ $move -eq 1 ]]; then
605                 mv $interactive -v "$old_path" "$new_path" || exit 1
606                 [[ -e "$new_path" ]] && reencrypt_path "$new_path"
607
608                 set_git "$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
611                         set_git "$new_path"
612                         git_add_file "$new_path" "Rename ${1} to ${2}."
613                 fi
614                 set_git "$old_path"
615                 if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
616                         git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
617                         set_git "$old_path"
618                         [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}."
619                 fi
620                 rmdir -p "$old_dir" 2>/dev/null
621         else
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}."
625         fi
626 }
627
628 cmd_git() {
629         set_git "$PREFIX/"
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."
634
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" "$@"
643         else
644                 die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
645         fi
646 }
647
648 cmd_extension_or_show() {
649         if ! cmd_extension "$@"; then
650                 COMMAND="show"
651                 cmd_show "$@"
652         fi
653 }
654
655 SYSTEM_EXTENSION_DIR="/pass/lib/password-store/extensions"
656 cmd_extension() {
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"
666         else
667                 return 1
668         fi
669         shift
670         source "$extension" "$@"
671         return 0
672 }
673
674 #
675 # END subcommand functions
676 #
677
678 PROGRAM="${0##*/}"
679 COMMAND="$1"
680
681 case "$1" in
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 "$@" ;;
696 esac
697 exit 0

UCC git Repository :: git.ucc.asn.au