#!/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 "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

. $HOME/.dpw || error 'Please create configuration file at ~/.dpw'

error() {
    echo $@ >&2
    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
gpg_encrypt() {
    ( printf '%s\n' "$2"; cat ) \
            | gpg_quiet -c -o $1 \
                      --no-symkey-cache \
                      --passphrase-fd 0 \
                      --cipher-algo AES256
}

# Disable echo while typing the password if in a terminal
hidden_prompt() {
    if is_tty; 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=$(tempfile)
    ( <&3 xclip -verbose -selection clipboard 2>&1 & echo $! > $pidfile ) \
	| output-trigger $(cat $pidfile) #> /dev/null 2>&1
    rm $pidfile

    echo pasted >&2
}

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() {
    (
	cd $db || exit 1
	for f in *.gpg critical/*.gpg; do
	    if [ x"$(dirname $f)" != x"." ]; then
		echo -n $(dirname $f)/
	    fi
            echo $(basename $f .gpg)
	done
    )
}

# 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() {
    echo $@
    dmenu -fn "$FONT" -nf red -nb red -p "$@" &
    sleep 0.2
    kill $!
    exit 1
}

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

    site_list=$(dpw_list)
    if exit_fail; then
	dpw_menu_error "Listing sites failed"
    fi

    site=$(printf '%s' "$site_list" \
	| dmenu -fn "$FONT" -i -p "Site:" -l 4)
    if exit_fail; then
	dpw_menu_error "Selecting site failed"
    fi

    dbpw=$(dmenu -fn "$FONT" -i -nf '#222222' -p "Password:" </dev/null)
    if exit_fail; then
	dpw_menu_error "Entering DB password failed"
    fi

    content=$(printf '%s' "$dbpw" | gpg_decrypt $db/$site.gpg)
    if exit_fail; then
	dpw_menu_error "Decrypting DB failed"
    fi

    if [ -z "$content" ]; then
	dpw_menu_error "Empty file?!"
    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 $!
}

dpw_create() {
    site=$1
    file=$db/$site.gpg
    tmp=$(tempfile)

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

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

    hidden_prompt "DB Password" pw
    dpw_test_pw $pw || error "Wrong DB Password"

    is_tty && echo "Content:"
    gpg_encrypt $tmp $pw # reads stdin
    exit_fail && error "Encrypting file failed"

    chmod 600 $tmp
    mv $tmp $file

    if [ $? -ne 0 ]; then
	echo "gpg_encrypt failed for $site"
	exit 1
    fi
}

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

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