Skip to content
Snippets Groups Projects
password-store.sh 10.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    
    
    # Copyright (C) 2012 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
    
    
    PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    ID="$PREFIX/.gpg-id"
    
    GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
    
    GPG_OPTS="--quiet --yes --batch"
    
    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         |
    
    |       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] 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 45 seconds.
    
        $program insert [--no-echo,-n | --multiline,-m] [--force,-f] pass-name
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
            Insert new password. Optionally, the console can be enabled to not
    
            echo the password back. Or, optionally, it 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 commit -m "$2"
    }
    yesno() {
    	read -p "$1 [y/N] " response
    	[[ $response == "y" || $response == "Y" ]] || exit 1
    }
    
    	# 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.
    
    	before="$(xclip -o -selection clipboard | base64)"
    	echo -n "$1" | xclip -selection clipboard
    	(
    
    		now="$(xclip -o -selection clipboard | base64)"
    		if [[ $now != $(echo -n "$1" | base64) ]]; then
    			before="$now"
    		fi
    
    		# 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 clipboard
    	) & disown
    
    	echo "Copied $2 to clipboard. Will clear in 45 seconds."
    }
    
    tmpdir() {
    	if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then
    		tmp_dir="$(TMPDIR=/dev/shm mktemp -t "$template" -d)"
    	else
    
    		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"
    
    		tmp_dir="$(mktemp -t "$template" -d)"
    	fi
    
    }
    GPG="gpg"
    GETOPT="getopt"
    
    # source /path/to/platform-defined-functions
    #
    # END Platform definable
    #
    
    
    program="$(basename "$0")"
    
    command="$1"
    
    if is_command "$command"; then
    
    	shift
    else
    	command="show"
    
    case "$command" in
    	init)
    
    		reencrypt=0
    
    		opts="$($GETOPT -o e -l reencrypt -n "$program" -- "$@")"
    		err=$?
    		eval set -- "$opts"
    		while true; do case $1 in
    			-e|--reencrypt) reencrypt=1; shift ;;
    			--) shift; break ;;
    		esac done
    
    
    		if [[ $# -ne 1 ]]; then
    
    			echo "Usage: $program $command [--reencrypt,-e] gpg-id"
    
    		gpg_id="$1"
    		mkdir -v -p "$PREFIX"
    
    		echo "$gpg_id" > "$ID"
    		echo "Password store initialized for $gpg_id."
    
    		git_add_file "$ID" "Set GPG id to $gpg_id."
    
    
    		if [[ $reencrypt -eq 1 ]]; then
    			find "$PREFIX" -iname '*.gpg' | while read passfile; do
    				$GPG -d $GPG_OPTS "$passfile" | $GPG -e -r "$gpg_id" -o "$passfile.new" $GPG_OPTS &&
    				mv -v "$passfile.new" "$passfile"
    			done
    			git_add_file "$PREFIX" "Reencrypted entire store using new GPG id $gpg_id."
    		fi
    
    	version|--version)
    		version
    		exit 0
    		;;
    
    if [[ -n $PASSWORD_STORE_KEY ]]; then
    	ID="$PASSWORD_STORE_KEY"
    
    elif [[ ! -f $ID ]]; then
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    	echo "You must run:"
    
    	echo "    $program init your-gpg-id"
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    	echo "before you may use the password store."
    	echo
    	usage
    	exit 1
    else
    	ID="$(head -n 1 "$ID")"
    fi
    
    
    case "$command" in
    
    	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"
    		if [[ -d $PREFIX/$path ]]; then
    			if [[ $path == "" ]]; then
    				echo "Password Store"
    			else
    				echo $path
    			fi
    
    			tree --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\(.*\)\.gpg$/\1/'
    
    		else
    			passfile="$PREFIX/$path.gpg"
    
    			if [[ ! -f $passfile ]]; then
    
    				echo "$path is not in the password store."
    				exit 1
    			fi
    
    			if [[ $clip -eq 0 ]]; then
    
    				clip "$($GPG -d $GPG_OPTS "$passfile" | head -n 1)" "$path"
    
    		multiline=0
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    		noecho=0
    
    		opts="$($GETOPT -o mnf -l multiline,no-echo,force -n "$program" -- "$@")"
    
    		err=$?
    		eval set -- "$opts"
    		while true; do case $1 in
    
    			-m|--multiline) multiline=1; shift ;;
    
    			-n|--no-echo) noecho=1; shift ;;
    
    			--) shift; break ;;
    		esac done
    
    
    		if [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 1 ) || $# -ne 1 ]]; then
    
    			echo "Usage: $program $command [--no-echo,-n | --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")"
    
    		if [[ $multiline -eq 1 ]]; then
    
    			echo "Enter contents of $path and press Ctrl+D when finished:"
    			echo
    
    			$GPG -e -r "$ID" -o "$passfile" $GPG_OPTS
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    		elif [[ $noecho -eq 1 ]]; then
    
    			while true; do
    
    				read -p "Enter password for $path: " -s password
    
    				read -p "Retype password for $path: " -s password_again
    
    				echo
    				if [[ $password == $password_again ]]; then
    
    					$GPG -e -r "$ID" -o "$passfile" $GPG_OPTS <<<"$password"
    
    					break
    				else
    					echo "Error: the entered passwords do not match."
    				fi
    			done
    		else
    
    			read -p "Enter password for $path: " -e password
    
    			$GPG -e -r "$ID" -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")"
    		passfile="$PREFIX/$path.gpg"
    
    Jason A. Donenfeld's avatar
    Jason A. Donenfeld committed
    		template="$program.XXXXXXXXXXXXX"
    
    		trap '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 -r "$ID" -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")"
    		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 -r "$ID" -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
    
    		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