Skip to content
Snippets Groups Projects
password-store.sh 13.8 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

#
# BEGIN helper functions
#
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"
	local response
	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
		cmd_usage
		exit 1
	fi

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

# END helper functions

#
# BEGIN platform definable
#

	# 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.

	local sleep_argv0="password store sleep on display $DISPLAY"
	pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
	local before="$(xclip -o -selection "$X_SELECTION" | base64)"
	echo -n "$1" | xclip -selection "$X_SELECTION"
		( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
		local 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
#
# BEGIN subcommand functions
#
cmd_version() {
	cat <<-_EOF
	============================================
	= pass: the standard unix password manager =
	=                                          =
	=                   v1.5                   =
	=                                          =
	=             Jason A. Donenfeld           =
	=               Jason@zx2c4.com            =
	=                                          =
	= http://zx2c4.com/projects/password-store =
	============================================
	_EOF
}
cmd_usage() {
	cmd_version
	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.
	    $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.
	More information may be found in the pass(1) man page.
	_EOF
}
cmd_init() {
	local reencrypt=0
	local id_path=""

	local opts
	opts="$($GETOPT -o ep: -l reencrypt,path: -n "$program" -- "$@")"
	local 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..."
		exit 1
	fi
	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;
	mkdir -v -p "$PREFIX/$id_path"
	local gpg_id="$PREFIX/$id_path/.gpg-id"
	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%, }."

	if [[ $reencrypt -eq 1 ]]; then
		local passfile
		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"
		done
		git_add_file "$PREFIX/$id_path" "Reencrypted password store using new GPG id ${id_print}."
	fi
}
cmd_show() {
	local clip=0
	local opts
	opts="$($GETOPT -o c -l clip -n "$program" -- "$@")"
	local err=$?
	eval set -- "$opts"
	while true; do case $1 in
		-c|--clip) clip=1; shift ;;
		--) shift; break ;;
	esac done
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed

	if [[ $err -ne 0 ]]; then
		echo "Usage: $program $command [--clip,-c] [pass-name]"
		exit 1
	fi
	local path="$1"
	local passfile="$PREFIX/$path.gpg"
	if [[ -f $passfile ]]; then
		if [[ $clip -eq 0 ]]; then
			exec $GPG -d $GPG_OPTS "$passfile"
			local pass="$($GPG -d $GPG_OPTS "$passfile" | head -n 1)"
			[[ -n $pass ]] || exit 1
			clip "$pass" "$path"
		fi
	elif [[ -d $PREFIX/$path ]]; then
		if [[ -z $path ]]; then
			echo "Password Store"
		else
			echo "${path%\/}"
		tree -l --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\.gpg$//'
	else
		echo "$path is not in the password store."
		exit 1
	fi
}

cmd_find() {
	if [[ -z "$@" ]]; then
		echo "Usage: $program $command pass-names..."
		exit 1
	fi
	if ! tree --version | grep -q "Jason A. Donenfeld"; 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 and --caseinsensitive switches. Please ask your distribution"
		echo "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
	local terms="$@"
	echo "Search Terms: $terms"
	tree -l --noreport -P "*${terms// /*|*}*" --prune --matchdirs --caseinsensitive "$PREFIX" | tail -n +2 | sed 's/\.gpg$//'
}

cmd_insert() {
	local multiline=0
	local noecho=1
	local force=0

	local opts
	opts="$($GETOPT -o mef -l multiline,echo,force -n "$program" -- "$@")"
	local err=$?
	eval set -- "$opts"
	while true; do case $1 in
		-m|--multiline) multiline=1; shift ;;
		-e|--echo) noecho=0; shift ;;
		-f|--force) force=1; 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
	local path="$1"
	local 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
	elif [[ $noecho -eq 1 ]]; then
		local password
		local password_again
		while true; do
			read -r -p "Enter password for $path: " -s password
			echo
			read -r -p "Retype password for $path: " -s password_again
			echo
			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."
		done
	else
		local password
		read -r -p "Enter password for $path: " -e password
		$GPG -e "${gpg_recipient_args[@]}" -o "$passfile" $GPG_OPTS <<<"$password"
	fi
	git_add_file "$passfile" "Added given password for $path to store."
}
cmd_edit() {
	if [[ $# -ne 1 ]]; then
		echo "Usage: $program $command pass-name"
		exit 1
	fi
	local path="$1"
	mkdir -p -v "$PREFIX/$(dirname "$path")"
	set_gpg_recipients "$(dirname "$path")"
	local passfile="$PREFIX/$path.gpg"
	local template="$program.XXXXXXXXXXXXX"

	trap '$SHRED "$tmp_file"; rm -rf "$tmp_dir" "$tmp_file"' INT TERM EXIT

	tmpdir #Defines $tmp_dir
	local tmp_file="$(TMPDIR="$tmp_dir" mktemp -t "$template")"

	local action="Added"
	if [[ -f $passfile ]]; then
		$GPG -d -o "$tmp_file" $GPG_OPTS "$passfile" || exit 1
		action="Edited"
	fi
	${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}."
}

cmd_generate() {
	local clip=0
	local force=0
	local symbols="-y"

	local opts
	opts="$($GETOPT -o ncf -l no-symbols,clip,force -n "$program" -- "$@")"
	local err=$?
	eval set -- "$opts"
	while true; do case $1 in
		-n|--no-symbols) symbols=""; shift ;;
		-c|--clip) clip=1; shift ;;
		-f|--force) force=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
	local path="$1"
	local 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")"
	local passfile="$PREFIX/$path.gpg"

	[[ $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
	$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
}

cmd_delete() {
	local recursive=""
	local force=0

	local opts
	opts="$($GETOPT -o rf -l recursive,force -n "$program" -- "$@")"
	local 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
	fi
	local path="$1"

	local passfile="$PREFIX/${path%/}"
	if [[ ! -d $passfile ]]; then
		passfile="$PREFIX/$path.gpg"
		if [[ ! -f $passfile ]]; then
			echo "$path is not in the password store."
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
			exit 1
		fi
	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
}

cmd_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 "$@"
	else
		echo "Error: the password store is not a git repository."
Jason A. Donenfeld's avatar
Jason A. Donenfeld committed
		exit 1
	fi
}

#
# END subcommand functions
#

program="${0##*/}"
command="$1"

case "$1" in
	init) shift;			cmd_init "$@"; ;;
	help|--help) shift;		cmd_usage "$@"; ;;
	version|--version) shift;	cmd_version "$@"; ;;
	show|ls|list) shift;		cmd_show "$@"; ;;
	find|search) shift;		cmd_find "$@"; ;;
	insert) shift;			cmd_insert "$@"; ;;
	edit) shift;			cmd_edit "$@"; ;;
	generate) shift;		cmd_generate "$@"; ;;
	delete|rm|remove) shift;	cmd_delete "$@"; ;;
	git) shift;			cmd_git "$@"; ;;
	-*) shift;			cmd_usage "$@"; exit 1; ;;
	*) command="show";		cmd_show "$@"; ;;