Skip to content
Snippets Groups Projects
password-store.sh 13.2 KiB
Newer Older
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
# Copyright (C) 2012 - 2014 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}"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

GPG_OPTS="--quiet --yes --compress-algo=none"
GPG="gpg"
which gpg2 &>/dev/null && GPG="gpg2"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS="$GPG_OPTS --batch --use-agent"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	cat <<-_EOF
	============================================
	= pass: the standard unix password manager =
	=                                          =
	=                   v1.5                   =
	=                                          =
	=             Jason A. Donenfeld           =
	=               Jason@zx2c4.com            =
	=                                          =
	= http://zx2c4.com/projects/password-store =
	============================================
	_EOF
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
usage() {
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	echo
	cat <<-_EOF
	Usage:
	    $program init [--reencrypt,-e] [--path=subfolder,-p subfolder] gpg-id...
		Initialize new password storage and use gpg-id for encryption.
		Optionally reencrypt existing passwords using new gpg-id.
	    $program [ls] [subfolder]
		List passwords.
	    $program find pass-names...
	    	List passwords that match pass-names.
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	    $program [show] [--clip,-c] 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 insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
		Insert new password. Optionally, echo the password back to the console
		during entry. Or, optionally, the entry may be multiline. Prompt before
		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] [--force,-f] pass-name pass-length
		Generate a new password of pass-length with optionally no symbols.
		Optionally put it on the clipboard and clear board after 45 seconds.
		Prompt before overwriting existing password unless forced.
	    $program rm [--recursive,-r] [--force,-f] pass-name
		Remove existing password or directory, optionally forcefully.
	    $program git git-command-args...
		If the password store is a git repository, execute a git command
		specified by git-command-args.
	    $program help
		Show this text.
	    $program version
		Show version information.
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	More information may be found in the pass(1) man page.
	_EOF
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
}
is_command() {
	case "$1" in
		init|ls|list|find|search|show|insert|edit|generate|remove|rm|delete|git|help|--help|version|--version) return 0 ;;
		*) return 1 ;;
	esac
}
git_add_file() {
	[[ -d $GIT_DIR ]] || return
	git add "$1" || return
	[[ -n $(git status --porcelain "$1") ]] || return
	[[ $(git config --bool --get pass.signcommits) == "true" ]] && sign="-S" || sign=""
	git commit $sign -m "$2"
	read -r -p "$1 [y/N] " response
	[[ $response == [yY] ]] || exit 1
set_gpg_recipients() {
	gpg_recipient_args=( )

	if [[ -n $PASSWORD_STORE_KEY ]]; then
		for gpg_id in $PASSWORD_STORE_KEY; do
			gpg_recipient_args+=( "-r" "$gpg_id" )
		done
		return
	fi

	local current="$PREFIX/$1"
	while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
		current="${current%/*}"
	done
	current="$current/.gpg-id"

	if [[ ! -f $current ]]; then
		cat <<-_EOF
		ERROR: You must run:
		    $program init your-gpg-id
		before you may use the password store.

		_EOF
		usage
		exit 1
	fi

	while read -r gpg_id; do
		gpg_recipient_args+=( "-r" "$gpg_id" )
	done < "$current"
}

	# This base64 business is a disgusting hack to deal with newline inconsistancies
	# in shell. There must be a better way to deal with this, but because I'm a dolt,
	# we're going with this for now.

	sleep_argv0="password store sleep on display $DISPLAY"
	pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
	before="$(xclip -o -selection "$X_SELECTION" | base64)"
	echo -n "$1" | xclip -selection "$X_SELECTION"
		( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
		now="$(xclip -o -selection "$X_SELECTION" | base64)"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		[[ $now != $(echo -n "$1" | base64) ]] && before="$now"
		# It might be nice to programatically check to see if klipper exists,
		# as well as checking for other common clipboard managers. But for now,
		# this works fine -- if qdbus isn't there or if klipper isn't running,
		# this essentially becomes a no-op.
		#
		# Clipboard managers frequently write their history out in plaintext,
		# so we axe it here:
		qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null

		echo "$before" | base64 -d | xclip -selection "$X_SELECTION"
	echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
tmpdir() {
	if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
jbeta's avatar
jbeta committed
		tmp_dir="$(TMPDIR=/dev/shm mktemp -d -t "$template")"
		yesno "$(echo    "Your system does not have /dev/shm, which means that it may"
		         echo    "be difficult to entirely erase the temporary non-encrypted"
		         echo    "password file after editing. Are you sure you would like to"
jbeta's avatar
jbeta committed
		tmp_dir="$(mktemp -d -t "$template")"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
SHRED="shred -f -z"

# source /path/to/platform-defined-functions
#
# END Platform definable
#

program="${0##*/}"
command="$1"
if is_command "$command"; then
	shift
else
	command="show"
case "$command" in
	init)
		opts="$($GETOPT -o ep: -l reencrypt,path: -n "$program" -- "$@")"
		err=$?
		eval set -- "$opts"
		while true; do case $1 in
			-e|--reencrypt) reencrypt=1; shift ;;
			-p|--path) id_path="$2"; shift 2 ;;
			--) shift; break ;;
		esac done

		if [[ $err -ne 0 || $# -lt 1 ]]; then
			echo "Usage: $program $command [--reencrypt,-e] [--path=subfolder,-p subfolder] gpg-id..."
		if [[ -n $id_path && ! -d $PREFIX/$id_path ]]; then
			if [[ -e $PREFIX/$id_path ]]; then
				echo "Error: $PREFIX/$id_path exists but is not a directory."
				exit 1;
			fi
		fi
		mkdir -v -p "$PREFIX/$id_path"
		gpg_id="$PREFIX/$id_path/.gpg-id"
		printf "%s\n" "$@" > "$gpg_id"
		id_print="$(printf "%s, " "$@")"
		echo "Password store initialized for ${id_print%, }"
		git_add_file "$gpg_id" "Set GPG id to ${id_print%, }."

		if [[ $reencrypt -eq 1 ]]; then
			find "$PREFIX/$id_path" -iname '*.gpg' | while read -r passfile; do
				fake_uniqueness_safety="$RANDOM"
				passfile_dir=${passfile%/*}
				passfile_dir=${passfile_dir#$PREFIX}
				passfile_dir=${passfile_dir#/}
				set_gpg_recipients "$passfile_dir"
				$GPG -d $GPG_OPTS "$passfile" | $GPG -e "${gpg_recipient_args[@]}" -o "$passfile.new.$fake_uniqueness_safety" $GPG_OPTS &&
				mv -v "$passfile.new.$fake_uniqueness_safety" "$passfile"
			git_add_file "$PREFIX/$id_path" "Reencrypted password store using new GPG id ${id_print}."
	version|--version)
		version
		exit 0
		;;
	show|ls|list)
		opts="$($GETOPT -o c -l clip -n "$program" -- "$@")"
		err=$?
		eval set -- "$opts"
		while true; do case $1 in
			-c|--clip) clip=1; shift ;;
			--) shift; break ;;
		esac done

		if [[ $err -ne 0 ]]; then
			echo "Usage: $program $command [--clip,-c] [pass-name]"
			exit 1
		path="$1"
		passfile="$PREFIX/$path.gpg"
		if [[ -f $passfile ]]; then
			if [[ $clip -eq 0 ]]; then
				exec $GPG -d $GPG_OPTS "$passfile"
				pass="$($GPG -d $GPG_OPTS "$passfile" | head -n 1)"
				[[ -n $pass ]] || exit 1
				clip "$pass" "$path"
		elif [[ -d $PREFIX/$path ]]; then
			if [[ -z $path ]]; then
				echo "Password Store"
			else
				echo "${path%\/}"
			fi
			tree -l --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\.gpg$//'
		else
			echo "$path is not in the password store."
			exit 1
	find|search)
		if [[ -z "$@" ]]; then
			echo "Usage: $program $command pass-names..."
			exit 1
		fi
		if ! tree --help |& grep -q "^  --matchdirs"; then
			echo "ERROR: $program: incompatible tree command"
			echo
			echo "Your version of the tree command is missing the relevent patch to add the"
			echo "--matchdirs switch. Please ask your distribution to patch your version of"
			echo "tree with:"
			echo "   http://git.zx2c4.com/password-store/plain/contrib/tree-1.6.0-matchdirs.patch"
			echo "Sorry for the inconvenience."
			exit 1
		fi
		terms="$@"
		echo "Search Terms: $terms"
		tree -l --noreport -P "*${terms// /*|*}*" --prune --matchdirs "$PREFIX" | tail -n +2 | sed 's/\.gpg$//'
		;;
		multiline=0
		opts="$($GETOPT -o mef -l multiline,echo,force -n "$program" -- "$@")"
		err=$?
		eval set -- "$opts"
		while true; do case $1 in
			-m|--multiline) multiline=1; shift ;;
			-e|--echo) noecho=0; shift ;;
			--) shift; break ;;
		esac done

		if [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]]; then
			echo "Usage: $program $command [--echo,-e | --multiline,-m] [--force,-f] pass-name"
			exit 1
		fi
		path="$1"
		passfile="$PREFIX/$path.gpg"

		[[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
		mkdir -p -v "$PREFIX/$(dirname "$path")"
		set_gpg_recipients "$(dirname "$path")"
		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
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		elif [[ $noecho -eq 1 ]]; then
			while true; do
				read -r -p "Enter password for $path: " -s password
				read -r -p "Retype password for $path: " -s password_again
				if [[ $password == "$password_again" ]]; then
					$GPG -e "${gpg_recipient_args[@]}" -o "$passfile" $GPG_OPTS <<<"$password"
					break
				else
					echo "Error: the entered passwords do not match."
				fi
			done
		else
			read -r -p "Enter password for $path: " -e password
			$GPG -e "${gpg_recipient_args[@]}" -o "$passfile" $GPG_OPTS <<<"$password"
		git_add_file "$passfile" "Added given password for $path to store."
		;;
	edit)
		if [[ $# -ne 1 ]]; then
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
			echo "Usage: $program $command pass-name"
			exit 1
		fi

		path="$1"
		mkdir -p -v "$PREFIX/$(dirname "$path")"
		set_gpg_recipients "$(dirname "$path")"
		passfile="$PREFIX/$path.gpg"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		template="$program.XXXXXXXXXXXXX"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		trap '$SHRED "$tmp_file"; rm -rf "$tmp_dir" "$tmp_file"' INT TERM EXIT
		tmpdir #Defines $tmp_dir
		tmp_file="$(TMPDIR="$tmp_dir" mktemp -t "$template")"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

		action="Added"
		if [[ -f $passfile ]]; then
			$GPG -d -o "$tmp_file" $GPG_OPTS "$passfile" || exit 1
			action="Edited"
rupa's avatar
rupa committed
		${EDITOR:-vi} "$tmp_file"
		while ! $GPG -e "${gpg_recipient_args[@]}" -o "$passfile" $GPG_OPTS "$tmp_file"; do
			echo "GPG encryption failed. Retrying."
			sleep 1
		done
		git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}."
		;;
	generate)
		clip=0
		symbols="-y"
		opts="$($GETOPT -o ncf -l no-symbols,clip,force -n "$program" -- "$@")"
		err=$?
		eval set -- "$opts"
		while true; do case $1 in
			-n|--no-symbols) symbols=""; shift ;;
			-c|--clip) clip=1; shift ;;
			--) shift; break ;;
		esac done

		if [[ $err -ne 0 || $# -ne 2 ]]; then
			echo "Usage: $program $command [--no-symbols,-n] [--clip,-c] [--force,-f] pass-name pass-length"
			exit 1
		fi
		path="$1"
		length="$2"
		if [[ ! $length =~ ^[0-9]+$ ]]; then
			echo "pass-length \"$length\" must be a number."
			exit 1
		fi
		mkdir -p -v "$PREFIX/$(dirname "$path")"
		set_gpg_recipients "$(dirname "$path")"
		passfile="$PREFIX/$path.gpg"

		[[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"

		pass="$(pwgen -s $symbols $length 1)"
		[[ -n $pass ]] || exit 1
		$GPG -e "${gpg_recipient_args[@]}" -o "$passfile" $GPG_OPTS <<<"$pass"
		git_add_file "$passfile" "Added generated password for $path to store."
		if [[ $clip -eq 0 ]]; then
			echo "The generated password to $path is:"
			echo "$pass"
		else
			clip "$pass" "$path"
		fi
		;;
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	delete|rm|remove)
		force=0
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

		opts="$($GETOPT -o rf -l recursive,force -n "$program" -- "$@")"
		err=$?
		eval set -- "$opts"
		while true; do case $1 in
			-r|--recursive) recursive="-r"; shift ;;
			-f|--force) force=1; shift ;;
			--) shift; break ;;
		esac done
		if [[ $# -ne 1 ]]; then
			echo "Usage: $program $command [--recursive,-r] [--force,-f] pass-name"
			exit 1
		passfile="$PREFIX/${path%/}"
		if [[ ! -d $passfile ]]; then
			passfile="$PREFIX/$path.gpg"
			if [[ ! -f $passfile ]]; then
				echo "$path is not in the password store."
				exit 1
			fi

		[[ $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"
			git commit -m "Removed $path from store."
		fi
		;;
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
	git)
		if [[ $1 == "init" ]]; then
			git "$@" || exit 1
			git_add_file "$PREFIX" "Added current contents of password store."
		elif [[ -d $GIT_DIR ]]; then
			exec git "$@"
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		else
			echo "Error: the password store is not a git repository."
			exit 1
		fi
		;;
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		exit 1