From c9f8d31bcaa8c3cfc71996e9e9ae3b8fd5d5c773 Mon Sep 17 00:00:00 2001 From: David Adam Date: Mon, 6 Mar 2017 23:30:09 +0800 Subject: [PATCH] pass: bump to latest release --- pass/bin/pass | 245 ++++++++++++++++++++++++++----------- pass/share/man/man1/pass.1 | 92 ++++++++++---- 2 files changed, 242 insertions(+), 95 deletions(-) diff --git a/pass/bin/pass b/pass/bin/pass index 564bec0..1006659 100755 --- a/pass/bin/pass +++ b/pass/bin/pass @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright (C) 2012 - 2014 Jason A. Donenfeld . All Rights Reserved. +# Copyright (C) 2012 - 2017 Jason A. Donenfeld . 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 diff --git a/pass/share/man/man1/pass.1 b/pass/share/man/man1/pass.1 index e1fe605..e842178 100644 --- a/pass/share/man/man1/pass.1 +++ b/pass/share/man/man1/pass.1 @@ -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 -- 2.20.1