#!/bin/sh # Copyright 1999-2005 Gentoo Foundation # Copyright 2007 Aron Griffis # Copyright 2009-2017 Funtoo Solutions, Inc. # lockfile() Copyright 2009 Parallels, Inc. # Distributed under the terms of the GNU General Public License v2 # Originally authored by Daniel Robbins # Maintained August 2002 - April 2003 by Seth Chandler # Maintained and rewritten April 2004 - July 2007 by Aron Griffis # Maintained July 2009 - Sept 2017 by Daniel Robbins # Maintained September 2017 - present by Ryan Harris version=2.8.5 PATH="${PATH:-/usr/bin:/bin:/sbin:/usr/sbin:/usr/ucb}" maintainer="x48rph@gmail.com" unset mesglog unset myaction unset agentsopt havelock=false unset hostopt ignoreopt=false noaskopt=false noguiopt=false nolockopt=false lockwait=5 openssh=unknown sunssh=unknown confhost=unknown sshconfig=false quickopt=false quietopt=false clearopt=false color=true inheritwhich=local-once unset stopwhich unset timeout unset ssh_timeout attempts=1 unset sshavail unset sshkeys unset gpgkeys unset mykeys keydir="${HOME}/.keychain" unset envf evalopt=false queryopt=false confirmopt=false absoluteopt=false systemdopt=false unset ssh_confirm unset GREP_OPTIONS gpg_prog_name="gpg" BLUE="" CYAN="" CYANN="" GREEN="" RED="" PURP="" OFF="" # GNU awk and sed have regex issues in a multibyte environment. If any locale # variables are set, then override by setting LC_ALL unset pinentry_locale if [ -n "$LANG$LC_ALL" ] || [ -n "$(locale 2>/dev/null | egrep -v '="?(|POSIX|C)"?$' 2>/dev/null)" ]; then # save LC_ALL so that pinentry-curses works right. This has always worked # correctly for me but peper and kloeri had problems with it. pinentry_lc_all="$LC_ALL" LC_ALL=C export LC_ALL fi # synopsis: qprint "message" qprint() { $quietopt || echo "$*" >&2 } # synopsis: mesg "message" # Prettily print something to stderr, honors quietopt mesg() { qprint " ${GREEN}*${OFF} $*" } # synopsis: warn "message" # Prettily print a warning to stderr warn() { echo " ${RED}* Warning${OFF}: $*" >&2 } # synopsis: error "message" # Prettily print an error error() { echo " ${RED}* Error${OFF}: $*" >&2 } # synopsis: die "message" # Prettily print an error, then abort die() { [ -n "$1" ] && error "$*" qprint $evalopt && { echo; echo "false;"; } exit 1 } # synopsis: versinfo # Display the version information versinfo() { qprint qprint " Copyright ${CYANN}2002-2006${OFF} Gentoo Foundation;" qprint " Copyright ${CYANN}2007${OFF} Aron Griffis;" qprint " Copyright ${CYANN}2009-2017${OFF} Funtoo Solutions, Inc;" qprint " lockfile() Copyright ${CYANN}2009${OFF} Parallels, Inc." qprint qprint " Keychain is free software: you can redistribute it and/or modify" qprint " it under the terms of the ${CYANN}GNU General Public License version 2${OFF} as" qprint " published by the Free Software Foundation." qprint } # synopsis: helpinfo # Display the help information. There's no really good way to use qprint for # this... helpinfo() { cat >&1 <&1)" in *OpenSSH*) openssh=true ;; *Sun?SSH*) sunssh=true ;; esac } # synopsis: getuser # Set the global string $me getuser() { # id -un gives euid, which might be different from USER or LOGNAME me=$(id -un) || die "Who are you? id -un doesn't know..." } # synopsis: getos # Set the global string $OSTYPE getos() { OSTYPE=$(uname) || die 'uname failed' } # synopsis: verifykeydir # Make sure the key dir is set up correctly. Exits on error. verifykeydir() { # Create keydir if it doesn't exist already if [ -f "${keydir}" ]; then die "${keydir} is a file (it should be a directory)" # Solaris 9 doesn't have -e; using -d.... elif [ ! -d "${keydir}" ]; then ( umask 0077 && mkdir "${keydir}"; ) || die "can't create ${keydir}" fi } lockfile() { # This function originates from Parallels Inc.'s OpenVZ vpsreboot script # Description: This function attempts to acquire the lock. If it succeeds, # it returns 0. If it fails, it returns 1. This function retuns immediately # and only tries to acquire the lock once. tmpfile="$lockf.$$" echo $$ >"$tmpfile" 2>/dev/null || exit if ln "$tmpfile" "$lockf" 2>/dev/null; then rm -f "$tmpfile" havelock=true && return 0 fi if kill -0 $(cat $lockf 2>/dev/null) 2>/dev/null; then rm -f "$tmpfile" return 1 fi if ln "$tmpfile" "$lockf" 2>/dev/null; then rm -f "$tmpfile" havelock=true && return 0 fi rm -f "$tmpfile" "$lockf" && return 1 } takelock() { # Description: This function calls lockfile() multiple times if necessary # to try to acquire the lock. It returns 0 on success and 1 on failure. # Change in behavior: if timeout expires, we will forcefully acquire lock. [ "$havelock" = "true" ] && return 0 [ "$nolockopt" = "true" ] && return 0 # First attempt: lockfile && return 0 counter=0 mesg "Waiting $lockwait seconds for lock..." while [ "$counter" -lt "$(( $lockwait * 2 ))" ] do lockfile && return 0 sleep 0.5; counter=$(( $counter + 1 )) done rm -f "$lockf" && lockfile && return 0 return 1 } # synopsis: droplock # Drops the lock if we're holding it. droplock() { $havelock && [ -n "$lockf" ] && rm -f "$lockf" } # synopsis: findpids [prog] # Returns a space-separated list of agent pids. # prog can be ssh or gpg, defaults to ssh. Note that if another prog is ever # added, need to pay attention to the length for Solaris compatibility. findpids() { fp_prog=${1-ssh} unset fp_psout # Different systems require different invocations of ps. Try to generalize # the best we can. The only requirement is that the agent command name # appears in the line, and the PID is the first item on the line. [ -n "$OSTYPE" ] || getos # Try systems where we know what to do first case "$OSTYPE" in AIX|*bsd*|*BSD*|CYGWIN|darwin*|Linux|linux-gnu|OSF1) fp_psout=$(ps x 2>/dev/null) ;; # BSD syntax HP-UX) fp_psout=$(ps -u $me 2>/dev/null) ;; # SysV syntax SunOS) case $(uname -r) in [56]*) fp_psout=$(ps -u $me 2>/dev/null) ;; # SysV syntax *) fp_psout=$(ps x 2>/dev/null) ;; # BSD syntax esac ;; GNU|gnu) fp_psout=$(ps -g 2>/dev/null) ;; # GNU Hurd syntax esac # If we didn't get a match above, try a list of possibilities... # The first one will probably fail on systems supporting only BSD syntax. if [ -z "$fp_psout" ]; then fp_psout=$(UNIX95=1 ps -u $me -o pid,comm 2>/dev/null | grep '^ *[0-9]') [ -z "$fp_psout" ] && fp_psout=$(ps x 2>/dev/null) [ -z "$fp_psout" ] && fp_psout=$(ps w 2>/dev/null) # Busybox syntax fi # Return the list of pids; ignore case for Cygwin. # Check only 8 characters since Solaris truncates at that length. # Ignore defunct ssh-agents (bug 28599) if [ -n "$fp_psout" ]; then echo "$fp_psout" | \ awk "BEGIN{IGNORECASE=1} /defunct/{next} /$fp_prog-[a]gen/{print \$1}" | xargs return 0 fi # If none worked, we're stuck error "Unable to use \"ps\" to scan for $fp_prog-agent processes" error "Please report to $maintainer via http://bugs.gentoo.org" return 1 } # synopsis: stopagent [prog] # --stop tells keychain to kill the existing agent(s) # prog can be ssh or gpg, defaults to ssh. stopagent() { stop_prog=${1-ssh} eval stop_except=\$\{${stop_prog}_agent_pid\} stop_mypids=$(findpids "$stop_prog") [ $? = 0 ] || die if [ -z "$stop_mypids" ]; then mesg "No $stop_prog-agent(s) found running" return 0 fi case "$stopwhich" in all) kill $stop_mypids >/dev/null 2>&1 mesg "All ${CYANN}$me${OFF}'s $stop_prog-agents stopped: ${CYANN}$stop_mypids${OFF}" ;; others) # Try to handle the case where we *will* inherit a pid kill -0 $stop_except >/dev/null 2>&1 if [ -z "$stop_except" -o $? != 0 -o \ "$inheritwhich" = local -o "$inheritwhich" = any ]; then if [ "$inheritwhich" != none ]; then eval stop_except=\$\{inherit_${stop_prog}_agent_pid\} kill -0 $stop_except >/dev/null 2>&1 if [ -z "$stop_except" -o $? != 0 ]; then # Handle ssh2 eval stop_except=\$\{inherit_${stop_prog}2_agent_pid\} fi fi fi # Filter out the running agent pid unset stop_mynewpids for stop_x in $stop_mypids; do [ $stop_x -eq $stop_except ] 2>/dev/null && continue stop_mynewpids="${stop_mynewpids+$stop_mynewpids }$stop_x" done if [ -n "$stop_mynewpids" ]; then kill $stop_mynewpids >/dev/null 2>&1 mesg "Other ${CYANN}$me${OFF}'s $stop_prog-agents stopped: ${CYANN}$stop_mynewpids${OFF}" else mesg "No other $stop_prog-agent(s) than keychain's $stop_except found running" fi ;; mine) if [ $stop_except -gt 0 ] 2>/dev/null; then kill $stop_except >/dev/null 2>&1 mesg "Keychain $stop_prog-agents stopped: ${CYANN}$stop_except${OFF}" else mesg "No keychain $stop_prog-agent found running" fi ;; esac # remove pid files if keychain-controlled if [ "$stopwhich" != others ]; then if [ "$stop_prog" != ssh ]; then rm -f "${pidf}-$stop_prog" "${cshpidf}-$stop_prog" "${fishpidf}-$stop_prog" 2>/dev/null else rm -f "${pidf}" "${cshpidf}" "${fishpidf}" 2>/dev/null fi eval unset ${stop_prog}_agent_pid fi } # synopsis: inheritagents # Save agent variables from the environment before they get wiped out inheritagents() { # Verify these global vars are null unset inherit_ssh_auth_sock inherit_ssh_agent_pid unset inherit_ssh2_auth_sock inherit_ssh2_agent_sock unset inherit_gpg_agent_info inherit_gpg_agent_pid # Save variables so we can inherit a running agent if [ "$inheritwhich" != none ]; then if wantagent ssh; then if [ -n "$SSH_AUTH_SOCK" ]; then inherit_ssh_auth_sock="$SSH_AUTH_SOCK" inherit_ssh_agent_pid="$SSH_AGENT_PID" fi if [ -n "$SSH2_AUTH_SOCK" ]; then inherit_ssh2_auth_sock="$SSH2_AUTH_SOCK" inherit_ssh2_agent_pid="$SSH2_AGENT_PID" fi fi if wantagent gpg; then if [ -n "$GPG_AGENT_INFO" ]; then inherit_gpg_agent_info="$GPG_AGENT_INFO" inherit_gpg_agent_pid=$(echo "$GPG_AGENT_INFO" | cut -f2 -d:) # GnuPG v.2.1+ removes $GPG_AGENT_INFO else gpg_socket_dir="${GNUPGHOME:=$HOME/.gnupg}" if [ ! -S "${GNUPGHOME:=$HOME/.gnupg}/S.gpg-agent" ]; then gpg_socket_dir="${XDG_RUNTIME_DIR}/gnupg" fi if [ -S "${gpg_socket_dir}/S.gpg-agent" ]; then inherit_gpg_agent_pid=$(findpids "${gpg_prog_name}") inherit_gpg_agent_info="${gpg_socket_dir}/S.gpg-agent:${inherit_gpg_agent_pid}:1" fi fi fi fi } # synopsis: validinherit # Test inherit_* variables for validity validinherit() { vi_agent="$1" vi_status=0 if [ "$vi_agent" = ssh ]; then if [ -n "$inherit_ssh_auth_sock" ]; then ls "$inherit_ssh_auth_sock" >/dev/null 2>&1 if [ $? != 0 ]; then warn "SSH_AUTH_SOCK in environment is invalid; ignoring it" unset inherit_ssh_auth_sock inherit_ssh_agent_pid vi_status=1 fi fi if [ -n "$inherit_ssh2_auth_sock" ]; then ls "$inherit_ssh2_auth_sock" >/dev/null 2>&1 if [ $? != 0 ]; then warn "SSH2_AUTH_SOCK in environment is invalid; ignoring it" unset inherit_ssh2_auth_sock inherit_ssh2_agent_pid vi_status=1 fi fi elif [ "$vi_agent" = gpg ]; then if [ -n "$inherit_gpg_agent_pid" ]; then kill -0 "$inherit_gpg_agent_pid" >/dev/null 2>&1 if [ $? != 0 ]; then unset inherit_gpg_agent_pid inherit_gpg_agent_info warn "GPG_AGENT_INFO in environment is invalid; ignoring it" vi_status=1 fi fi fi return $vi_status } # synopsis: catpidf_shell shell agents... # cat the pid files for the given agents. This is used by loadagents and also # for keychain output when --eval is given. catpidf_shell() { case "$1" in */fish|fish) cp_pidf="$fishpidf" ;; *csh) cp_pidf="$cshpidf" ;; *) cp_pidf="$pidf" ;; esac shift for cp_a in "$@"; do case "${cp_a}" in ssh) [ -f "$cp_pidf" ] && cat "$cp_pidf" ;; *) [ -f "${cp_pidf}-$cp_a" ] && cat "${cp_pidf}-$cp_a" ;; esac echo done return 0 } # synopsis: catpidf agents... # cat the pid files for the given agents, appropriate for the current value of # $SHELL. This is used for keychain output when --eval is given. catpidf() { catpidf_shell "$SHELL" "$@" } # synopsis: loadagents agents... # Load agent variables from $pidf and copy implementation-specific environment # variables into generic global strings loadagents() { for la_a in "$@"; do case "$la_a" in ssh) unset SSH_AUTH_SOCK SSH_AGENT_PID SSH2_AUTH_SOCK SSH2_AGENT_PID eval "$(catpidf_shell sh $la_a)" if [ -n "$SSH_AUTH_SOCK" ]; then ssh_auth_sock=$SSH_AUTH_SOCK ssh_agent_pid=$SSH_AGENT_PID elif [ -n "$SSH2_AUTH_SOCK" ]; then ssh_auth_sock=$SSH2_AUTH_SOCK ssh_agent_pid=$SSH2_AGENT_PID else unset ssh_auth_sock ssh_agent_pid fi ;; gpg) unset GPG_AGENT_INFO eval "$(catpidf_shell sh $la_a)" if [ -n "$GPG_AGENT_INFO" ]; then la_IFS="$IFS" # save current IFS IFS=':' # set IFS to colon to separate PATH set -- $GPG_AGENT_INFO IFS="$la_IFS" # restore IFS gpg_agent_pid=$2 fi ;; *) eval "$(catpidf_shell sh $la_a)" ;; esac done return 0 } # synopsis: startagent [prog] # Starts an agent if it isn't already running. # Requires $ssh_agent_pid startagent() { start_prog=${1-ssh} start_proto=${2-${start_prog}} unset start_pid start_inherit_pid=none start_mypids=$(findpids "$start_prog") [ $? = 0 ] || die # Unfortunately there isn't much way to genericize this without introducing # a lot more supporting code/structures. if [ "$start_prog" = ssh ]; then start_pidf="$pidf" start_cshpidf="$cshpidf" start_fishpidf="$fishpidf" start_pid="$ssh_agent_pid" if [ -n "$inherit_ssh_auth_sock" -o -n "$inherit_ssh2_auth_sock" ]; then if [ -n "$inherit_ssh_agent_pid" ]; then start_inherit_pid="$inherit_ssh_agent_pid" elif [ -n "$inherit_ssh2_agent_pid" ]; then start_inherit_pid="$inherit_ssh2_agent_pid" else start_inherit_pid="forwarded" fi fi else start_pidf="${pidf}-$start_prog" start_cshpidf="${cshpidf}-$start_prog" start_fishpidf="${fishpidf}-$start_prog" if [ "$start_prog" = gpg ]; then start_pid="$gpg_agent_pid" if [ -n "$inherit_gpg_agent_pid" ]; then start_inherit_pid="$inherit_gpg_agent_pid" fi else error "I don't know how to start $start_prog-agent (1)" return 1 fi fi [ "$start_pid" -gt 0 ] 2>/dev/null || start_pid=none # This hack makes the case statement easier if [ "$inheritwhich" = any -o "$inheritwhich" = any-once ]; then start_fwdflg=forwarded else unset start_fwdflg fi # Check for an existing agent start_tester="$inheritwhich: $start_mypids $start_fwdflg " case "$start_tester" in none:*" $start_pid "*|*-once:*" $start_pid "*) mesg "Found existing ${start_prog}-agent: ${CYANN}$start_pid${OFF}" return 0 ;; *:*" $start_inherit_pid "*) # This test was postponed until now to prevent generating warnings validinherit "$start_prog" if [ $? != 0 ]; then # inherit_* vars have been removed from the environment. Try # again now startagent "$start_prog" return $? fi mesg "Inheriting ${start_prog}-agent ($start_inherit_pid)" ;; *) # start_inherit_pid might be "forwarded" which we don't allow with, # for example, local-once (the default setting) start_inherit_pid=none ;; esac # Init the bourne-formatted pidfile ( umask 0177 && :> "$start_pidf"; ) if [ $? != 0 ]; then rm -f "$start_pidf" "$start_cshpidf" "$start_fishpidf" 2>/dev/null error "can't create $start_pidf" return 1 fi # Init the csh-formatted pidfile ( umask 0177 && :> "$start_cshpidf"; ) if [ $? != 0 ]; then rm -f "$start_pidf" "$start_cshpidf" "$start_fishpidf" 2>/dev/null error "can't create $start_cshpidf" return 1 fi # Init the fish-formatted pidfile ( umask 0177 && :> "$start_fishpidf"; ) if [ $? != 0 ]; then rm -f "$start_pidf" "$start_cshpidf" "$start_fishpidf" 2>/dev/null error "can't create $start_fishpidf" return 1 fi # Determine content for files unset start_out if [ "$start_inherit_pid" = none ]; then # Start the agent. # Branch again since the agents start differently mesg "Starting ${start_prog}-agent..." if [ "$start_prog" = ssh ]; then start_out=$(ssh-agent ${ssh_timeout}) elif [ "$start_prog" = gpg ]; then if [ -n "${timeout}" ]; then gpg_cache_ttl="$(expr $timeout \* 60)" start_gpg_timeout="--default-cache-ttl $gpg_cache_ttl --max-cache-ttl $gpg_cache_ttl" else unset start_gpg_timeout fi # the 1.9.x series of gpg spews debug on stderr start_out=$(gpg-agent --daemon --write-env-file $start_gpg_timeout 2>/dev/null) else error "I don't know how to start $start_prog-agent (2)" return 1 fi if [ $? != 0 -a $? != 2 ]; then rm -f "$start_pidf" "$start_cshpidf" "$start_fishpidf" 2>/dev/null error "Failed to start ${start_prog}-agent" return 1 fi elif [ "$start_prog" = ssh -a -n "$inherit_ssh_auth_sock" ]; then start_out="SSH_AUTH_SOCK=$inherit_ssh_auth_sock; export SSH_AUTH_SOCK;" if [ "$inherit_ssh_agent_pid" -gt 0 ] 2>/dev/null; then start_out="$start_out SSH_AGENT_PID=$inherit_ssh_agent_pid; export SSH_AGENT_PID;" fi elif [ "$start_prog" = ssh -a -n "$inherit_ssh2_auth_sock" ]; then start_out="SSH2_AUTH_SOCK=$inherit_ssh2_auth_sock; export SSH2_AUTH_SOCK; SSH2_AGENT_PID=$inherit_ssh2_agent_pid; export SSH2_AGENT_PID;" if [ "$inherit_ssh2_agent_pid" -gt 0 ] 2>/dev/null; then start_out="$start_out SSH2_AGENT_PID=$inherit_ssh2_agent_pid; export SSH2_AGENT_PID;" fi elif [ "$start_prog" = "${gpg_prog_name}" -a -n "$inherit_gpg_agent_info" ]; then start_out="GPG_AGENT_INFO=$inherit_gpg_agent_info; export GPG_AGENT_INFO;" else die "something bad happened" # should never be here fi # Add content to pidfiles. # Some versions of ssh-agent don't understand -s, which means to # generate Bourne shell syntax. It appears they also ignore SHELL, # according to http://bugs.gentoo.org/show_bug.cgi?id=52874 # So make no assumptions. start_out=$(echo "$start_out" | grep -v 'Agent pid') case "$start_out" in setenv*) echo "$start_out" >"$start_cshpidf" echo "$start_out" | awk '{print $2"="$3" export "$2";"}' >"$start_pidf" ;; *) echo "$start_out" >"$start_pidf" echo "$start_out" | sed 's/;.*/;/' | sed 's/=/ /' | sed 's/^/setenv /' >"$start_cshpidf" echo "$start_out" | sed 's/;.*/;/' | sed 's/^\(.*\)=\(.*\);/set -e \1; set -x -U \1 \2;/' >"$start_fishpidf" ;; esac # Hey the agent should be started now... load it up! loadagents "$start_prog" } # synopsis: extract_fingerprints # Extract the fingerprints from standard input, returns space-separated list. # Utility routine for ssh_l and ssh_f extract_fingerprints() { while read ef_line; do case "$ef_line" in *\ *\ [0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:*) # Sun SSH spits out different things depending on the type of # key. For example: # md5 1024 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 /home/barney/.ssh/id_dsa(DSA) # 2048 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 /home/barney/.ssh/id_rsa.pub echo "$ef_line" | cut -f3 -d' ' ;; *\ [0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:*) # The more consistent OpenSSH format, we hope # 1024 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 /home/barney/.ssh/id_dsa (DSA) echo "$ef_line" | cut -f2 -d' ' ;; *\ [A-Z0-9][A-Z0-9]*:[A-Za-z0-9+/][A-Za-z0-9+/]*) # The new OpenSSH 6.8+ format, # 1024 SHA256:mVPwvezndPv/ARoIadVY98vAC0g+P/5633yTC4d/wXE /home/barney/.ssh/id_dsa (DSA) echo "$ef_line" | cut -f2 -d' ' ;; *) # Fall back to filename. Note that commercial ssh is handled # explicitly in ssh_l and ssh_f, so hopefully this rule will # never fire. warn "Can't determine fingerprint from the following line, falling back to filename" mesg "$ef_line" basename "$ef_line" | sed 's/[ (].*//' ;; esac done | xargs } # synopsis: ssh_l # Return space-separated list of known fingerprints ssh_l() { sl_mylist=$(ssh-add -l 2>/dev/null) sl_retval=$? if $openssh; then # Error codes: # 0 success # 1 OpenSSH_3.8.1p1 on Linux: no identities (not an error) # OpenSSH_3.0.2p1 on HP-UX: can't connect to auth agent # 2 can't connect to auth agent case $sl_retval in 0) echo "$sl_mylist" | extract_fingerprints ;; 1) case "$sl_mylist" in *"open a connection"*) sl_retval=2 ;; esac ;; esac return $sl_retval elif $sunssh; then # Error codes (from http://docs.sun.com/db/doc/817-3936/6mjgdbvio?a=view) # 0 success (even when there are no keys) # 1 error case $sl_retval in 0) echo "$sl_mylist" | extract_fingerprints ;; 1) case "$sl_mylist" in *"open a connection"*) sl_retval=2 ;; esac ;; esac return $sl_retval else # Error codes: # 0 success - however might say "The authorization agent has no keys." # 1 can't connect to auth agent # 2 bad passphrase # 3 bad identity file # 4 the agent does not have the requested identity # 5 unspecified error if [ $sl_retval = 0 ]; then # Output of ssh-add -l: # The authorization agent has one key: # id_dsa_2048_a: 2048-bit dsa, agriffis@alpha.zk3.dec.com, Fri Jul 25 2003 10:53:49 -0400 # Since we don't have a fingerprint, just get the filenames *shrug* echo "$sl_mylist" | sed '2,$s/:.*//' | xargs fi return $sl_retval fi } # synopsis: ssh_f filename # Return fingerprint for a keyfile # Requires $openssh or $sunssh ssh_f() { sf_filename="$1" if $openssh || $sunssh; then realpath_bin="$(command -v realpath)" # if private key is symlink and symlink to *.pub is missing: if [ -L "$sf_filename" ] && [ ! -z "$realpath_bin" ]; then sf_filename="$($realpath_bin $sf_filename)" fi lsf_filename="$sf_filename.pub" if [ ! -f "$lsf_filename" ]; then # try to remove extension from private key, *then* add .pub, and see if we now find it: if [ -L "$sf_filename" ] && [ ! -z "$realpath_bin" ]; then sf_filename="$($realpath_bin $sf_filename)" fi lsf_filename=$(echo "$sf_filename" | sed 's/\.[^\.]*$//').pub if [ ! -f "$lsf_filename" ]; then warn "Cannot find separate public key for $1." lsf_filename="$sf_filename" fi fi sf_fing=$(ssh-keygen -l -f "$lsf_filename") || return 1 echo "$sf_fing" | extract_fingerprints else # can't get fingerprint for ssh2 so use filename *shrug* basename "$sf_filename" fi return 0 } # synopsis: gpg_listmissing # Uses $gpgkeys # Returns a newline-separated list of keys found to be missing. gpg_listmissing() { unset glm_missing GPG_TTY=$(tty) # Parse $gpgkeys into positional params to preserve spaces in filenames set -f # disable globbing glm_IFS="$IFS" # save current IFS IFS=" " # set IFS to newline set -- $gpgkeys IFS="$glm_IFS" # restore IFS set +f # re-enable globbing for glm_k in "$@"; do # Check if this key is known to the agent. Don't know another way... if echo | env -i GPG_TTY="$GPG_TTY" PATH="$PATH" GPG_AGENT_INFO="$GPG_AGENT_INFO" \ "${gpg_prog_name}" --no-options --use-agent --no-tty --sign --local-user "$glm_k" -o- >/dev/null 2>&1; then # already know about this key mesg "Known gpg key: ${CYANN}${glm_k}${OFF}" continue else # need to add this key if [ -z "$glm_missing" ]; then glm_missing="$glm_k" else glm_missing="$glm_missing $glm_k" fi fi done echo "$glm_missing" } # synopsis: ssh_listmissing # Uses $sshkeys and $sshavail # Returns a newline-separated list of keys found to be missing. ssh_listmissing() { unset slm_missing # Parse $sshkeys into positional params to preserve spaces in filenames set -f # disable globbing slm_IFS="$IFS" # save current IFS IFS=" " # set IFS to newline set -- $sshkeys IFS="$slm_IFS" # restore IFS set +f # re-enable globbing for slm_k in "$@"; do # Fingerprint current user-specified key slm_finger=$(ssh_f "$slm_k") || continue # Check if it needs to be added case " $sshavail " in *" $slm_finger "*) # already know about this key mesg "Known ssh key: ${CYANN}${slm_k}${OFF}" ;; *) # need to add this key if [ -z "$slm_missing" ]; then slm_missing="$slm_k" else slm_missing="$slm_missing $slm_k" fi ;; esac done echo "$slm_missing" } # synopsis: add_gpgkey # Adds a key to $gpgkeys add_gpgkey() { gpgkeys=${gpgkeys+"$gpgkeys "}"$1" } # synopsis: add_sshkey # Adds a key to $sshkeys add_sshkey() { sshkeys=${sshkeys+"$sshkeys "}"$1" } # synopsis: parse_mykeys # Sets $sshkeys and $gpgkeys based on $mykeys parse_mykeys() { # Possible path to the private key: if --confhost variable used. pkeypath="$1" # Parse $mykeys into positional params to preserve spaces in filenames set -f # disable globbing pm_IFS="$IFS" # save current IFS IFS=" " # set IFS to newline set -- $mykeys IFS="$pm_IFS" # restore IFS set +f # re-enable globbing for pm_k in "$@"; do # Check for ssh if wantagent ssh; then if [ -f "$pm_k" ]; then add_sshkey "$pm_k" ; continue elif [ -f "$HOME/.ssh/$pm_k" ]; then add_sshkey "$HOME/.ssh/$pm_k" ; continue elif [ -f "$HOME/.ssh2/$pm_k" ]; then add_sshkey "$HOME/.ssh2/$pm_k" ; continue elif [ -f "$pkeypath" ]; then add_sshkey "$pkeypath"; continue fi fi # Check for gpg if wantagent gpg; then "${gpg_prog_name}" --list-secret-keys "$pm_k" >/dev/null 2>&1 if [ $? -eq 0 ]; then add_gpgkey "$pm_k" ; continue fi fi $ignoreopt || warn "can't find $pm_k; skipping" continue done return 0 } # synopsis: setaction # Sets $myaction or dies if $myaction is already set setaction() { if [ -n "$myaction" ]; then die "you can't specify --$myaction and $1 at the same time" else myaction="$1" fi } # synopsis: setagents # Check validity of agentsopt setagents() { if [ -n "$agentsopt" ]; then agentsopt=$(echo "$agentsopt" | sed 's/,/ /g') unset new_agentsopt for a in $agentsopt; do if command -v ${a}-agent >/dev/null; then new_agentsopt="${new_agentsopt+$new_agentsopt }${a}" else warn "can't find ${a}-agent, removing from list" fi done agentsopt="${new_agentsopt}" else for a in ssh; do command -v ${a}-agent >/dev/null || continue agentsopt="${agentsopt+$agentsopt }${a}" done fi if [ -z "$agentsopt" ]; then die "no agents available to start" fi } # synopsis: confpath # Return private key path if found in ~/.ssh/config SSH configuration file. # Input: the name of the host we would like to connect to. confpath() { h="" while IFS= read -r line; do # get the Host directives case $line in *"Host "*) h=$(echo $line | awk '{print $2}') ;; esac case $line in *IdentityFile*) if [ $h = "$1" ]; then echo $line | awk '{print $2}' break fi esac done < ~/.ssh/config } # synopsis: wantagent prog # Return 0 (true) or 1 (false) depending on whether prog is one of the agents in # agentsopt wantagent() { case "$agentsopt" in "$1"|"$1 "*|*" $1 "*|*" $1") return 0 ;; *) return 1 ;; esac } # # MAIN PROGRAM # # parse the command-line while [ -n "$1" ]; do case "$1" in --help|-h) setaction help ;; --stop|-k) # As of version 2.5, --stop takes an argument. For the sake of # backward compatibility, only eat the arg if it's one we recognize. if [ "$2" = mine ]; then stopwhich=mine; shift elif [ "$2" = others ]; then stopwhich=others; shift elif [ "$2" = all ]; then stopwhich=all; shift else # backward compat stopwhich=all-warn fi ;; --version|-V) setaction version ;; --agents) shift agentsopt="$1" ;; --attempts) shift if [ "$1" -gt 0 ] 2>/dev/null; then attempts=$1 else die "--attempts requires a numeric argument greater than zero" fi ;; --clear) clearopt=true $quickopt && die "--quick and --clear are not compatible" ;; --confirm) confirmopt=true ;; --absolute) absoluteopt=true ;; --dir) shift case "$1" in */.*) keydir="$1" ;; '') die "--dir requires an argument" ;; *) if $absoluteopt; then keydir="$1" else keydir="$1/.keychain" # be backward-compatible fi ;; esac ;; --env) shift if [ -z "$1" ]; then die "--env requires an argument" else envf="$1" fi ;; --eval) evalopt=true ;; --list|-l) ssh-add -l quietopt=true ;; --list-fp|-L) ssh-add -L quietopt=true ;; --query) queryopt=true ;; --host) shift hostopt="$1" ;; --ignore-missing) ignoreopt=true ;; --inherit) shift case "$1" in local|any|local-once|any-once) inheritwhich="$1" ;; *) die "--inherit requires an argument (local, any, local-once or any-once)" ;; esac ;; --noinherit) inheritwhich=none ;; --noask) noaskopt=true ;; --nogui) noguiopt=true ;; --nolock) nolockopt=true ;; --lockwait) shift if [ "$1" -ge 0 ] 2>/dev/null; then lockwait="$1" else die "--lockwait requires an argument zero or greater." fi ;; --quick|-Q) quickopt=true $clearopt && die "--quick and --clear are not compatible" ;; --quiet|-q) quietopt=true ;; --confhost|-c) if [ -e ~/.ssh/config ]; then sshconfig=true confhost="$2" else warn "~/.ssh/config not found; --confhost/-c option ignored." fi ;; --nocolor) color=false ;; --timeout) shift if [ "$1" -gt 0 ] 2>/dev/null; then timeout=$1 else die "--timeout requires a numeric argument greater than zero" fi ;; --systemd) systemdopt=true ;; --gpg2) gpg_prog_name="gpg2" ;; --) shift IFS=" " mykeys=${mykeys+"$mykeys "}"$*" unset IFS break ;; -*) zero=$(basename "$0") echo "$zero: unknown option $1" >&2 $evalopt && { echo; echo "false;"; } exit 1 ;; *) mykeys=${mykeys+"$mykeys "}"$1" ;; esac shift done # Set filenames *after* parsing command-line options to allow # modification of $keydir and/or $hostopt # # 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. fishpidf is the .ssh-agent # file with fish-compatible syntax. lockf is the lockfile, used # to serialize the execution of multiple ssh-agent processes started # simultaneously [ -z "$hostopt" ] && hostopt="${HOSTNAME}" [ -z "$hostopt" ] && hostopt=$(uname -n 2>/dev/null || echo unknown) pidf="${keydir}/${hostopt}-sh" cshpidf="${keydir}/${hostopt}-csh" fishpidf="${keydir}/${hostopt}-fish" olockf="${keydir}/${hostopt}-lock" lockf="${keydir}/${hostopt}-lockf" # Read the env snippet (especially for things like PATH, but could modify # basically anything) if [ -z "$envf" ]; then envf="${keydir}/${hostopt}-env" [ -f "$envf" ] || envf="${keydir}/env" [ -f "$envf" ] || unset envf fi if [ -n "$envf" ]; then . "$envf" fi # Don't use color if there's no terminal on stderr if [ -n "$OFF" ]; then tty <&2 >/dev/null 2>&1 || color=false fi #disable color if necessary, right before our initial newline $color || unset BLUE CYAN CYANN GREEN PURP OFF RED qprint #initial newline mesg "${PURP}keychain ${OFF}${CYANN}${version}${OFF} ~ ${GREEN}http://www.funtoo.org${OFF}" [ "$myaction" = version ] && { versinfo; exit 0; } [ "$myaction" = help ] && { versinfo; helpinfo; exit 0; } # Set up traps # Don't use signal names because they don't work on Cygwin. if $clearopt; then trap '' 2 # disallow ^C until we've had a chance to --clear trap 'droplock; exit 1' 1 15 # drop the lock on signal trap 'droplock; exit 0' 0 # drop the lock on exit else # Don't use signal names because they don't work on Cygwin. trap 'droplock; exit 1' 1 2 15 # drop the lock on signal trap 'droplock; exit 0' 0 # drop the lock on exit fi setagents # verify/set $agentsopt verifykeydir # sets up $keydir wantagent ssh && testssh # sets $openssh and $sunssh getuser # sets $me # Inherit agent info from the environment before loadagents wipes it out. # Always call this since it checks $inheritopt and sets variables accordingly. inheritagents # --stop: kill the existing ssh-agent(s) and quit if [ -n "$stopwhich" ]; then if [ "$stopwhich" = all-warn ]; then warn "--stop without an argument is deprecated; see --help" stopwhich=all fi takelock || die if [ "$stopwhich" = mine -o "$stopwhich" = others ]; then loadagents $agentsopt fi for a in $agentsopt; do stopagent $a done if [ "$stopwhich" != others ]; then qprint exit 0 # stopagent is always successful fi fi # Note regarding locking: if we're trying to be quick, then don't take the lock. # It will be taken later if we discover we can't be quick. if $quickopt; then loadagents $agentsopt # sets ssh_auth_sock, ssh_agent_pid, etc unset nagentsopt for a in $agentsopt; do needstart=true # Trying to be quick has a price... If we discover the agent isn't running, # then we'll have to check things again (in startagent) after taking the # lock. So don't do the initial check unless --quick was specified. if [ $a = ssh ]; then sshavail=$(ssh_l) # try to use existing agent # 0 = found keys, 1 = no keys, 2 = no agent if [ $? = 0 -o \( $? = 1 -a -z "$mykeys" \) ]; then mesg "Found existing ssh-agent: ${CYANN}$ssh_agent_pid${OFF}" needstart=false fi elif [ $a = gpg ]; then # not much way to be quick on this if [ -n "$gpg_agent_pid" ]; then case " $(findpids "${gpg_prog_name}") " in *" $gpg_agent_pid "*) mesg "Found existing gpg-agent: ${CYANN}$gpg_agent_pid${OFF}" needstart=false ;; esac fi fi if $needstart; then nagentsopt="$nagentsopt $a" elif $evalopt; then catpidf $a fi done agentsopt="$nagentsopt" fi # If there are no agents remaining, then bow out now... [ -n "$agentsopt" ] || { qprint; exit 0; } # --timeout translates almost directly to ssh-add/ssh-agent -t, but ssh.com uses # minutes and OpenSSH uses seconds if [ -n "$timeout" ] && wantagent ssh; then ssh_timeout=$timeout if $openssh || $sunssh; then ssh_timeout=$(expr $ssh_timeout \* 60) fi ssh_timeout="-t $ssh_timeout" fi # There are agents remaining to start, and we now know we can't be quick. Take # the lock before continuing takelock || die loadagents $agentsopt unset nagentsopt for a in $agentsopt; do if $queryopt; then catpidf_shell sh $a | cut -d\; -f1 elif startagent $a; then nagentsopt="${nagentsopt+$nagentsopt }$a" $evalopt && catpidf $a fi done agentsopt="$nagentsopt" # If we are just querying the services, exit. $queryopt && exit 0 # If there are no agents remaining, then duck out now... [ -n "$agentsopt" ] || { qprint; exit 0; } # --confirm translates to ssh-add -c if $confirmopt && wantagent ssh; then if $openssh || $sunssh; then ssh_confirm=-c else warn "--confirm only works with OpenSSH" fi fi # --clear: remove all keys from the agent(s) if $clearopt; then for a in ${agentsopt}; do if [ $a = ssh ]; then sshout=$(ssh-add -D 2>&1) if [ $? = 0 ]; then mesg "ssh-agent: $sshout" else warn "ssh-agent: $sshout" fi elif [ $a = gpg ]; then kill -1 $gpg_agent_pid 2>/dev/null mesg "gpg-agent: All identities removed." else warn "--clear not supported for ${a}-agent" fi done trap 'droplock' 2 # done clearing, safe to ctrl-c fi if $systemdopt; then for a in $agentsopt; do systemctl --user set-environment $( catpidf_shell sh $a | cut -d\; -f1 ) done fi # --noask: "don't ask for keys", so we're all done $noaskopt && { qprint; exit 0; } # If the --confhost option used, determine the path to the private key as # written in the ~/.ssh/config and add it to ssh-add. if $sshconfig; then pkeypath=$(confpath "$confhost") eval pkeypath=$pkeypath fi # Parse $mykeys into ssh vs. gpg keys; it may be necessary in the future to # differentiate on the cmdline parse_mykeys "$pkeypath" || die # Load ssh keys if wantagent ssh; then sshavail=$(ssh_l) # update sshavail now that we're locked if [ "$myaction" = "list" ]; then for key in $sshavail end; do [ "$key" = "end" ] && continue echo "$key" done else sshkeys="$(ssh_listmissing)" # cache list of missing keys, newline-separated sshattempts=$attempts savedisplay="$DISPLAY" # Attempt to add the keys while [ -n "$sshkeys" ]; do mesg "Adding ${CYANN}"$(echo "$sshkeys" | wc -l)"${OFF} ssh key(s): $(echo $sshkeys)" # Parse $sshkeys into positional params to preserve spaces in filenames. # This *must* happen after any calls to subroutines because pure Bourne # shell doesn't restore "$@" following a call. Eeeeek! set -f # disable globbing old_IFS="$IFS" # save current IFS IFS=" " # set IFS to newline set -- $sshkeys IFS="$old_IFS" # restore IFS set +f # re-enable globbing if $noguiopt || [ -z "$SSH_ASKPASS" -o -z "$DISPLAY" ]; then unset DISPLAY # DISPLAY="" can cause problems unset SSH_ASKPASS # make sure ssh-add doesn't try SSH_ASKPASS sshout=$(ssh-add ${ssh_timeout} ${ssh_confirm} "$@" 2>&1) else sshout=$(ssh-add ${ssh_timeout} ${ssh_confirm} "$@" 2>&1 /dev/null 2>&1 [ $? != 0 ] && tryagain=true done $tryagain || break if [ $gpgattempts = 1 ]; then die "Problem adding (is pinentry installed?); giving up" else warn "Problem adding; trying again" fi # Update the list of missing keys gpgkeys="$(gpg_listmissing)" # remember, newline-separated # Decrement the countdown gpgattempts=$(expr $gpgattempts - 1) done fi qprint # trailing newline # vim:sw=4 noexpandtab tw=120