pass: bump to latest release
authorDavid Adam <zanchey@ucc.gu.uwa.edu.au>
Mon, 6 Mar 2017 15:30:09 +0000 (23:30 +0800)
committerDavid Adam <zanchey@ucc.gu.uwa.edu.au>
Mon, 6 Mar 2017 15:30:09 +0000 (23:30 +0800)
pass/bin/pass
pass/share/man/man1/pass.1

index 564bec0..1006659 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-# Copyright (C) 2012 - 2014 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+# Copyright (C) 2012 - 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
 # This file is licensed under the GPLv2+. Please see COPYING for more information.
 
 umask "${PASSWORD_STORE_UMASK:-077}"
@@ -13,27 +13,37 @@ which gpg2 &>/dev/null && GPG="gpg2"
 [[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
 
 PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
+EXTENSIONS="${PASSWORD_STORE_EXTENSIONS_DIR:-$PREFIX/.extensions}"
 X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
 CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
+GENERATED_LENGTH="${PASSWORD_STORE_GENERATED_LENGTH:-25}"
+CHARACTER_SET="${PASSWORD_STORE_CHARACTER_SET:-[:graph:]}"
+CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}"
 
-export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
-export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
+export GIT_CEILING_DIRECTORIES="$PREFIX/.."
 
 #
 # BEGIN helper functions
 #
 
+set_git() {
+       INNER_GIT_DIR="${1%/*}"
+       while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do
+               INNER_GIT_DIR="${INNER_GIT_DIR%/*}"
+       done
+       [[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR=""
+}
 git_add_file() {
-       [[ -d $GIT_DIR ]] || return
-       git add "$1" || return
-       [[ -n $(git status --porcelain "$1") ]] || return
+       [[ -n $INNER_GIT_DIR ]] || return
+       git -C "$INNER_GIT_DIR" add "$1" || return
+       [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$1") ]] || return
        git_commit "$2"
 }
 git_commit() {
        local sign=""
-       [[ -d $GIT_DIR ]] || return
-       [[ $(git config --bool --get pass.signcommits) == "true" ]] && sign="-S"
-       git commit $sign -m "$1"
+       [[ -n $INNER_GIT_DIR ]] || return
+       [[ $(git -C "$INNER_GIT_DIR" config --bool --get pass.signcommits) == "true" ]] && sign="-S"
+       git -C "$INNER_GIT_DIR" commit $sign -m "$1"
 }
 yesno() {
        [[ -t 0 ]] || return 0
@@ -45,6 +55,17 @@ die() {
        echo "$@" >&2
        exit 1
 }
+verify_file() {
+       [[ -n $PASSWORD_STORE_SIGNING_KEY ]] || return 0
+       [[ -f $1.sig ]] || die "Signature for $1 does not exist."
+       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')"
+       local fingerprint found=0
+       for fingerprint in $PASSWORD_STORE_SIGNING_KEY; do
+               [[ $fingerprint =~ ^[A-F0-9]{40}$ ]] || continue
+               [[ $fingerprints == *$fingerprint* ]] && { found=1; break; }
+       done
+       [[ $found -eq 1 ]] || die "Signature for $1 is invalid."
+}
 set_gpg_recipients() {
        GPG_RECIPIENT_ARGS=( )
        GPG_RECIPIENTS=( )
@@ -74,6 +95,8 @@ set_gpg_recipients() {
                exit 1
        fi
 
+       verify_file "$current"
+
        local gpg_id
        while read -r gpg_id; do
                GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
@@ -110,7 +133,7 @@ reencrypt_path() {
                        mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
                fi
                prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
-       done < <(find "$1" -iname '*.gpg' -print0)
+       done < <(find "$1" -path '*/.git' -prune -o -iname '*.gpg' -print0)
 }
 check_sneaky_paths() {
        local path
@@ -136,7 +159,7 @@ clip() {
        local before="$(xclip -o -selection "$X_SELECTION" 2>/dev/null | base64)"
        echo -n "$1" | xclip -selection "$X_SELECTION" || die "Error: Could not copy data to the clipboard"
        (
-               ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
+               ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" )
                local now="$(xclip -o -selection "$X_SELECTION" | base64)"
                [[ $now != $(echo -n "$1" | base64) ]] && before="$now"
 
@@ -153,6 +176,23 @@ clip() {
        ) 2>/dev/null & disown
        echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
 }
+
+qrcode() {
+       if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then
+               if type feh >/dev/null 2>&1; then
+                       echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 -
+                       return
+               elif type gm >/dev/null 2>&1; then
+                       echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 -
+                       return
+               elif type display >/dev/null 2>&1; then
+                       echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 -
+                       return
+               fi
+       fi
+       echo -n "$1" | qrencode -t utf8
+}
+
 tmpdir() {
        [[ -n $SECURE_TMPDIR ]] && return
        local warn=1
@@ -200,7 +240,7 @@ cmd_version() {
        ============================================
        = pass: the standard unix password manager =
        =                                          =
-       =                  v1.6.5                  =
+       =                   v1.7                   =
        =                                          =
        =             Jason A. Donenfeld           =
        =               Jason@zx2c4.com            =
@@ -222,7 +262,7 @@ cmd_usage() {
                List passwords.
            $PROGRAM find pass-names...
                List passwords that match pass-names.
-           $PROGRAM [show] [--clip,-c] pass-name
+           $PROGRAM [show] [--clip[=line-number],-c[line-number]] pass-name
                Show existing password and optionally put it on the clipboard.
                If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
            $PROGRAM grep search-string
@@ -233,8 +273,8 @@ cmd_usage() {
                overwriting existing password unless forced.
            $PROGRAM edit pass-name
                Insert a new password or edit an existing password using ${EDITOR:-vi}.
-           $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length
-               Generate a new password of pass-length with optionally no symbols.
+           $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]
+               Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols.
                Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
                Prompt before overwriting existing password unless forced.
                Optionally replace only the first line of an existing file with a new password.
@@ -271,49 +311,66 @@ cmd_init() {
        [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory."
 
        local gpg_id="$PREFIX/$id_path/.gpg-id"
+       set_git "$gpg_id"
 
        if [[ $# -eq 1 && -z $1 ]]; then
                [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
                rm -v -f "$gpg_id" || exit 1
-               if [[ -d $GIT_DIR ]]; then
-                       git rm -qr "$gpg_id"
-                       git_commit "Deinitialize ${gpg_id}."
+               if [[ -n $INNER_GIT_DIR ]]; then
+                       git -C "$INNER_GIT_DIR" rm -qr "$gpg_id"
+                       git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}."
                fi
                rmdir -p "${gpg_id%/*}" 2>/dev/null
        else
                mkdir -v -p "$PREFIX/$id_path"
                printf "%s\n" "$@" > "$gpg_id"
                local id_print="$(printf "%s, " "$@")"
-               echo "Password store initialized for ${id_print%, }"
-               git_add_file "$gpg_id" "Set GPG id to ${id_print%, }."
+               echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}"
+               git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}."
+               if [[ -n $PASSWORD_STORE_SIGNING_KEY ]]; then
+                       local signing_keys=( ) key
+                       for key in $PASSWORD_STORE_SIGNING_KEY; do
+                               signing_keys+=( --default-key $key )
+                       done
+                       gpg "${GPG_OPTS[@]}" "${signing_keys[@]}" --detach-sign "$gpg_id" || die "Could not sign .gpg_id."
+                       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')"
+                       [[ -n $key ]] || die "Signing of .gpg_id unsuccessful."
+                       git_add_file "$gpg_id.sig" "Signing new GPG id with ${key//[$IFS]/,}."
+               fi
        fi
 
        reencrypt_path "$PREFIX/$id_path"
-       git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }."
+       git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}."
 }
 
 cmd_show() {
-       local opts clip=0
-       opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")"
+       local opts selected_line clip=0 qrcode=0
+       opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")"
        local err=$?
        eval set -- "$opts"
        while true; do case $1 in
-               -c|--clip) clip=1; shift ;;
+               -q|--qrcode) qrcode=1; selected_line="${2:-1}"; shift 2 ;;
+               -c|--clip) clip=1; selected_line="${2:-1}"; shift 2 ;;
                --) shift; break ;;
        esac done
 
-       [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] [pass-name]"
+       [[ $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]"
 
        local path="$1"
        local passfile="$PREFIX/$path.gpg"
        check_sneaky_paths "$path"
        if [[ -f $passfile ]]; then
-               if [[ $clip -eq 0 ]]; then
+               if [[ $clip -eq 0 && $qrcode -eq 0 ]]; then
                        $GPG -d "${GPG_OPTS[@]}" "$passfile" || exit $?
                else
-                       local pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | head -n 1)"
-                       [[ -n $pass ]] || exit 1
-                       clip "$pass" "$path"
+                       [[ $selected_line =~ ^[0-9]+$ ]] || die "Clip location '$selected_line' is not a number."
+                       local pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +${selected_line} | head -n 1)"
+                       [[ -n $pass ]] || die "There is no password to put on the clipboard at line ${selected_line}."
+                       if [[ $clip -eq 1 ]]; then
+                               clip "$pass" "$path"
+                       elif [[ $qrcode -eq 1 ]]; then
+                               qrcode "$pass" "$path"
+                       fi
                fi
        elif [[ -d $PREFIX/$path ]]; then
                if [[ -z $path ]]; then
@@ -321,7 +378,7 @@ cmd_show() {
                else
                        echo "${path%\/}"
                fi
-               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
+               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
        elif [[ -z $path ]]; then
                die "Error: password store is empty. Try \"pass init\"."
        else
@@ -330,10 +387,10 @@ cmd_show() {
 }
 
 cmd_find() {
-       [[ -z "$@" ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
+       [[ $# -eq 0 ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
        IFS="," eval 'echo "Search Terms: $*"'
        local terms="*$(printf '%s*|*' "$@")"
-       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'
+       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'
 }
 
 cmd_grep() {
@@ -341,7 +398,7 @@ cmd_grep() {
        local search="$1" passfile grepresults
        while read -r -d "" passfile; do
                grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$search")"
-               [ $? -ne 0 ] && continue
+               [[ $? -ne 0 ]] && continue
                passfile="${passfile%.gpg}"
                passfile="${passfile#$PREFIX/}"
                local passfile_dir="${passfile%/*}/"
@@ -349,7 +406,7 @@ cmd_grep() {
                passfile="${passfile##*/}"
                printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
                echo "$grepresults"
-       done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
+       done < <(find -L "$PREFIX" -path '*/.git' -prune -o -iname '*.gpg' -print0)
 }
 
 cmd_insert() {
@@ -365,9 +422,10 @@ cmd_insert() {
        esac done
 
        [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
-       local path="$1"
+       local path="${1%/}"
        local passfile="$PREFIX/$path.gpg"
        check_sneaky_paths "$path"
+       set_git "$passfile"
 
        [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
 
@@ -377,7 +435,7 @@ cmd_insert() {
        if [[ $multiline -eq 1 ]]; then
                echo "Enter contents of $path and press Ctrl+D when finished:"
                echo
-               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}"
+               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
        elif [[ $noecho -eq 1 ]]; then
                local password password_again
                while true; do
@@ -386,16 +444,16 @@ cmd_insert() {
                        read -r -p "Retype password for $path: " -s password_again || exit 1
                        echo
                        if [[ $password == "$password_again" ]]; then
-                               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
+                               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password" || die "Password encryption aborted."
                                break
                        else
-                               echo "Error: the entered passwords do not match."
+                               die "Error: the entered passwords do not match."
                        fi
                done
        else
                local password
                read -r -p "Enter password for $path: " -e password
-               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
+               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password" || die "Password encryption aborted."
        fi
        git_add_file "$passfile" "Add given password for $path to store."
 }
@@ -403,14 +461,15 @@ cmd_insert() {
 cmd_edit() {
        [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
 
-       local path="$1"
+       local path="${1%/}"
        check_sneaky_paths "$path"
        mkdir -p -v "$PREFIX/$(dirname "$path")"
        set_gpg_recipients "$(dirname "$path")"
        local passfile="$PREFIX/$path.gpg"
+       set_git "$passfile"
 
        tmpdir #Defines $SECURE_TMPDIR
-       local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXX")-${path//\//-}.txt"
+       local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt"
 
 
        local action="Add"
@@ -428,33 +487,35 @@ cmd_edit() {
 }
 
 cmd_generate() {
-       local opts clip=0 force=0 symbols="-y" inplace=0
-       opts="$($GETOPT -o ncif -l no-symbols,clip,in-place,force -n "$PROGRAM" -- "$@")"
+       local opts qrcode=0 clip=0 force=0 characters="$CHARACTER_SET" inplace=0 pass
+       opts="$($GETOPT -o nqcif -l no-symbols,qrcode,clip,in-place,force -n "$PROGRAM" -- "$@")"
        local err=$?
        eval set -- "$opts"
        while true; do case $1 in
-               -n|--no-symbols) symbols=""; shift ;;
+               -n|--no-symbols) characters="$CHARACTER_SET_NO_SYMBOLS"; shift ;;
+               -q|--qrcode) qrcode=1; shift ;;
                -c|--clip) clip=1; shift ;;
                -f|--force) force=1; shift ;;
                -i|--in-place) inplace=1; shift ;;
                --) shift; break ;;
        esac done
 
-       [[ $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"
+       [[ $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]"
        local path="$1"
-       local length="$2"
+       local length="${2:-$GENERATED_LENGTH}"
        check_sneaky_paths "$path"
        [[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
        mkdir -p -v "$PREFIX/$(dirname "$path")"
        set_gpg_recipients "$(dirname "$path")"
        local passfile="$PREFIX/$path.gpg"
+       set_git "$passfile"
 
        [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
 
-       local pass="$(pwgen -s $symbols $length 1)"
-       [[ -n $pass ]] || exit 1
+       read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom)
+       [[ ${#pass} -eq $length ]] || die "Could not generate password from /dev/urandom."
        if [[ $inplace -eq 0 ]]; then
-               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass"
+               $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass" || die "Password encryption aborted."
        else
                local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
                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
@@ -468,10 +529,12 @@ cmd_generate() {
        [[ $inplace -eq 1 ]] && verb="Replace"
        git_add_file "$passfile" "$verb generated password for ${path}."
 
-       if [[ $clip -eq 0 ]]; then
-               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"
-       else
+       if [[ $clip -eq 1 ]]; then
                clip "$pass" "$path"
+       elif [[ $qrcode -eq 1 ]]; then
+               qrcode "$pass" "$path"
+       else
+               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"
        fi
 }
 
@@ -489,17 +552,19 @@ cmd_delete() {
        local path="$1"
        check_sneaky_paths "$path"
 
-       local passfile="$PREFIX/${path%/}"
-       if [[ ! -d $passfile ]]; then
-               passfile="$PREFIX/$path.gpg"
-               [[ ! -f $passfile ]] && die "Error: $path is not in the password store."
-       fi
+       local passdir="$PREFIX/${path%/}"
+       local passfile="$PREFIX/$path.gpg"
+       [[ -f $passfile && -d $passdir && $path == */ || ! -f $passfile ]] && passfile="${passdir%/}/"
+       [[ -e $passfile ]] || die "Error: $path is not in the password store."
+       set_git "$passfile"
 
        [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
 
        rm $recursive -f -v "$passfile"
-       if [[ -d $GIT_DIR && ! -e $passfile ]]; then
-               git rm -qr "$passfile"
+       set_git "$passfile"
+       if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then
+               git -C "$INNER_GIT_DIR" rm -qr "$passfile"
+               set_git "$passfile"
                git_commit "Remove $path from store."
        fi
        rmdir -p "${passfile%/*}" 2>/dev/null
@@ -519,29 +584,39 @@ cmd_copy_move() {
        [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
        check_sneaky_paths "$@"
        local old_path="$PREFIX/${1%/}"
-       local new_path="$PREFIX/$2"
        local old_dir="$old_path"
+       local new_path="$PREFIX/$2"
 
-       if [[ ! -d $old_path ]]; then
+       if ! [[ -f $old_path.gpg && -d $old_path && $1 == */ || ! -f $old_path.gpg ]]; then
                old_dir="${old_path%/*}"
                old_path="${old_path}.gpg"
-               [[ ! -f $old_path ]] && die "Error: $1 is not in the password store."
        fi
+       echo "$old_path"
+       [[ -e $old_path ]] || die "Error: $1 is not in the password store."
 
        mkdir -p -v "${new_path%/*}"
-       [[ -d $old_path || -d $new_path || $new_path =~ /$ ]] || new_path="${new_path}.gpg"
+       [[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg"
 
        local interactive="-i"
        [[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
 
+       set_git "$new_path"
        if [[ $move -eq 1 ]]; then
                mv $interactive -v "$old_path" "$new_path" || exit 1
                [[ -e "$new_path" ]] && reencrypt_path "$new_path"
 
-               if [[ -d $GIT_DIR && ! -e $old_path ]]; then
-                       git rm -qr "$old_path"
+               set_git "$new_path"
+               if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
+                       git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
+                       set_git "$new_path"
                        git_add_file "$new_path" "Rename ${1} to ${2}."
                fi
+               set_git "$old_path"
+               if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
+                       git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
+                       set_git "$old_path"
+                       [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}."
+               fi
                rmdir -p "$old_dir" 2>/dev/null
        else
                cp $interactive -r -v "$old_path" "$new_path" || exit 1
@@ -551,23 +626,51 @@ cmd_copy_move() {
 }
 
 cmd_git() {
+       set_git "$PREFIX/"
        if [[ $1 == "init" ]]; then
-               git "$@" || exit 1
+               INNER_GIT_DIR="$PREFIX"
+               git -C "$INNER_GIT_DIR" "$@" || exit 1
                git_add_file "$PREFIX" "Add current contents of password store."
 
                echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
                git_add_file .gitattributes "Configure git repository for gpg file diff."
-               git config --local diff.gpg.binary true
-               git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
-       elif [[ -d $GIT_DIR ]]; then
+               git -C "$INNER_GIT_DIR" config --local diff.gpg.binary true
+               git -C "$INNER_GIT_DIR" config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
+       elif [[ -n $INNER_GIT_DIR ]]; then
                tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
                export TMPDIR="$SECURE_TMPDIR"
-               git "$@"
+               git -C "$INNER_GIT_DIR" "$@"
        else
                die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
        fi
 }
 
+cmd_extension_or_show() {
+       if ! cmd_extension "$@"; then
+               COMMAND="show"
+               cmd_show "$@"
+       fi
+}
+
+SYSTEM_EXTENSION_DIR="/pass/lib/password-store/extensions"
+cmd_extension() {
+       check_sneaky_paths "$1"
+       local user_extension system_extension extension
+       [[ -n $SYSTEM_EXTENSION_DIR ]] && system_extension="$SYSTEM_EXTENSION_DIR/$1.bash"
+       [[ $PASSWORD_STORE_ENABLE_EXTENSIONS == true ]] && user_extension="$EXTENSIONS/$1.bash"
+       if [[ -n $user_extension && -f $user_extension && -x $user_extension ]]; then
+               verify_file "$user_extension"
+               extension="$user_extension"
+       elif [[ -n $system_extension && -f $system_extension && -x $system_extension ]]; then
+               extension="$system_extension"
+       else
+               return 1
+       fi
+       shift
+       source "$extension" "$@"
+       return 0
+}
+
 #
 # END subcommand functions
 #
@@ -589,6 +692,6 @@ case "$1" in
        rename|mv) shift;               cmd_copy_move "move" "$@" ;;
        copy|cp) shift;                 cmd_copy_move "copy" "$@" ;;
        git) shift;                     cmd_git "$@" ;;
-       *) COMMAND="show";              cmd_show "$@" ;;
+       *)                              cmd_extension_or_show "$@" ;;
 esac
 exit 0
index e1fe605..e842178 100644 (file)
@@ -30,18 +30,25 @@ If no COMMAND is specified, COMMAND defaults to either
 .B show
 or
 .BR ls ,
-depending on the type of specifier in ARGS. Otherwise COMMAND must be one of
-the valid commands listed below.
+depending on the type of specifier in ARGS. Alternatively, if \fIPASSWORD_STORE_ENABLE_EXTENSIONS\fP
+is set to "true", and the file \fI.extensions/COMMAND.bash\fP exists inside the
+password store and is executable, then it is sourced into the environment,
+passing any arguments and environment variables. Extensions existing in a
+system-wide directory, only installable by the administrator, are always enabled.
+
+Otherwise COMMAND must be one of the valid commands listed below.
 
 Several of the commands below rely on or provide additional functionality if
 the password store directory is also a git repository. If the password store
 directory is a git repository, all password store modification commands will
-cause a corresponding git commit. See the \fIEXTENDED GIT EXAMPLE\fP section
-for a detailed description using \fBinit\fP and
+cause a corresponding git commit. Sub-directories may be separate nested git
+repositories, and pass will use the inner-most directory relative to the
+current password. See the \fIEXTENDED GIT EXAMPLE\fP section for a detailed
+description using \fBinit\fP and
 .BR git (1).
 
 The \fBinit\fP command must be run before other commands in order to initialize
-the password store with the correct gpg key id. Passwords are encrypting using
+the password store with the correct gpg key id. Passwords are encrypted using
 the gpg key set with \fBinit\fP.
 
 There is a corresponding bash completion script for use with tab completing
@@ -86,12 +93,15 @@ List names of passwords inside the tree that match \fIpass-names\fP by using the
 .BR tree (1)
 program. This command is alternatively named \fBsearch\fP.
 .TP
-\fBshow\fP [ \fI--clip\fP, \fI-c\fP ] \fIpass-name\fP
+\fBshow\fP [ \fI--clip\fP[=\fIline-number\fP], \fI-c\fP[\fIline-number\fP] ] [ \fI--qrcode\fP[=\fIline-number\fP], \fI-q\fP[\fIline-number\fP] ] \fIpass-name\fP
 Decrypt and print a password named \fIpass-name\fP. If \fI--clip\fP or \fI-c\fP
-is specified, do not print the password but instead copy the first line to the
-clipboard using
+is specified, do not print the password but instead copy the first (or otherwise specified)
+line to the clipboard using
 .BR xclip (1)
-and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds.
+and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. If \fI--qrcode\fP
+or \fI-q\fP is specified, do not print the password but instead display a QR code using
+.BR qrencode (1)
+either to the terminal or graphically if supported.
 .TP
 \fBinsert\fP [ \fI--echo\fP, \fI-e\fP | \fI--multiline\fP, \fI-m\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP
 Insert a new password into the password store called \fIpass-name\fP. This will
@@ -111,16 +121,20 @@ ensure that temporary files are created in \fI/dev/shm\fP in order to avoid writ
 difficult-to-erase disk sectors. If \fI/dev/shm\fP is not accessible, fallback to
 the ordinary \fITMPDIR\fP location, and print a warning.
 .TP
-\fBgenerate\fP [ \fI--no-symbols\fP, \fI-n\fP ] [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name pass-length\fP
-Generate a new password using
-.BR pwgen (1)
-of length \fIpass-length\fP and insert into \fIpass-name\fP. If \fI--no-symbols\fP or \fI-n\fP
-is specified, do not use any non-alphanumeric characters in the generated password.
+\fBgenerate\fP [ \fI--no-symbols\fP, \fI-n\fP ] [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name [pass-length]\fP
+Generate a new password using \fB/dev/urandom\fP of length \fIpass-length\fP
+(or \fIPASSWORD_STORE_GENERATED_LENGTH\fP if unspecified) and insert into
+\fIpass-name\fP. If \fI--no-symbols\fP or \fI-n\fP is specified, do not use
+any non-alphanumeric characters in the generated password. The character sets used
+in generating passwords can be changed with the \fIPASSWORD_STORE_CHARACTER_SET\fP and
+\fIPASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS\fP environment variables, described below.
 If \fI--clip\fP or \fI-c\fP is specified, do not print the password but instead copy
 it to the clipboard using
 .BR xclip (1)
-and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds.
-Prompt before overwriting an existing password,
+and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. If \fI--qrcode\fP
+or \fI-q\fP is specified, do not print the password but instead display a QR code using
+.BR qrencode (1)
+either to the terminal or graphically if supported. Prompt before overwriting an existing password,
 unless \fI--force\fP or \fI-f\fP is specified. If \fI--in-place\fP or \fI-i\fP is
 specified, do not interactively prompt, and only replace the first line of the password
 file with the new generated password, keeping the remainder of the file intact.
@@ -383,6 +397,9 @@ Contains the default gpg key identification used for encryption and decryption.
 Multiple gpg keys may be specified in this file, one per line. If this file
 exists in any sub directories, passwords inside those sub directories are
 encrypted using those keys. This should be set using the \fBinit\fP command.
+.TP
+.B ~/.password-store/.extensions
+The directory containing extension files.
 
 .SH ENVIRONMENT VARIABLES
 
@@ -392,14 +409,9 @@ Overrides the default password storage directory.
 .TP
 .I PASSWORD_STORE_KEY
 Overrides the default gpg key identification set by \fBinit\fP. Keys must not
-contain spaces and thus use of the hexidecimal key signature is recommended.
+contain spaces and thus use of the hexadecimal key signature is recommended.
 Multiple keys may be specified separated by spaces. 
 .TP
-.I PASSWORD_STORE_GIT
-Overrides the default root of the git repository, which is helpful if
-\fIPASSWORD_STORE_DIR\fP is temporarily set to a sub-directory of the default
-password store.
-.TP
 .I PASSWORD_STORE_GPG_OPTS
 Additional options to be passed to all invocations of GPG.
 .TP
@@ -415,13 +427,45 @@ Specifies the number of seconds to wait before restoring the clipboard, by defau
 .I PASSWORD_STORE_UMASK
 Sets the umask of all files modified by pass, by default \fI077\fP.
 .TP
+.I PASSWORD_STORE_GENERATED_LENGTH
+The default password length if the \fIpass-length\fP parameter to \fBgenerate\fP
+is unspecified.
+.TP
+.I PASSWORD_STORE_CHARACTER_SET
+The character set to be used in password generation for \fBgenerate\fP. This value
+is to be interpreted by \fBtr\fP. See
+.BR tr (1)
+for more info.
+.TP
+.I PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS
+The character set to be used in no-symbol password generation for \fBgenerate\fP,
+when \fI--no-symbols\fP, \fI-n\fP is specified. This value is to be interpreted
+by \fBtr\fP. See
+.BR tr (1)
+for more info.
+.TP
+.I PASSWORD_STORE_ENABLE_EXTENSIONS
+This environment variable must be set to "true" for extensions to be enabled.
+.TP
+.I PASSWORD_STORE_EXTENSIONS_DIR
+The location to look for executable extension files, by default
+\fIPASSWORD_STORE_DIR/.extensions\fP.
+.TP
+.I PASSWORD_STORE_SIGNING_KEY
+If this environment variable is set, then all \fB.gpg-id\fP files and non-system extension files
+must be signed using a detached signature using the GPG key specified by the full 40 character
+upper-case fingerprint in this variable. If multiple fingerprints are specified, each
+separated by a whitespace character, then signatures must match at least one.
+The \fBinit\fP command will keep signatures of \fB.gpg-id\fP files up to date.
+.TP
 .I EDITOR
 The location of the text editor used by \fBedit\fP.
 .SH SEE ALSO
 .BR gpg2 (1),
-.BR pwgen (1),
+.BR tr (1),
 .BR git (1),
-.BR xclip (1).
+.BR xclip (1),
+.BR qrencode (1).
 
 .SH AUTHOR
 .B pass

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