#!/bin/bash
#  Das Password Manager
#  Copyright (C) 2013  Daniel Gröber <dpw@dxld.at>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

set -o pipefail
shopt -s globstar

FONT='Monospace-10'

usage() {
    echo "usage: dpw COMMAND [--version | -v] [--verbose]"
    echo "COMMAND is one of: menu, create, passwd"
}

version() {
    echo 0.1
    exit 0
}

# Parse common command line options
while getopts "hvl-:" flag; do
    case "$flag" in
        -)
            case $1 in
                --version) version;   shift;;
                --help)    HELP=1;    shift;;
                --verbose) VERBOSE=1; shift;;
                *) echo "Unknown long-option: $1"; exit 1;;
            esac
            ;;

        v) version;;
        h) HELP=1;;
        l) VERBOSE=1;;
        \?) exit 1;;
    esac
done
shift $(( OPTIND - 1))


if [ -z $1 ]; then
    echo >&2 "Error: No command given"
    usage
    exit 1
fi

cmd=$1; shift

if [ -e "$(dirname "$0")"/pmenu ]; then
        LIBEXECDIR="$(dirname "$0")"
fi

LIBEXECDIR="@LIBEXECDIR@"
tmp=$(echo "$LIBEXECDIR" | head -c1)
if [ "$tmp" = "@" ]; then
    export PATH=$(dirname $0):$PATH
else
    export PATH=$LIBEXECDIR/dpw:$PATH
fi

# Configuration file should set db=
. $HOME/.dpw || error 'Please create configuration file at ~/.dpw'

error() {
    [ "$GRAPHICAL" ] && dpw_menu_error "Error: $*"
    echo >&2 "Error: $*"
    exit 1
}

exit_ok() {
    [ $? -eq 0 ]
}

exit_fail() {
    [ $? -ne 0 ]
}

is_tty() {
    [ -t 0 ]
}

gpg_quiet() {
    if [ -n "$VERBOSE" ]; then
        gpg --batch --yes $@
    else
        gpg --batch --yes -q --no-tty 2>/dev/null $@
    fi
}

# Read passphrase from stdin (because dmenu puts it there) and decrypt $1 to
# stdout suppressing all gpg clutter
gpg_decrypt() {
    gpg_quiet --no-symkey-cache --passphrase-fd 0 -d $@
}

# Read file from stdin encrypt using passphrase in $2, output to $1
# Usage gpg_encrypt OUTPUT_FILE PASSPHRASE
gpg_encrypt() {
    gpg_quiet -c -o "$1" \
        --no-symkey-cache \
        --passphrase-file <(echo "$2") \
        --cipher-algo AES256 \
	--digest-algo SHA256 \
        --compress-algo=none \
	--s2k-mode 3 \
	--s2k-digest-algo sha512 \
	--s2k-count 65011712 \
	--force-mdc
}


# Disable echo while typing the password if in a terminal
hidden_prompt() {
    if [ ! "$GRAPHICAL" ]; then
	settings=$(stty -g)
	trap "stty '$settings'" 0
	stty -echo
	echo -n "$1: "
	IFS="" read -r REPLY
	echo
	stty "$settings"
    else
	IFS="" read -r REPLY
    fi

    eval "$2=$REPLY"

#    read -s -r "?$1: " REPLY; is_tty && echo

}

clipboard_copy() {
    exec 3<&0
    # All this is needed to make this command reliably exit after the user
    # pastes the password somewhere
    # see comments in output-trigger.c for details
    pidfile=$(mktemp --tmpdir -t --suffix=.pid dpw-xclip.XXXXXX)
    ( <&3 xclip -verbose -selection clipboard 2>&1 & echo $! > $pidfile ) \
	| output-trigger $(cat $pidfile) #> /dev/null 2>&1
    rm $pidfile

    echo pasted >&2
}

dpw_paste() {
	pmenu -p "Pasting: $1" &
	printf '%s' "$2" | clipboard_copy
	kill $!
}

# Use: https://github.com/erichgoldman/add-url-to-window-title
get_browser_url() {
	xprop -id $(xdotool getactivewindow) _NET_WM_NAME \
		| sed -rn 's/^.* = ".* :: ([^ —/]+)\/? — [a-zA-Z ]+"$/\1/p'

}

get_possible_domains() {
	url=$1
	while [ "$url" ]; do
		printf '%s\n' "$url"
		shorter_url=${url#*.}
		[ "$shorter_url" != "$url" ] || break
		[ "${shorter_url#*.}" != "$shorter_url" ] || break
		url=$shorter_url
	done
}

usage_menu() {
    echo "usage: dpw menu [--verbose]"
    echo "Display an X11 window to select the file to decrypt and read the"
    echo "master-password"
}

usage_create() {
    echo "usage: dpw create [--verbose] SITE"
    echo "Create a new password file for SITE"
}

usage_passwd() {
    echo "usage: dpw passwd [--verbose]"
    echo "Change the master-password for the password database"
}

dpw_list_ex() {
	echo Create new password
	echo Change password
	dpw_list

}

dpw_list() ( #< subshell
	cd "$db" || exit 1
        find . -mindepth 1 -name '*.gpg' \
		| sed -e 's@\./@@' -e 's/\.gpg$//' \
		| sort
)

# echo $PW | dpw_get $ID > decrypted
dpw_get() {
    gpg_decrypt "$db"/"$1".gpg
}

dpw_test_pw() {
    echo $1 | dpw_get "$(dpw_list | head -n1)" > /dev/null
    exit_ok || return 1
}

dpw_menu_error() {
    dmenu -fn "$FONT" -nf red -nb red -p "$@" &
    sleep 1
    kill $!
    exit 1
}

dpw_menu_create_pw() {
	dbpw_prompt
	url=$(get_browser_url)
	site=$(get_possible_domains "$url" | dmenu -fn "$FONT" -l 3 -i -p "Create for Site:")
	if exit_fail || [ -z "$site" ]; then
		error "Getting site name failed"
	fi

	user=$(dmenu -fn "$FONT" -i -p "[Optional] User:" </dev/null)

	pw=$(apg -n1 -m31 -a1 -M NCL) || \
		error "Password creation failed (apg)"
	[ "$pw" ] || error "Password creation empty (apg)"
	set -- "$pw"
	case "$user" in
		'') ;;
		*@*) email="$user"; unset user
		     site="$email@$site"
		     set -- "$@" display-order=email email="$email"
		     ;;

		*)   set -- "$@" display-order=user user="$user"
		     ;;
	esac
	printf '%s\n' "$@" | dpw_create "$site" \
		|| exit $?

	[ "$(printf '%s\n' "$dbpw" | dpw_get "$site" | head -n1)" = "$pw" ] \
		|| error "DB Validation failed"

	[ "$email" ] && dpw_paste "E-Mail" "$email"
	[ "$user"  ] && dpw_paste "User" "$user"
	dpw_paste "Password (1 of 2)" "$pw"
	dpw_paste "Password (2 of 2)" "$pw"
}

dpw_menu() {
    killall -u $(whoami) pmenu dmenu > /dev/null 2>&1
#    trap 'kill "$(jobs -p)" > /dev/null' 0 2 15

    selection=$(dpw_list_ex | dmenu -fn "$FONT" -i -p "Site:" -l 4)
    if exit_fail; then
	error "Selecting site failed"
    fi

    case $selection in
	    'Create new password')  dpw_menu_create_pw; exit $? ;;
	    #'Change password')     dpw_change; exit $? ;;
	    #'Edit entry')          dpw_edit; exit $? ;;
	    *) site=$selection ;;
    esac

    dbpw_prompt
    content=$(printf '%s' "$dbpw" | dpw_get "$site")
    if exit_fail; then
	    error "Decrypting Entry failed"
    fi

    unset dbpw

    if [ -z "$content" ]; then
	    error "Empty Entry?!"
    fi

    pw=$(printf "%s" "$content" | head -n1)
    display_order=$(printf "%s" "$content" | grep "^display-order=" | head -n1 \
	| awk -v FS="=" '{ print $2 }')

    echo do $display_order

    for field in $display_order; do
	value=$(printf "%s" "$content" | grep "^$field=" | head -n1 \
	    | awk -v FS="=" '{ print $2 }')

	pmenu -p "Pasting: $field" &
	printf '%s' "$value" | clipboard_copy
	kill $!
    done

    pmenu -p "Pasting: Password" &
    printf '%s' "$pw" | clipboard_copy
    kill $!
}

dbpw_prompt() {
	if [ "$GRAPHICAL" ]; then
		dbpw=$(dmenu -fn "$FONT" -i -nf '#222222' -p "DB Password:" </dev/null)
		if exit_fail; then
			error "Entering DB password failed"
		fi
	else
		hidden_prompt "DB Password" dbpw
	fi

	dpw_test_pw "$dbpw" || error "Wrong DB Password"
}

dpw_create() {
    site=$1
    file=$db/$site.gpg
    tmp=$(mktemp --tmpdir -t --suffix=.gpg dpw-create.XXXXXX)

    if [ -z "$site" ] || [ $# -gt 1 ]; then
	usage_create
	exit 1
    fi

    if [ -f "$file" ]; then
	error "ERROR: File $file exists, remove it first!"
    fi

    [ "$dbpw" ] || { dbpw_prompt || exit 1; }

    [ "$GRAPHICAL" ] || echo "Content:"
    gpg_encrypt "$tmp" "$dbpw" # reads stdin
    exit_fail && error "Encrypting file failed"

    chmod 600 "$tmp" && mv "$tmp" "$file" || error "Moving file into DB failed"
}

dpw_passwd() {
    hidden_prompt "Old DB Password" opw
    hidden_prompt "New DB Password" npw
    hidden_prompt "Repeat DB New Password" rnpw

    if [ -z $opw -o -z $npw ]; then
	echo "Both passphrases must be non zero-length strings"
	exit 1
    fi

    if [ $npw != $rnpw ]; then
        echo "Passphrases didn't match"
        exit 1
    fi

    if ! dpw_test_pw $opw; then
        error "Wrong Old DB Password"
        exit 1
    fi

    for f in $db/**/*.gpg; do
	echo $f
	new_name=$(tempfile -d ${XDG_RUNTIME_DIR:-/tmp})

        mv -n $f $f.old
	exit_fail && error "mv failed"

	while true; do
            # --ignore-mdc-error: gpg started requiring MACs and my old pw files
            # are still encrypted without one
            echo $opw | gpg_decrypt --ignore-mdc-error $f.old | gpg_encrypt $new_name $npw
            exit_ok && break

            #[ x"$npw" = x"$opw" ] && break

            #retry with newpw, maybe we failed halfway through before
            echo "retry."
            echo $npw | gpg_decrypt --ignore-mdc-error $f.old > /dev/null
            if exit_ok; then
                    mv $f.old $f
	            exit_fail && error "mv failed"
            fi
            exit_ok && break

            error "Could not decrypt $f"
	done

	mv -n $new_name $f
	exit_fail && error "mv failed"

	shred -n 10 -z --remove $f.old&
    done

    wait
}

if [ -n "$HELP" ]; then
    usage_$cmd || error "Unknown command $cmd"
    exit 0
fi

is_tty && GRAPHICAL= || GRAPHICAL=1

case $cmd in
    menu)
	GRAPHICAL=1
	dpw_menu $@
        ;;
    create)
	dpw_create $@
	;;
    passwd)
	dpw_passwd $@
	;;
    list)
	dpw_list
	;;
    *)
        echo "ERROR: unknown command '$cmd'"
        usage
        exit 1
        ;;
esac
