3 # Copyright (C) 2012 - 2014 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 X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
17 CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
19 export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
20 export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
23 # BEGIN helper functions
27 [[ -d $GIT_DIR ]] || return
28 git add "$1" || return
29 [[ -n $(git status --porcelain "$1") ]] || return
34 [[ -d $GIT_DIR ]] || return
35 [[ $(git config --bool --get pass.signcommits) == "true" ]] && sign="-S"
36 git commit $sign -m "$1"
39 [[ -t 0 ]] || return 0
41 read -r -p "$1 [y/N] " response
42 [[ $response == [yY] ]] || exit 1
48 set_gpg_recipients() {
49 GPG_RECIPIENT_ARGS=( )
52 if [[ -n $PASSWORD_STORE_KEY ]]; then
53 for gpg_id in $PASSWORD_STORE_KEY; do
54 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
55 GPG_RECIPIENTS+=( "$gpg_id" )
60 local current="$PREFIX/$1"
61 while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
62 current="${current%/*}"
64 current="$current/.gpg-id"
66 if [[ ! -f $current ]]; then
69 $PROGRAM init your-gpg-id
70 before you may use the password store.
78 while read -r gpg_id; do
79 GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
80 GPG_RECIPIENTS+=( "$gpg_id" )
85 local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile
86 local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")"
87 while read -r -d "" passfile; do
88 local passfile_dir="${passfile%/*}"
89 passfile_dir="${passfile_dir#$PREFIX}"
90 passfile_dir="${passfile_dir#/}"
91 local passfile_display="${passfile#$PREFIX/}"
92 passfile_display="${passfile_display%.gpg}"
93 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
95 set_gpg_recipients "$passfile_dir"
96 if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
97 for index in "${!GPG_RECIPIENTS[@]}"; do
98 local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)"
99 [[ -z $group ]] && continue
100 IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
101 unset GPG_RECIPIENTS[$index]
103 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)"
105 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)"
107 if [[ $gpg_keys != "$current_keys" ]]; then
108 echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
109 $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
110 mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
112 prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
113 done < <(find "$1" -iname '*.gpg' -print0)
115 check_sneaky_paths() {
118 [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
123 # END helper functions
127 # BEGIN platform definable
131 # This base64 business is because bash cannot store binary data in a shell
132 # variable. Specifically, it cannot store nulls nor (non-trivally) store
133 # trailing new lines.
134 local sleep_argv0="password store sleep on display $DISPLAY"
135 pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
136 local before="$(xclip -o -selection "$X_SELECTION" 2>/dev/null | base64)"
137 echo -n "$1" | xclip -selection "$X_SELECTION" || die "Error: Could not copy data to the clipboard"
139 ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
140 local now="$(xclip -o -selection "$X_SELECTION" | base64)"
141 [[ $now != $(echo -n "$1" | base64) ]] && before="$now"
143 # It might be nice to programatically check to see if klipper exists,
144 # as well as checking for other common clipboard managers. But for now,
145 # this works fine -- if qdbus isn't there or if klipper isn't running,
146 # this essentially becomes a no-op.
148 # Clipboard managers frequently write their history out in plaintext,
150 qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null
152 echo "$before" | base64 -d | xclip -selection "$X_SELECTION"
153 ) 2>/dev/null & disown
154 echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
157 [[ -n $SECURE_TMPDIR ]] && return
159 [[ $1 == "nowarn" ]] && warn=0
160 local template="$PROGRAM.XXXXXXXXXXXXX"
161 if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
162 SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")"
164 rm -rf "$SECURE_TMPDIR"
166 trap remove_tmpfile INT TERM EXIT
168 [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF
169 Your system does not have /dev/shm, which means that it may
170 be difficult to entirely erase the temporary non-encrypted
171 password file after editing.
173 Are you sure you would like to continue?
176 SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
178 find "$SECURE_TMPDIR" -type f -exec $SHRED {} +
179 rm -rf "$SECURE_TMPDIR"
181 trap shred_tmpfile INT TERM EXIT
190 # END platform definable
195 # BEGIN subcommand functions
200 ============================================
201 = pass: the standard unix password manager =
205 = Jason A. Donenfeld =
208 = http://www.passwordstore.org/ =
209 ============================================
218 $PROGRAM init [--path=subfolder,-p subfolder] gpg-id...
219 Initialize new password storage and use gpg-id for encryption.
220 Selectively reencrypt existing passwords using new gpg-id.
221 $PROGRAM [ls] [subfolder]
223 $PROGRAM find pass-names...
224 List passwords that match pass-names.
225 $PROGRAM [show] [--clip,-c] pass-name
226 Show existing password and optionally put it on the clipboard.
227 If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
228 $PROGRAM grep search-string
229 Search for password files containing search-string when decrypted.
230 $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
231 Insert new password. Optionally, echo the password back to the console
232 during entry. Or, optionally, the entry may be multiline. Prompt before
233 overwriting existing password unless forced.
234 $PROGRAM edit pass-name
235 Insert a new password or edit an existing password using ${EDITOR:-vi}.
236 $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length
237 Generate a new password of pass-length with optionally no symbols.
238 Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
239 Prompt before overwriting existing password unless forced.
240 Optionally replace only the first line of an existing file with a new password.
241 $PROGRAM rm [--recursive,-r] [--force,-f] pass-name
242 Remove existing password or directory, optionally forcefully.
243 $PROGRAM mv [--force,-f] old-path new-path
244 Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
245 $PROGRAM cp [--force,-f] old-path new-path
246 Copies old-path to new-path, optionally forcefully, selectively reencrypting.
247 $PROGRAM git git-command-args...
248 If the password store is a git repository, execute a git command
249 specified by git-command-args.
253 Show version information.
255 More information may be found in the pass(1) man page.
260 local opts id_path=""
261 opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")"
264 while true; do case $1 in
265 -p|--path) id_path="$2"; shift 2 ;;
269 [[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..."
270 [[ -n $id_path ]] && check_sneaky_paths "$id_path"
271 [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory."
273 local gpg_id="$PREFIX/$id_path/.gpg-id"
275 if [[ $# -eq 1 && -z $1 ]]; then
276 [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
277 rm -v -f "$gpg_id" || exit 1
278 if [[ -d $GIT_DIR ]]; then
280 git_commit "Deinitialize ${gpg_id}."
282 rmdir -p "${gpg_id%/*}" 2>/dev/null
284 mkdir -v -p "$PREFIX/$id_path"
285 printf "%s\n" "$@" > "$gpg_id"
286 local id_print="$(printf "%s, " "$@")"
287 echo "Password store initialized for ${id_print%, }"
288 git_add_file "$gpg_id" "Set GPG id to ${id_print%, }."
291 reencrypt_path "$PREFIX/$id_path"
292 git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }."
297 opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")"
300 while true; do case $1 in
301 -c|--clip) clip=1; shift ;;
305 [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] [pass-name]"
308 local passfile="$PREFIX/$path.gpg"
309 check_sneaky_paths "$path"
310 if [[ -f $passfile ]]; then
311 if [[ $clip -eq 0 ]]; then
312 $GPG -d "${GPG_OPTS[@]}" "$passfile" || exit $?
314 local pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | head -n 1)"
315 [[ -n $pass ]] || exit 1
318 elif [[ -d $PREFIX/$path ]]; then
319 if [[ -z $path ]]; then
320 echo "Password Store"
324 tree -C -l --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g' # remove .gpg at end of line, but keep colors
325 elif [[ -z $path ]]; then
326 die "Error: password store is empty. Try \"pass init\"."
328 die "Error: $path is not in the password store."
333 [[ -z "$@" ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
334 IFS="," eval 'echo "Search Terms: $*"'
335 local terms="*$(printf '%s*|*' "$@")"
336 tree -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g'
340 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND search-string"
341 local search="$1" passfile grepresults
342 while read -r -d "" passfile; do
343 grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$search")"
344 [ $? -ne 0 ] && continue
345 passfile="${passfile%.gpg}"
346 passfile="${passfile#$PREFIX/}"
347 local passfile_dir="${passfile%/*}/"
348 [[ $passfile_dir == "${passfile}/" ]] && passfile_dir=""
349 passfile="${passfile##*/}"
350 printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
352 done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
356 local opts multiline=0 noecho=1 force=0
357 opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")"
360 while true; do case $1 in
361 -m|--multiline) multiline=1; shift ;;
362 -e|--echo) noecho=0; shift ;;
363 -f|--force) force=1; shift ;;
367 [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
369 local passfile="$PREFIX/$path.gpg"
370 check_sneaky_paths "$path"
372 [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
374 mkdir -p -v "$PREFIX/$(dirname "$path")"
375 set_gpg_recipients "$(dirname "$path")"
377 if [[ $multiline -eq 1 ]]; then
378 echo "Enter contents of $path and press Ctrl+D when finished:"
380 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}"
381 elif [[ $noecho -eq 1 ]]; then
382 local password password_again
384 read -r -p "Enter password for $path: " -s password || exit 1
386 read -r -p "Retype password for $path: " -s password_again || exit 1
388 if [[ $password == "$password_again" ]]; then
389 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
392 echo "Error: the entered passwords do not match."
397 read -r -p "Enter password for $path: " -e password
398 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
400 git_add_file "$passfile" "Add given password for $path to store."
404 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
407 check_sneaky_paths "$path"
408 mkdir -p -v "$PREFIX/$(dirname "$path")"
409 set_gpg_recipients "$(dirname "$path")"
410 local passfile="$PREFIX/$path.gpg"
412 tmpdir #Defines $SECURE_TMPDIR
413 local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXX")-${path//\//-}.txt"
417 if [[ -f $passfile ]]; then
418 $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1
421 ${EDITOR:-vi} "$tmp_file"
422 [[ -f $tmp_file ]] || die "New password not saved."
423 $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged."
424 while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do
425 yesno "GPG encryption failed. Would you like to try again?"
427 git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
431 local opts clip=0 force=0 symbols="-y" inplace=0
432 opts="$($GETOPT -o ncif -l no-symbols,clip,in-place,force -n "$PROGRAM" -- "$@")"
435 while true; do case $1 in
436 -n|--no-symbols) symbols=""; shift ;;
437 -c|--clip) clip=1; shift ;;
438 -f|--force) force=1; shift ;;
439 -i|--in-place) inplace=1; shift ;;
443 [[ $err -ne 0 || $# -ne 2 || ( $force -eq 1 && $inplace -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length"
446 check_sneaky_paths "$path"
447 [[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
448 mkdir -p -v "$PREFIX/$(dirname "$path")"
449 set_gpg_recipients "$(dirname "$path")"
450 local passfile="$PREFIX/$path.gpg"
452 [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
454 local pass="$(pwgen -s $symbols $length 1)"
455 [[ -n $pass ]] || exit 1
456 if [[ $inplace -eq 0 ]]; then
457 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass"
459 local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
460 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
461 mv "$passfile_temp" "$passfile"
463 rm -f "$passfile_temp"
464 die "Could not reencrypt new password."
468 [[ $inplace -eq 1 ]] && verb="Replace"
469 git_add_file "$passfile" "$verb generated password for ${path}."
471 if [[ $clip -eq 0 ]]; then
472 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"
479 local opts recursive="" force=0
480 opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")"
483 while true; do case $1 in
484 -r|--recursive) recursive="-r"; shift ;;
485 -f|--force) force=1; shift ;;
488 [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
490 check_sneaky_paths "$path"
492 local passfile="$PREFIX/${path%/}"
493 if [[ ! -d $passfile ]]; then
494 passfile="$PREFIX/$path.gpg"
495 [[ ! -f $passfile ]] && die "Error: $path is not in the password store."
498 [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
500 rm $recursive -f -v "$passfile"
501 if [[ -d $GIT_DIR && ! -e $passfile ]]; then
502 git rm -qr "$passfile"
503 git_commit "Remove $path from store."
505 rmdir -p "${passfile%/*}" 2>/dev/null
509 local opts move=1 force=0
510 [[ $1 == "copy" ]] && move=0
512 opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
515 while true; do case $1 in
516 -f|--force) force=1; shift ;;
519 [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
520 check_sneaky_paths "$@"
521 local old_path="$PREFIX/${1%/}"
522 local new_path="$PREFIX/$2"
523 local old_dir="$old_path"
525 if [[ ! -d $old_path ]]; then
526 old_dir="${old_path%/*}"
527 old_path="${old_path}.gpg"
528 [[ ! -f $old_path ]] && die "Error: $1 is not in the password store."
531 mkdir -p -v "${new_path%/*}"
532 [[ -d $old_path || -d $new_path || $new_path =~ /$ ]] || new_path="${new_path}.gpg"
534 local interactive="-i"
535 [[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
537 if [[ $move -eq 1 ]]; then
538 mv $interactive -v "$old_path" "$new_path" || exit 1
539 [[ -e "$new_path" ]] && reencrypt_path "$new_path"
541 if [[ -d $GIT_DIR && ! -e $old_path ]]; then
542 git rm -qr "$old_path"
543 git_add_file "$new_path" "Rename ${1} to ${2}."
545 rmdir -p "$old_dir" 2>/dev/null
547 cp $interactive -r -v "$old_path" "$new_path" || exit 1
548 [[ -e "$new_path" ]] && reencrypt_path "$new_path"
549 git_add_file "$new_path" "Copy ${1} to ${2}."
554 if [[ $1 == "init" ]]; then
556 git_add_file "$PREFIX" "Add current contents of password store."
558 echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
559 git_add_file .gitattributes "Configure git repository for gpg file diff."
560 git config --local diff.gpg.binary true
561 git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
562 elif [[ -d $GIT_DIR ]]; then
563 tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
564 export TMPDIR="$SECURE_TMPDIR"
567 die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
572 # END subcommand functions
579 init) shift; cmd_init "$@" ;;
580 help|--help) shift; cmd_usage "$@" ;;
581 version|--version) shift; cmd_version "$@" ;;
582 show|ls|list) shift; cmd_show "$@" ;;
583 find|search) shift; cmd_find "$@" ;;
584 grep) shift; cmd_grep "$@" ;;
585 insert|add) shift; cmd_insert "$@" ;;
586 edit) shift; cmd_edit "$@" ;;
587 generate) shift; cmd_generate "$@" ;;
588 delete|rm|remove) shift; cmd_delete "$@" ;;
589 rename|mv) shift; cmd_copy_move "move" "$@" ;;
590 copy|cp) shift; cmd_copy_move "copy" "$@" ;;
591 git) shift; cmd_git "$@" ;;
592 *) COMMAND="show"; cmd_show "$@" ;;