#!/usr/bin/env bash

# monkeysphere: MonkeySphere client tool
#
# The monkeysphere scripts are written by:
# Jameson Rollins <jrollins@fifthhorseman.net>
# Jamie McClelland <jm@mayfirst.org>
# Daniel Kahn Gillmor <dkg@fifthhorseman.net>
#
# They are Copyright 2008, and are all released under the GPL, version 3
# or later.

########################################################################
PGRM=$(basename $0)

SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
export SYSSHAREDIR
. "${SYSSHAREDIR}/common" || exit 1

# UTC date in ISO 8601 format if needed
DATE=$(date -u '+%FT%T')

# unset some environment variables that could screw things up
unset GREP_OPTIONS

# default return code
RETURN=0

# set the file creation mask to be only owner rw
umask 077

########################################################################
# FUNCTIONS
########################################################################

usage() {
    cat <<EOF >&2
usage: $PGRM <subcommand> [options] [args]
Monkeysphere client tool.

subcommands:
 update-known_hosts (k) [HOST]...    update known_hosts file
 update-authorized_keys (a)          update authorized_keys file
 gen-subkey (g) [KEYID]              generate an authentication subkey
   --length (-l) BITS                  key length in bits (2048)
   --expire (-e) EXPIRE                date to expire
 subkey-to-ssh-agent (s)             store authentication subkey in ssh-agent
 help (h,?)                          this help

EOF
}

# generate a subkey with the 'a' usage flags set
gen_subkey(){
    local keyLength
    local keyExpire
    local keyID
    local gpgOut
    local userID

    # set default key parameter values
    keyLength=
    keyExpire=

    # get options
    TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o l:e: -l length:,expire: -n "$PGRM" -- "$@") || failure "getopt failed!  Does your getopt support GNU-style long options?"

    if [ $? != 0 ] ; then
	exit 1
    fi

    # Note the quotes around `$TEMP': they are essential!
    eval set -- "$TEMP"

    while true ; do
	case "$1" in
	    -l|--length)
		keyLength="$2"
		shift 2
		;;
	    -e|--expire)
		keyExpire="$2"
		shift 2
		;;
	    --)
		shift
		;;
            *)
		break
		;;
	esac
    done

    if [ -z "$1" ] ; then
	# find all secret keys
	keyID=$(gpg --with-colons --list-secret-keys | grep ^sec | cut -f5 -d: | sort -u)
	# if multiple sec keys exist, fail
	if (( $(echo "$keyID" | wc -l) > 1 )) ; then
	    echo "Multiple secret keys found:"
	    echo "$keyID"
	    failure "Please specify which primary key to use."
	fi
    else
	keyID="$1"
    fi
    if [ -z "$keyID" ] ; then
	failure "You have no secret key available.  You should create an OpenPGP
key before joining the monkeysphere. You can do this with:
   gpg --gen-key"
    fi

    # get key output, and fail if not found
    gpgOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons \
	"$keyID") || failure

    # fail if multiple sec lines are returned, which means the id
    # given is not unique
    if [ $(echo "$gpgOut" | grep -c '^sec:') -gt '1' ] ; then
	failure "Key ID '$keyID' is not unique."
    fi

    # prompt if an authentication subkey already exists
    if echo "$gpgOut" | egrep "^(sec|ssb):" | cut -d: -f 12 | grep -q a ; then
	echo "An authentication subkey already exists for key '$keyID'."
	read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
	if [ "${OK/y/Y}" != 'Y' ] ; then
	    failure "aborting."
	fi
    fi

    # set subkey defaults
    # prompt about key expiration if not specified
    keyExpire=$(get_gpg_expiration "$keyExpire")

    # generate the list of commands that will be passed to edit-key
    editCommands=$(cat <<EOF
addkey
7
S
E
A
Q
$keyLength
$keyExpire
save
EOF
)

    log verbose "generating subkey..."
    fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
    (umask 077 && mkfifo "$fifoDir/pass")
    echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &

    passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"

    rm -rf "$fifoDir"
    wait
    log verbose "done."
}

function subkey_to_ssh_agent() {
    # try to add all authentication subkeys to the agent:

    local sshaddresponse
    local secretkeys
    local authsubkeys
    local workingdir
    local keysuccess
    local subkey
    local publine
    local kname

    if ! test_gnu_dummy_s2k_extension ; then
	failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
You may want to consider patching or upgrading to GnuTLS 2.6 or later.

For more details, see:
 http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
    fi

    # if there's no agent running, don't bother:
    if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
	failure "No ssh-agent available."
    fi

    # and if it looks like it's running, but we can't actually talk to
    # it, bail out:
    ssh-add -l >/dev/null
    sshaddresponse="$?"
    if [ "$sshaddresponse" = "2" ]; then
	failure "Could not connect to ssh-agent"
    fi
    
    # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
    secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
	grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')

    if [ -z "$secretkeys" ]; then
	failure "You have no secret keys in your keyring!
You might want to run 'gpg --gen-key'."
    fi
    
    authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
	--fingerprint --fingerprint $secretkeys | \
	cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
	grep '^fpr::' | cut -f3 -d: | sort -u)

    if [ -z "$authsubkeys" ]; then
	failure "no authentication-capable subkeys available.
You might want to 'monkeysphere gen-subkey'"
    fi

    workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
    umask 077
    mkfifo "$workingdir/passphrase"
    keysuccess=1

    # FIXME: we're currently allowing any other options to get passed
    # through to ssh-add.  should we limit it to known ones?  For
    # example: -d or -c and/or -t <lifetime> 

    for subkey in $authsubkeys; do 
	# choose a label by which this key will be known in the agent:
	# we are labelling the key by User ID instead of by
	# fingerprint, but filtering out all / characters to make sure
	# the filename is legit.

	primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)

	#kname="[monkeysphere] $primaryuid"
	kname="$primaryuid"

	if [ "$1" = '-d' ]; then
	    # we're removing the subkey:
	    gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
	    (cd "$workingdir" && ssh-add -d "$kname")
	else
	    # we're adding the subkey:
	    mkfifo "$workingdir/$kname"
	    gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
		--export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
		--export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
	    (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&

	    passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
	    wait %2
	fi
	keysuccess="$?"

	rm -f "$workingdir/$kname"
    done

    rm -rf "$workingdir"

    # FIXME: sort out the return values: we're just returning the
    # success or failure of the final authentication subkey in this
    # case.  What if earlier ones failed?
    exit "$keysuccess"
}

########################################################################
# MAIN
########################################################################

# unset variables that should be defined only in config file
unset KEYSERVER
unset CHECK_KEYSERVER
unset KNOWN_HOSTS
unset HASH_KNOWN_HOSTS
unset AUTHORIZED_KEYS

# load global config
[ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"

# set monkeysphere home directory
MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
mkdir -p -m 0700 "$MONKEYSPHERE_HOME"

# load local config
[ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"

# set empty config variables with ones from the environment, or from
# config file, or with defaults
LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
# if keyserver not specified in env or monkeysphere.conf,
# look in gpg.conf
if [ -z "$KEYSERVER" ] ; then
    if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
	KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
    fi
fi
# if it's still not specified, use the default
KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}

# other variables not in config file
AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}

# export GNUPGHOME and make sure gpg home exists with proper
# permissions
export GNUPGHOME
mkdir -p -m 0700 "$GNUPGHOME"
export LOG_LEVEL

# get subcommand
COMMAND="$1"
[ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
shift

case $COMMAND in
    'update-known_hosts'|'update-known-hosts'|'k')
	MODE='known_hosts'

	# check permissions on the known_hosts file path
	if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
	    failure "Improper permissions on known_hosts file path."
	fi

        # if hosts are specified on the command line, process just
        # those hosts
	if [ "$1" ] ; then
	    update_known_hosts "$@"
	    RETURN="$?"

        # otherwise, if no hosts are specified, process every host
        # in the user's known_hosts file
	else
	    # exit if the known_hosts file does not exist
	    if [ ! -e "$KNOWN_HOSTS" ] ; then
		log error "known_hosts file '$KNOWN_HOSTS' does not exist."
		exit
	    fi

	    process_known_hosts
	    RETURN="$?"
	fi
	;;

    'update-authorized_keys'|'update-authorized-keys'|'a')
	MODE='authorized_keys'

	# check permissions on the authorized_user_ids file path
	if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
	    failure "Improper permissions on authorized_user_ids file path."
	fi

	# check permissions on the authorized_keys file path
	if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
	    failure "Improper permissions on authorized_keys file path."
	fi

        # exit if the authorized_user_ids file is empty
	if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
	    log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
	    exit
	fi

	# process authorized_user_ids file
	process_authorized_user_ids "$AUTHORIZED_USER_IDS"
	RETURN="$?"
	;;

    'gen-subkey'|'g')
	gen_subkey "$@"
	;;

    'subkey-to-ssh-agent'|'s')
	subkey_to_ssh_agent "$@"
	;;

    '--help'|'help'|'-h'|'h'|'?')
        usage
        ;;

    *)
        failure "Unknown command: '$COMMAND'
Type '$PGRM help' for usage."
        ;;
esac

exit "$RETURN"