password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

password-store.sh (26050B)


      1 #!/usr/bin/env bash
      2 
      3 # Copyright (C) 2012 - 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 command -v 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:-[:punct:][:alnum:]}"
     21 CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}"
     22 
     23 unset GIT_DIR GIT_WORK_TREE GIT_NAMESPACE GIT_INDEX_FILE GIT_INDEX_VERSION GIT_OBJECT_DIRECTORY GIT_COMMON_DIR
     24 export GIT_CEILING_DIRECTORIES="$PREFIX/.."
     25 
     26 #
     27 # BEGIN helper functions
     28 #
     29 
     30 set_git() {
     31 	INNER_GIT_DIR="${1%/*}"
     32 	while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do
     33 		INNER_GIT_DIR="${INNER_GIT_DIR%/*}"
     34 	done
     35 	[[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR=""
     36 }
     37 git_add_file() {
     38 	[[ -n $INNER_GIT_DIR ]] || return
     39 	git -C "$INNER_GIT_DIR" add "$1" || return
     40 	[[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$1") ]] || return
     41 	git_commit "$2"
     42 }
     43 git_commit() {
     44 	local sign=""
     45 	[[ -n $INNER_GIT_DIR ]] || return
     46 	[[ $(git -C "$INNER_GIT_DIR" config --bool --get pass.signcommits) == "true" ]] && sign="-S"
     47 	git -C "$INNER_GIT_DIR" commit $sign -m "$1"
     48 }
     49 yesno() {
     50 	[[ -t 0 ]] || return 0
     51 	local response
     52 	read -r -p "$1 [y/N] " response
     53 	[[ $response == [yY] ]] || exit 1
     54 }
     55 die() {
     56 	echo "$@" >&2
     57 	exit 1
     58 }
     59 verify_file() {
     60 	[[ -n $PASSWORD_STORE_SIGNING_KEY ]] || return 0
     61 	[[ -f $1.sig ]] || die "Signature for $1 does not exist."
     62 	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')"
     63 	local fingerprint found=0
     64 	for fingerprint in $PASSWORD_STORE_SIGNING_KEY; do
     65 		[[ $fingerprint =~ ^[A-F0-9]{40}$ ]] || continue
     66 		[[ $fingerprints == *$fingerprint* ]] && { found=1; break; }
     67 	done
     68 	[[ $found -eq 1 ]] || die "Signature for $1 is invalid."
     69 }
     70 set_gpg_recipients() {
     71 	GPG_RECIPIENT_ARGS=( )
     72 	GPG_RECIPIENTS=( )
     73 	local gpg_id
     74 
     75 	if [[ -n $PASSWORD_STORE_KEY ]]; then
     76 		for gpg_id in $PASSWORD_STORE_KEY; do
     77 			GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
     78 			GPG_RECIPIENTS+=( "$gpg_id" )
     79 		done
     80 		return
     81 	fi
     82 
     83 	local current="$PREFIX/$1"
     84 	while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
     85 		current="${current%/*}"
     86 	done
     87 	current="$current/.gpg-id"
     88 
     89 	if [[ ! -f $current ]]; then
     90 		cat >&2 <<-_EOF
     91 		Error: You must run:
     92 		    $PROGRAM init your-gpg-id
     93 		before you may use the password store.
     94 
     95 		_EOF
     96 		cmd_usage
     97 		exit 1
     98 	fi
     99 
    100 	verify_file "$current"
    101 
    102 	while read -r gpg_id; do
    103 		gpg_id="${gpg_id%%#*}" # strip comment
    104 		[[ -n $gpg_id ]] || continue
    105 		GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
    106 		GPG_RECIPIENTS+=( "$gpg_id" )
    107 	done < "$current"
    108 }
    109 
    110 reencrypt_path() {
    111 	local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile
    112 	local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")"
    113 	while read -r -d "" passfile; do
    114 		[[ -L $passfile ]] && continue
    115 		local passfile_dir="${passfile%/*}"
    116 		passfile_dir="${passfile_dir#$PREFIX}"
    117 		passfile_dir="${passfile_dir#/}"
    118 		local passfile_display="${passfile#$PREFIX/}"
    119 		passfile_display="${passfile_display%.gpg}"
    120 		local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
    121 
    122 		set_gpg_recipients "$passfile_dir"
    123 		if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
    124 			for index in "${!GPG_RECIPIENTS[@]}"; do
    125 				local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)"
    126 				[[ -z $group ]] && continue
    127 				IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
    128 				unset "GPG_RECIPIENTS[$index]"
    129 			done
    130 			gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/^sub:[^idr:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)"
    131 		fi
    132 		current_keys="$(LC_ALL=C $GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$passfile" 2>&1 | sed -nE 's/^gpg: public key is ([A-F0-9]+)$/\1/p' | LC_ALL=C sort -u)"
    133 
    134 		if [[ $gpg_keys != "$current_keys" ]]; then
    135 			echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
    136 			$GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
    137 			mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
    138 		fi
    139 		prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
    140 	done < <(find "$1" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0)
    141 }
    142 check_sneaky_paths() {
    143 	local path
    144 	for path in "$@"; do
    145 		[[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
    146 	done
    147 }
    148 
    149 #
    150 # END helper functions
    151 #
    152 
    153 #
    154 # BEGIN platform definable
    155 #
    156 
    157 clip() {
    158 	if [[ -n $WAYLAND_DISPLAY ]] && command -v wl-copy &> /dev/null; then
    159 		local copy_cmd=( wl-copy )
    160 		local paste_cmd=( wl-paste -n )
    161 		if [[ $X_SELECTION == primary ]]; then
    162 			copy_cmd+=( --primary )
    163 			paste_cmd+=( --primary )
    164 		fi
    165 		local display_name="$WAYLAND_DISPLAY"
    166 	elif [[ -n $DISPLAY ]] && command -v xclip &> /dev/null; then
    167 		local copy_cmd=( xclip -selection "$X_SELECTION" )
    168 		local paste_cmd=( xclip -o -selection "$X_SELECTION" )
    169 		local display_name="$DISPLAY"
    170 	else
    171 		die "Error: No X11 or Wayland display and clipper detected"
    172 	fi
    173 	local sleep_argv0="password store sleep on display $display_name"
    174 
    175 	# This base64 business is because bash cannot store binary data in a shell
    176 	# variable. Specifically, it cannot store nulls nor (non-trivally) store
    177 	# trailing new lines.
    178 	pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
    179 	local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)"
    180 	echo -n "$1" | "${copy_cmd[@]}" || die "Error: Could not copy data to the clipboard"
    181 	(
    182 		( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" )
    183 		local now="$("${paste_cmd[@]}" | $BASE64)"
    184 		[[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now"
    185 
    186 		# It might be nice to programatically check to see if klipper exists,
    187 		# as well as checking for other common clipboard managers. But for now,
    188 		# this works fine -- if qdbus isn't there or if klipper isn't running,
    189 		# this essentially becomes a no-op.
    190 		#
    191 		# Clipboard managers frequently write their history out in plaintext,
    192 		# so we axe it here:
    193 		qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null
    194 
    195 		echo "$before" | $BASE64 -d | "${copy_cmd[@]}"
    196 	) >/dev/null 2>&1 & disown
    197 	echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
    198 }
    199 
    200 qrcode() {
    201 	if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then
    202 		if type feh >/dev/null 2>&1; then
    203 			echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 -
    204 			return
    205 		elif type gm >/dev/null 2>&1; then
    206 			echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 -
    207 			return
    208 		elif type display >/dev/null 2>&1; then
    209 			echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 -
    210 			return
    211 		fi
    212 	fi
    213 	echo -n "$1" | qrencode -t utf8
    214 }
    215 
    216 tmpdir() {
    217 	[[ -n $SECURE_TMPDIR ]] && return
    218 	local warn=1
    219 	[[ $1 == "nowarn" ]] && warn=0
    220 	local template="$PROGRAM.XXXXXXXXXXXXX"
    221 	if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
    222 		SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")"
    223 		remove_tmpfile() {
    224 			rm -rf "$SECURE_TMPDIR"
    225 		}
    226 		trap remove_tmpfile EXIT
    227 	else
    228 		[[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF
    229 		Your system does not have /dev/shm, which means that it may
    230 		be difficult to entirely erase the temporary non-encrypted
    231 		password file after editing.
    232 
    233 		Are you sure you would like to continue?
    234 		_EOF
    235 		)"
    236 		SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
    237 		shred_tmpfile() {
    238 			find "$SECURE_TMPDIR" -type f -exec $SHRED {} +
    239 			rm -rf "$SECURE_TMPDIR"
    240 		}
    241 		trap shred_tmpfile EXIT
    242 	fi
    243 
    244 }
    245 GETOPT="getopt"
    246 SHRED="shred -f -z"
    247 BASE64="base64"
    248 
    249 source "$(dirname "$0")/platform/$(uname | cut -d _ -f 1 | tr '[:upper:]' '[:lower:]').sh" 2>/dev/null # PLATFORM_FUNCTION_FILE
    250 
    251 #
    252 # END platform definable
    253 #
    254 
    255 
    256 #
    257 # BEGIN subcommand functions
    258 #
    259 
    260 cmd_version() {
    261 	cat <<-_EOF
    262 	============================================
    263 	= pass: the standard unix password manager =
    264 	=                                          =
    265 	=                  v1.7.4                  =
    266 	=                                          =
    267 	=             Jason A. Donenfeld           =
    268 	=               Jason@zx2c4.com            =
    269 	=                                          =
    270 	=      http://www.passwordstore.org/       =
    271 	============================================
    272 	_EOF
    273 }
    274 
    275 cmd_usage() {
    276 	cmd_version
    277 	echo
    278 	cat <<-_EOF
    279 	Usage:
    280 	    $PROGRAM init [--path=subfolder,-p subfolder] gpg-id...
    281 	        Initialize new password storage and use gpg-id for encryption.
    282 	        Selectively reencrypt existing passwords using new gpg-id.
    283 	    $PROGRAM [ls] [subfolder]
    284 	        List passwords.
    285 	    $PROGRAM find pass-names...
    286 	    	List passwords that match pass-names.
    287 	    $PROGRAM [show] [--clip[=line-number],-c[line-number]] pass-name
    288 	        Show existing password and optionally put it on the clipboard.
    289 	        If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
    290 	    $PROGRAM grep [GREPOPTIONS] search-string
    291 	        Search for password files containing search-string when decrypted.
    292 	    $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
    293 	        Insert new password. Optionally, echo the password back to the console
    294 	        during entry. Or, optionally, the entry may be multiline. Prompt before
    295 	        overwriting existing password unless forced.
    296 	    $PROGRAM edit pass-name
    297 	        Insert a new password or edit an existing password using ${EDITOR:-vi}.
    298 	    $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length]
    299 	        Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols.
    300 	        Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
    301 	        Prompt before overwriting existing password unless forced.
    302 	        Optionally replace only the first line of an existing file with a new password.
    303 	    $PROGRAM rm [--recursive,-r] [--force,-f] pass-name
    304 	        Remove existing password or directory, optionally forcefully.
    305 	    $PROGRAM mv [--force,-f] old-path new-path
    306 	        Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
    307 	    $PROGRAM cp [--force,-f] old-path new-path
    308 	        Copies old-path to new-path, optionally forcefully, selectively reencrypting.
    309 	    $PROGRAM git git-command-args...
    310 	        If the password store is a git repository, execute a git command
    311 	        specified by git-command-args.
    312 	    $PROGRAM help
    313 	        Show this text.
    314 	    $PROGRAM version
    315 	        Show version information.
    316 
    317 	More information may be found in the pass(1) man page.
    318 	_EOF
    319 }
    320 
    321 cmd_init() {
    322 	local opts id_path=""
    323 	opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")"
    324 	local err=$?
    325 	eval set -- "$opts"
    326 	while true; do case $1 in
    327 		-p|--path) id_path="$2"; shift 2 ;;
    328 		--) shift; break ;;
    329 	esac done
    330 
    331 	[[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..."
    332 	[[ -n $id_path ]] && check_sneaky_paths "$id_path"
    333 	[[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory."
    334 
    335 	local gpg_id="$PREFIX/$id_path/.gpg-id"
    336 	set_git "$gpg_id"
    337 
    338 	if [[ $# -eq 1 && -z $1 ]]; then
    339 		[[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
    340 		rm -v -f "$gpg_id" || exit 1
    341 		if [[ -n $INNER_GIT_DIR ]]; then
    342 			git -C "$INNER_GIT_DIR" rm -qr "$gpg_id"
    343 			git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}."
    344 		fi
    345 		rmdir -p "${gpg_id%/*}" 2>/dev/null
    346 	else
    347 		mkdir -v -p "$PREFIX/$id_path"
    348 		printf "%s\n" "$@" > "$gpg_id"
    349 		local id_print="$(printf "%s, " "$@")"
    350 		echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}"
    351 		git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}."
    352 		if [[ -n $PASSWORD_STORE_SIGNING_KEY ]]; then
    353 			local signing_keys=( ) key
    354 			for key in $PASSWORD_STORE_SIGNING_KEY; do
    355 				signing_keys+=( --default-key $key )
    356 			done
    357 			$GPG "${GPG_OPTS[@]}" "${signing_keys[@]}" --detach-sign "$gpg_id" || die "Could not sign .gpg_id."
    358 			key="$($GPG "${GPG_OPTS[@]}" --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')"
    359 			[[ -n $key ]] || die "Signing of .gpg_id unsuccessful."
    360 			git_add_file "$gpg_id.sig" "Signing new GPG id with ${key//[$IFS]/,}."
    361 		fi
    362 	fi
    363 
    364 	reencrypt_path "$PREFIX/$id_path"
    365 	git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}."
    366 }
    367 
    368 cmd_show() {
    369 	local opts selected_line clip=0 qrcode=0
    370 	opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")"
    371 	local err=$?
    372 	eval set -- "$opts"
    373 	while true; do case $1 in
    374 		-q|--qrcode) qrcode=1; selected_line="${2:-1}"; shift 2 ;;
    375 		-c|--clip) clip=1; selected_line="${2:-1}"; shift 2 ;;
    376 		--) shift; break ;;
    377 	esac done
    378 
    379 	[[ $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]"
    380 
    381 	local pass
    382 	local path="$1"
    383 	local passfile="$PREFIX/$path.gpg"
    384 	check_sneaky_paths "$path"
    385 	if [[ -f $passfile ]]; then
    386 		if [[ $clip -eq 0 && $qrcode -eq 0 ]]; then
    387 			pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | $BASE64)" || exit $?
    388 			echo "$pass" | $BASE64 -d
    389 		else
    390 			[[ $selected_line =~ ^[0-9]+$ ]] || die "Clip location '$selected_line' is not a number."
    391 			pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +${selected_line} | head -n 1)" || exit $?
    392 			[[ -n $pass ]] || die "There is no password to put on the clipboard at line ${selected_line}."
    393 			if [[ $clip -eq 1 ]]; then
    394 				clip "$pass" "$path"
    395 			elif [[ $qrcode -eq 1 ]]; then
    396 				qrcode "$pass" "$path"
    397 			fi
    398 		fi
    399 	elif [[ -d $PREFIX/$path ]]; then
    400 		if [[ -z $path ]]; then
    401 			echo "Password Store"
    402 		else
    403 			echo "${path%\/}"
    404 		fi
    405 		tree -N -C -l --noreport "$PREFIX/$path" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' # remove .gpg at end of line, but keep colors
    406 	elif [[ -z $path ]]; then
    407 		die "Error: password store is empty. Try \"pass init\"."
    408 	else
    409 		die "Error: $path is not in the password store."
    410 	fi
    411 }
    412 
    413 cmd_find() {
    414 	[[ $# -eq 0 ]] && die "Usage: $PROGRAM $COMMAND pass-names..."
    415 	IFS="," eval 'echo "Search Terms: $*"'
    416 	local terms="*$(printf '%s*|*' "$@")"
    417 	tree -N -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g'
    418 }
    419 
    420 cmd_grep() {
    421 	[[ $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [GREPOPTIONS] search-string"
    422 	local passfile grepresults
    423 	while read -r -d "" passfile; do
    424 		grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$@")"
    425 		[[ $? -ne 0 ]] && continue
    426 		passfile="${passfile%.gpg}"
    427 		passfile="${passfile#$PREFIX/}"
    428 		local passfile_dir="${passfile%/*}/"
    429 		[[ $passfile_dir == "${passfile}/" ]] && passfile_dir=""
    430 		passfile="${passfile##*/}"
    431 		printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
    432 		echo "$grepresults"
    433 	done < <(find -L "$PREFIX" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0)
    434 }
    435 
    436 cmd_insert() {
    437 	local opts multiline=0 noecho=1 force=0
    438 	opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")"
    439 	local err=$?
    440 	eval set -- "$opts"
    441 	while true; do case $1 in
    442 		-m|--multiline) multiline=1; shift ;;
    443 		-e|--echo) noecho=0; shift ;;
    444 		-f|--force) force=1; shift ;;
    445 		--) shift; break ;;
    446 	esac done
    447 
    448 	[[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
    449 	local path="${1%/}"
    450 	local passfile="$PREFIX/$path.gpg"
    451 	check_sneaky_paths "$path"
    452 	set_git "$passfile"
    453 
    454 	[[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
    455 
    456 	mkdir -p -v "$PREFIX/$(dirname -- "$path")"
    457 	set_gpg_recipients "$(dirname -- "$path")"
    458 
    459 	if [[ $multiline -eq 1 ]]; then
    460 		echo "Enter contents of $path and press Ctrl+D when finished:"
    461 		echo
    462 		$GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
    463 	elif [[ $noecho -eq 1 ]]; then
    464 		local password password_again
    465 		while true; do
    466 			read -r -p "Enter password for $path: " -s password || exit 1
    467 			echo
    468 			read -r -p "Retype password for $path: " -s password_again || exit 1
    469 			echo
    470 			if [[ $password == "$password_again" ]]; then
    471 				echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
    472 				break
    473 			else
    474 				die "Error: the entered passwords do not match."
    475 			fi
    476 		done
    477 	else
    478 		local password
    479 		read -r -p "Enter password for $path: " -e password
    480 		echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
    481 	fi
    482 	git_add_file "$passfile" "Add given password for $path to store."
    483 }
    484 
    485 cmd_edit() {
    486 	[[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name"
    487 
    488 	local path="${1%/}"
    489 	check_sneaky_paths "$path"
    490 	mkdir -p -v "$PREFIX/$(dirname -- "$path")"
    491 	set_gpg_recipients "$(dirname -- "$path")"
    492 	local passfile="$PREFIX/$path.gpg"
    493 	set_git "$passfile"
    494 
    495 	tmpdir #Defines $SECURE_TMPDIR
    496 	local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt"
    497 
    498 	local action="Add"
    499 	if [[ -f $passfile ]]; then
    500 		$GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1
    501 		action="Edit"
    502 	fi
    503 	${EDITOR:-vi} "$tmp_file"
    504 	[[ -f $tmp_file ]] || die "New password not saved."
    505 	$GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged."
    506 	while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do
    507 		yesno "GPG encryption failed. Would you like to try again?"
    508 	done
    509 	git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
    510 }
    511 
    512 cmd_generate() {
    513 	local opts qrcode=0 clip=0 force=0 characters="$CHARACTER_SET" inplace=0 pass
    514 	opts="$($GETOPT -o nqcif -l no-symbols,qrcode,clip,in-place,force -n "$PROGRAM" -- "$@")"
    515 	local err=$?
    516 	eval set -- "$opts"
    517 	while true; do case $1 in
    518 		-n|--no-symbols) characters="$CHARACTER_SET_NO_SYMBOLS"; shift ;;
    519 		-q|--qrcode) qrcode=1; shift ;;
    520 		-c|--clip) clip=1; shift ;;
    521 		-f|--force) force=1; shift ;;
    522 		-i|--in-place) inplace=1; shift ;;
    523 		--) shift; break ;;
    524 	esac done
    525 
    526 	[[ $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]"
    527 	local path="$1"
    528 	local length="${2:-$GENERATED_LENGTH}"
    529 	check_sneaky_paths "$path"
    530 	[[ $length =~ ^[0-9]+$ ]] || die "Error: pass-length \"$length\" must be a number."
    531 	[[ $length -gt 0 ]] || die "Error: pass-length must be greater than zero."
    532 	mkdir -p -v "$PREFIX/$(dirname -- "$path")"
    533 	set_gpg_recipients "$(dirname -- "$path")"
    534 	local passfile="$PREFIX/$path.gpg"
    535 	set_git "$passfile"
    536 
    537 	[[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
    538 
    539 	read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom)
    540 	[[ ${#pass} -eq $length ]] || die "Could not generate password from /dev/urandom."
    541 	if [[ $inplace -eq 0 ]]; then
    542 		echo "$pass" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted."
    543 	else
    544 		local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
    545 		if { echo "$pass"; $GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +2; } | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then
    546 			mv "$passfile_temp" "$passfile"
    547 		else
    548 			rm -f "$passfile_temp"
    549 			die "Could not reencrypt new password."
    550 		fi
    551 	fi
    552 	local verb="Add"
    553 	[[ $inplace -eq 1 ]] && verb="Replace"
    554 	git_add_file "$passfile" "$verb generated password for ${path}."
    555 
    556 	if [[ $clip -eq 1 ]]; then
    557 		clip "$pass" "$path"
    558 	elif [[ $qrcode -eq 1 ]]; then
    559 		qrcode "$pass" "$path"
    560 	else
    561 		printf "\e[1mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass"
    562 	fi
    563 }
    564 
    565 cmd_delete() {
    566 	local opts recursive="" force=0
    567 	opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")"
    568 	local err=$?
    569 	eval set -- "$opts"
    570 	while true; do case $1 in
    571 		-r|--recursive) recursive="-r"; shift ;;
    572 		-f|--force) force=1; shift ;;
    573 		--) shift; break ;;
    574 	esac done
    575 	[[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
    576 	local path="$1"
    577 	check_sneaky_paths "$path"
    578 
    579 	local passdir="$PREFIX/${path%/}"
    580 	local passfile="$PREFIX/$path.gpg"
    581 	[[ -f $passfile && -d $passdir && $path == */ || ! -f $passfile ]] && passfile="${passdir%/}/"
    582 	[[ -e $passfile ]] || die "Error: $path is not in the password store."
    583 	set_git "$passfile"
    584 
    585 	[[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
    586 
    587 	rm $recursive -f -v "$passfile"
    588 	set_git "$passfile"
    589 	if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then
    590 		git -C "$INNER_GIT_DIR" rm -qr "$passfile"
    591 		set_git "$passfile"
    592 		git_commit "Remove $path from store."
    593 	fi
    594 	rmdir -p "${passfile%/*}" 2>/dev/null
    595 }
    596 
    597 cmd_copy_move() {
    598 	local opts move=1 force=0
    599 	[[ $1 == "copy" ]] && move=0
    600 	shift
    601 	opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
    602 	local err=$?
    603 	eval set -- "$opts"
    604 	while true; do case $1 in
    605 		-f|--force) force=1; shift ;;
    606 		--) shift; break ;;
    607 	esac done
    608 	[[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
    609 	check_sneaky_paths "$@"
    610 	local old_path="$PREFIX/${1%/}"
    611 	local old_dir="$old_path"
    612 	local new_path="$PREFIX/$2"
    613 
    614 	if ! [[ -f $old_path.gpg && -d $old_path && $1 == */ || ! -f $old_path.gpg ]]; then
    615 		old_dir="${old_path%/*}"
    616 		old_path="${old_path}.gpg"
    617 	fi
    618 	echo "$old_path"
    619 	[[ -e $old_path ]] || die "Error: $1 is not in the password store."
    620 
    621 	mkdir -p -v "${new_path%/*}"
    622 	[[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg"
    623 
    624 	local interactive="-i"
    625 	[[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
    626 
    627 	set_git "$new_path"
    628 	if [[ $move -eq 1 ]]; then
    629 		mv $interactive -v "$old_path" "$new_path" || exit 1
    630 		[[ -e "$new_path" ]] && reencrypt_path "$new_path"
    631 
    632 		set_git "$new_path"
    633 		if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
    634 			git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
    635 			set_git "$new_path"
    636 			git_add_file "$new_path" "Rename ${1} to ${2}."
    637 		fi
    638 		set_git "$old_path"
    639 		if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then
    640 			git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null
    641 			set_git "$old_path"
    642 			[[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}."
    643 		fi
    644 		rmdir -p "$old_dir" 2>/dev/null
    645 	else
    646 		cp $interactive -r -v "$old_path" "$new_path" || exit 1
    647 		[[ -e "$new_path" ]] && reencrypt_path "$new_path"
    648 		git_add_file "$new_path" "Copy ${1} to ${2}."
    649 	fi
    650 }
    651 
    652 cmd_git() {
    653 	set_git "$PREFIX/"
    654 	if [[ $1 == "init" ]]; then
    655 		INNER_GIT_DIR="$PREFIX"
    656 		git -C "$INNER_GIT_DIR" "$@" || exit 1
    657 		git_add_file "$PREFIX" "Add current contents of password store."
    658 
    659 		echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
    660 		git_add_file .gitattributes "Configure git repository for gpg file diff."
    661 		git -C "$INNER_GIT_DIR" config --local diff.gpg.binary true
    662 		git -C "$INNER_GIT_DIR" config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
    663 	elif [[ -n $INNER_GIT_DIR ]]; then
    664 		tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
    665 		export TMPDIR="$SECURE_TMPDIR"
    666 		git -C "$INNER_GIT_DIR" "$@"
    667 	else
    668 		die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
    669 	fi
    670 }
    671 
    672 cmd_extension_or_show() {
    673 	if ! cmd_extension "$@"; then
    674 		COMMAND="show"
    675 		cmd_show "$@"
    676 	fi
    677 }
    678 
    679 SYSTEM_EXTENSION_DIR=""
    680 cmd_extension() {
    681 	check_sneaky_paths "$1"
    682 	local user_extension system_extension extension
    683 	[[ -n $SYSTEM_EXTENSION_DIR ]] && system_extension="$SYSTEM_EXTENSION_DIR/$1.bash"
    684 	[[ $PASSWORD_STORE_ENABLE_EXTENSIONS == true ]] && user_extension="$EXTENSIONS/$1.bash"
    685 	if [[ -n $user_extension && -f $user_extension && -x $user_extension ]]; then
    686 		verify_file "$user_extension"
    687 		extension="$user_extension"
    688 	elif [[ -n $system_extension && -f $system_extension && -x $system_extension ]]; then
    689 		extension="$system_extension"
    690 	else
    691 		return 1
    692 	fi
    693 	shift
    694 	source "$extension" "$@"
    695 	return 0
    696 }
    697 
    698 #
    699 # END subcommand functions
    700 #
    701 
    702 PROGRAM="${0##*/}"
    703 COMMAND="$1"
    704 
    705 case "$1" in
    706 	init) shift;			cmd_init "$@" ;;
    707 	help|--help) shift;		cmd_usage "$@" ;;
    708 	version|--version) shift;	cmd_version "$@" ;;
    709 	show|ls|list) shift;		cmd_show "$@" ;;
    710 	find|search) shift;		cmd_find "$@" ;;
    711 	grep) shift;			cmd_grep "$@" ;;
    712 	insert|add) shift;		cmd_insert "$@" ;;
    713 	edit) shift;			cmd_edit "$@" ;;
    714 	generate) shift;		cmd_generate "$@" ;;
    715 	delete|rm|remove) shift;	cmd_delete "$@" ;;
    716 	rename|mv) shift;		cmd_copy_move "move" "$@" ;;
    717 	copy|cp) shift;			cmd_copy_move "copy" "$@" ;;
    718 	git) shift;			cmd_git "$@" ;;
    719 	*)				cmd_extension_or_show "$@" ;;
    720 esac
    721 exit 0