#!/bin/bash
# Copyright 1999-2003 Gentoo Technologies, Inc.
# Distributed under the terms of the GNU General Public License v2
# Author: Daniel Robbins <drobbins@gentoo.org>
# Current Maintainer: Seth Chandler <sethbc@gentoo.org>
# $Header: /home/cvsroot/gentoo-src/keychain/keychain,v 1.27 2003/04/17 18:40:59 sethbc Exp $

version=2.0.3

PATH="/sbin:/usr/sbin:${PATH}:/usr/ucb"; export PATH;

myaction=""
myactionarg=""
noaskopt=""
quietopt=""
nocolopt=""
clearopt=""
mykeys=""
keydir="${HOME}/.keychain"
while [ -n "$1" ]
do
	case $1 in
		-h|--help|--stop|-k)
			#action specified
			if [ -z "$myaction" ]
			then
				case $1 in
					-h|--help)
						myaction="help"
						;;
					--stop|-k)
						myaction="stop"
						;;
				esac
				myactionarg="$1"
			else
				echo "$0: you can't specify $myactionarg and $1 at the same time; exiting."
				exit 1
			fi
			;;
		--dir)
			shift
			keydir="${1}/.keychain"
			;;
		--clear)
			clearopt="yes"
			;;
		--noask)
			noaskopt="yes"
			;;
		-q|--quiet)
			quietopt="yes"
			;;
		--nocolor)
			nocolopt="yes"
			;;
		*)
			mykeys="${mykeys} $1"
			;;
	esac
	shift
done

#auto-detect whether echo -e works.
unset E
if [ -z "`echo -e`" ]
then
        E="-e"
fi

BLUE="\033[34;01m"
GREEN="\033[32;01m"
OFF="\033[0m"
CYAN="\033[36;01m"

	# color variables won't be defined if --nocolor is present
if [ "$nocolopt" = "yes" ]
then
  unset BLUE GREEN OFF CYAN
fi


#first, handle help.
if [ "$myaction" = "help" ]
then
	echo $E Usage: ${CYAN}${0}${OFF} [ ${GREEN}options${OFF} ] ${CYAN}sshkey${OFF} ...
	cat <<EOHELP

Description:

 Keychain is an OpenSSH key manager, typically run from ~/.bash_profile.  When
 run, it will make sure ssh-agent is running; if not, it will start ssh-agent.
 It will redirect ssh-agent's output to ~/.keychain/[hostname]-sh, so that cron
 jobs that need to use ssh-agent keys can simply source this file and make the
 necessary passwordless ssh connections.  In addition, when keychain runs, it
 will check with ssh-agent and make sure that the ssh RSA/DSA keys that you
 specified on the keychain command line have actually been added to ssh-agent.
 If not, you are prompted for the appropriate passphrases so that they can be
 added by keychain.

 Typically, one uses keychain by adding the following to the top of their
 ~/.bash_profile (or ~/.zlogin, in case of zsh):

EOHELP
	echo $E "  ${CYAN}keychain ~/.ssh/id_rsa ~/.ssh/id_dsa"
	echo $E "  . ~/.keychain/\${HOSTNAME}-sh${OFF}"
	echo
	echo $E "  # alt. syntax: . ~/.keychain/\`uname -n\`-sh"
	echo $E "  # note the use of back-quotes (\`) rather than single-quotes (') above."
	echo $E "  # We now include the hostname (\`uname -n\`) in the keychain filename"
	echo $E "  # for NFS-compatibility."
	echo
	echo " You can make keychain work with your csh-compatible shell by adding the"
	echo " following to your .cshrc:"
	echo
	echo $E "  ${CYAN}keychain ~/.ssh/id_rsa ~/.ssh/id_dsa"
	echo $E "  source ~/.keychain/\${HOSTNAME}-csh${OFF}"
	echo
	cat <<EOHELP
 Keychain allows all your apps and cron jobs to use a single ssh-agent process
 as an authentication agent.  By default, the ssh-agent started by keychain is
 long-running and will continue to run, even after you have logged out from the
 system.  If you'd like to tighten up security a bit, take a look at the
EOHELP
	echo $E " ${GREEN}--clear${OFF} option, described below."
	echo
	echo Options:
	echo   
	echo $E " ${GREEN}--clear${OFF}"
	echo
	cat <<EOHELP
 Tells keychain to delete all of ssh-agent's host keys.  Typically, This is
 used in the ~/.bash_profile.  The theory behind this is that keychain should
 assume that you are an intruder until proven otherwise.  However, while this
 option increases security, it still allows your cron jobs to use your ssh keys
 when you're logged out.
EOHELP
	echo   
	echo $E " ${GREEN}--dir [directoryname]${OFF}"
	echo
	cat <<EOHELP
 Keychain will look in [directoryname] for the .keychain file, rather than your
 home directory.
EOHELP
		
	echo
	echo $E " ${GREEN}--noask${OFF}"
	echo
	cat <<EOHELP
 This option tells keychain do everything it normally does (ensure ssh-agent is
 running, set up the ~/.keychain/[hostname]-{c}sh files) except that it will not
 prompt you to add any of the keys you specified if they haven't yet been added
 to ssh-agent.
EOHELP
	echo
	echo $E " ${GREEN}--nocolor${OFF}"
	echo
	echo " This option disables color highlighting for non vt-100-compatible terms."
	echo
	echo $E " ${GREEN}--stop | -k${OFF}"
	echo
	cat <<EOHELP
 This option tells keychain to stop all running ssh-agent processes, and then
 exit.
EOHELP
	echo
	echo $E " ${GREEN}--quiet | -q${OFF}"
	echo
	cat <<EOHELP
 This option tells keychain to turn off verbose mode and only print error
 messages and interactive messages. This is useful for login scripts etc.
EOHELP
	echo
#' this line is a simple fix for vim syntax highlighting
	rm -f "$lockf" 2> /dev/null
	exit 1
fi

# Query local host for operating system.
cygwin="no"
hpux="no"
myuname=`uname -s`
if [ -n "`echo $myuname | grep CYGWIN`" ]
then
	cygwin="yes"
elif [ -n "`echo $myuname | grep HP-UX`" ]
then
	hpux="yes"
fi

# Query local host for SSH application, presently supporting only
# OpenSSH (see http://www.openssh.org) when openssh="yes" and
# SSH2 (see http://www.ssh.com) when openssh="no".
openssh="no"
if [ -n "`ssh -V 2>&1 | grep OpenSSH`" ]
then
	openssh="yes"
fi

# Avoid trapping on signal names with Cygwin.
trapint="INT"
if [ "$cygwin" = "yes" ]
then
	trapint=2
fi

# Disallow ^C abortion, since keys may be in interim, unsafe states.
trap "" $trapint

# pidf holds the specific name of the keychain .ssh-agent-myhostname file.
# We use the new hostname extension for NFS compatibility. cshpidf is the
# .ssh-agent file with csh-compatible syntax. lockf is the lockfile, used
# to serialize the execution of multiple ssh-agent processes started 
# simultaneously (only works if lockfile from the procmail package is
# available.

hostname=`uname -n`
pidf="${keydir}/${hostname}-sh"
cshpidf="${keydir}/${hostname}-csh"
lockf="${keydir}/${hostname}-lock"

if [ -f ${keydir} ]
then
	echo "$0: ${keydir} is a file (it should be a directory;) please fix."
	exit 1
#Solaris 9 doesn't have -e; using -d....
elif [ ! -d ${keydir} ]
then
	mkdir ${keydir} || exit 1
	chmod 0700 ${keydir}
fi

# perform lock if we have lockfile available                                    
if type lockfile >/dev/null 2>&1; then                                          
	lockfile -1 -r 30 -l 35 -s 2 "$lockf"                                       
	if [ $? != 0 ]; then                                                        
		echo "$0: Couldn't get lock" >&2                                     
		exit 1                                                              
	fi
fi

if [ -z "$quietopt" ]
then
	echo
	echo $E "${GREEN}KeyChain ${version}; ${BLUE}http://www.gentoo.org/projects/keychain${OFF}"
	echo $E " Copyright 2002 Gentoo Technologies, Inc.; Distributed under the GPL" 
fi

me=`whoami`
if [ "$cygwin" = "yes" ]
then
	#for cygwin
	psopts="-u $me -f"
	grepopts="-i"
elif [ "$hpux" = "yes" ]
then	
	#for hp-ux
	psopts="-u $me -f"
else
	psopts="FAIL"
	#-x option needed on MacOS X, but just in case it doesn't work on other arches we have a "-u $me -f"
	#at the end. "-uxw" works on modern Linux systems.
	for x in "-x -u $me -f" "-uxw" "-u $me -f"
	do
		ps $x >/dev/null 2>&1
		if [ $? -eq 0 ]
		then
			psopts="$x"
			break
		fi
	done
	if [ "$psopts" = "FAIL" ]
	then
		echo "$0: unable to use \"ps\" to scan for ssh-agent processes.  Report keychain version and"
		echo "system configuration to sethbc@gentoo.org."
		rm -f "${lockf}" 2> /dev/null
		exit 1
	fi
fi

mypids=`ps $psopts 2>/dev/null | grep $grepopts "[s]sh-agent"` > /dev/null 2>&1
#extract the second item from mypids:
if [ -n "$mypids" ]
then
	set $mypids
	mypids=$2
fi

if [ "$myaction" = "stop" ]
then
	# --stop tells keychain to kill the existing ssh-agent(s), then exit
	kill $mypids > /dev/null 2>&1
	rm -f "${pidf}" "${cshpidf}" "$lockf" 2> /dev/null
	#`whoami` (rather than the $LOGNAME var) gives us the euid rather than the uid (what we want)
	if [ -z "$quietopt" ]
	then
		echo $E " ${GREEN}*${OFF} All ssh-agent(s) started by" `whoami` "are now stopped."
		echo
	fi
	exit 0
fi

SSH_AGENT_PID="NULL"
if [ -f $pidf ]
then
	. $pidf	
fi

# Copy application-specific environment variables into generic local variables.
SSH_AUTH_SOCK_NAME="SSH_AUTH_SOCK"
SSH_AGENT_PID_NAME="SSH_AGENT_PID"
if [ "$openssh" = "no" ]
then
	SSH_AUTH_SOCK=${SSH2_AUTH_SOCK}
	SSH_AGENT_PID=${SSH2_AGENT_PID}

	SSH_AUTH_SOCK_NAME="SSH2_AUTH_SOCK"
	SSH_AGENT_PID_NAME="SSH2_AGENT_PID"
fi

match="no"
for x in $mypids
do
	if [ "$x" = "$SSH_AGENT_PID" ]
	then
		if [ -z "$quietopt" ]
		then
			echo $E " ${GREEN}*${OFF} Found existing ssh-agent at PID ${x}"
		fi
		match="yes"
		break
	fi
done

if [ "$match" = "no" ]
then
	if [ -n "$mypids" ]
	then
		kill $mypids > /dev/null 2>&1
	fi
	if [ -z "$quietopt" ]
	then
		echo $E " ${GREEN}*${OFF} All previously running ssh-agent(s) have been stopped."
		echo $E " ${GREEN}*${OFF} Initializing ${pidf} file..."
	fi

	# "> pidf" doesn't work ash.  But it should work with any sh-compatible shell
	> "$pidf" || { echo "$0: Cannot create ${pidf}; exiting." 1>&2; rm -f "$pidf" "$cshpidf" "$lockf" 2> /dev/null; exit 1; }
	[ -z "$quietopt" ] && echo $E " ${GREEN}*${OFF} Initializing ${cshpidf} file..."
	> "$cshpidf" || { echo "$0: Cannot create ${cshpidf}; exiting." 1>&2; rm -f "$pidf" "$cshpidf" "$lockf" 2> /dev/null; exit 1; }
	chmod 0600 "$pidf" "$cshpidf"
	[ -z "$quietopt" ] && echo $E " ${GREEN}*${OFF} Starting new ssh-agent"
	nohup ssh-agent -s | grep -v 'Agent pid' > "$pidf"
	. "$pidf"
	echo "setenv $SSH_AUTH_SOCK_NAME $SSH_AUTH_SOCK;" > "$cshpidf"
	echo "setenv $SSH_AGENT_PID_NAME $SSH_AGENT_PID;" >> "$cshpidf"
fi

if [ -n "$clearopt" ]
then
	echo $E " ${GREEN}*${OFF} \c"
	ssh-add -D
fi

#now that keys are potentially cleared, it's safe to be aborted by ^C
trap - $trapint

if [ -n "$noaskopt" ]
then
	# --noask means "don't ask for keys", so skip this next part	
	echo
	rm -f "$lockf" 2> /dev/null
	exit 0
fi

# hook in to existing agent
. "$pidf"

missingkeys="START"
#below, previous count of missing keys, and count of missing keys, respectively.
#when the difference between these two numbers does not abort after three tries,
#we abort the loop (using $countdown)
pmcount=0
mcount=0
countdown=3
while [ $countdown -gt 1 ] && [ "$missingkeys" != "" ]
do
	pmcount=$mcount
	mcount=0
	myfail=0
	missingkeys=""

	# Generate and parse verbose listing of already added keys.
	if [ "$openssh" = "no" ]
	then
		myavail=`ssh-add -l 2>&1 | tail +2 | awk '{ sub(/:.*/, ""); print }'`
	else
		myavail=`ssh-add -l 2>&1 | cut -f2 -d " "`
	fi

	if [ $? -ne 0 ]
	then
		echo $E " ${CYAN}*${OFF} Problems listing keys; exiting..."
		exit 1
	fi
	for x in $mykeys
	do
		if [ ! -f "$x" ]
		then
			echo $E " ${CYAN}*${OFF} Can't find ${x}; skipping..."
			continue
		fi

		# Extract relevant metadata from current user-specified key.
		if [ "$openssh" = "no" ]
		then
			myfing=`basename ${x} 2>&1 | cut -f2 -d " "`
		else
			if [ -f "${x}.pub" ]
			then
				myfing=`ssh-keygen -l -f ${x}.pub 2>&1`
			else
				myfing=`ssh-keygen -l -f ${x} 2>&1`
				if [ $? -ne 0 ]
				then
					echo $E " ${CYAN}*${OFF} Warning: ${x}.pub missing; can't tell if key ${x} already loaded."
					myfail=3
				fi
			fi
			myfing=`echo ${myfing} | cut -f2 -d " "`
		fi

		skip=0
		for y in $myavail
		do
			if [ "$y" = "$myfing" ]
			then
				skip=1
				break
			fi
		done
		if [ $skip -ne 1 ]
		then
			missingkeys="$missingkeys $x"
			mcount=`expr $mcount + 1`
		fi
	done
	if [ "$missingkeys" = "" ]
	then
		break
	fi
	if [ `expr $pmcount - $mcount` -eq 0 ]
	then
		countdown=`expr $countdown - 1`
	else
		countdown=3
	fi
	if [ -z "$quietopt" ] 
	then
		echo $E " ${GREEN}*${OFF} ${BLUE}${mcount}${OFF} more keys to add..."
	fi	
	if [ -n "$SSH_ASKPASS" ]
	then
		ssh-add ${missingkeys} < /dev/null
	else
		ssh-add ${missingkeys}
	fi
	if [ $? -ne 0 ]
	then
		myfail=`expr $myfail + 1`
		echo $E " ${CYAN}*${OFF} Problem adding key${OFF}..."
	fi
done
if [ -z "$quietopt" ]
then
	echo
fi
#remove lockfile if it exists
rm -f "$lockf" 2> /dev/null

