Skip to content
Snippets Groups Projects
password-store.sh 12.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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.
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    umask 077
    
    
    GPG_OPTS="--quiet --yes --batch --compress-algo=none"
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
    
    SELECTION="${PASSWORD_STORE_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
    
    
    version() {
    	cat <<_EOF
    |-----------------------|
    |   Password Store      |
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    |       v.1.4.2         |
    
    |       by zx2c4        |
    |                       |
    |    Jason@zx2c4.com    |
    |  Jason A. Donenfeld   |
    |-----------------------|
    _EOF
    }
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    usage() {
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    	cat <<_EOF
    
    Usage:
    
        $program init [--reencrypt,-e] [--path=subfolder,-p subfolder] gpg-id...
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
            Initialize new password storage and use gpg-id for encryption.
    
            Optionally reencrypt existing passwords using new gpg-id.
    
        $program [ls] [subfolder]
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
            List passwords.
    
        $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
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
            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.
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
        $program git git-command-args...
            If the password store is a git repository, execute a git command
    
            specified by git-command-args.
    
        $program help
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
            Show this text.
    
        $program version
            Show version information.
    
    
    More information may be found in the pass(1) man page.
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    _EOF
    }
    
    is_command() {
    
    	case "$1" in
    
    		init|ls|list|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
    		echo "You must run:"
    		echo "    $program init your-gpg-id"
    		echo "before you may use the password store."
    		echo
    		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"
    	kill -9 $(grep -l -r --include="cmdline" "^$sleep_argv0" /proc/ 2>/dev/null | sed -n 's:/proc/\([0-9]\+\)/.*:\1:p' | sort | uniq) 2>/dev/null && sleep 0.1
    
    	before="$(xclip -o -selection "$SELECTION" | base64)"
    	echo -n "$1" | xclip -selection "$SELECTION"
    
    		( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
    
    		now="$(xclip -o -selection "$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 "$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
    				passfile_dir=${passfile%/*}
    				passfile_dir=${passfile_dir#$PREFIX}
    				passfile_dir=${passfile_dir#/}
    				set_gpg_recipients "$passfile_dir"
    				gpg2 -d $GPG_OPTS "$passfile" | gpg2 -e "${gpg_recipient_args[@]}" -o "$passfile.new" $GPG_OPTS &&
    
    				mv -v "$passfile.new" "$passfile"
    			done
    
    			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 gpg2 -d $GPG_OPTS "$passfile"
    
    				pass="$(gpg2 -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
    
    		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
    
    			gpg2 -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
    
    					gpg2 -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
    
    			gpg2 -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
    
    			gpg2 -d -o "$tmp_file" $GPG_OPTS "$passfile" || exit 1
    
    			action="Edited"
    
    rupa's avatar
    rupa committed
    		${EDITOR:-vi} "$tmp_file"
    
    		while ! gpg2 -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
    
    		gpg2 -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