Added info log output when a new key is added to known_hosts file.
[monkeysphere.git] / src / common
index 6fc5f3318d10dc39f4482ddf6efccea44003d7f5..efee9bd69bc1c2389937b1fd66077b887a94a81d 100644 (file)
@@ -4,6 +4,8 @@
 #
 # Written by
 # Jameson Rollins <jrollins@fifthhorseman.net>
+# Jamie McClelland <jm@mayfirst.org>
+# Daniel Kahn Gillmor <dkg@fifthhorseman.net>
 #
 # Copyright 2008, released under the GPL, version 3 or later
 
 ### COMMON VARIABLES
 
 # managed directories
-ETC="/etc/monkeysphere"
-export ETC
+SYSCONFIGDIR=${MONKEYSPHERE_SYSCONFIGDIR:-"/etc/monkeysphere"}
+export SYSCONFIGDIR
 
 ########################################################################
 ### UTILITY FUNCTIONS
 
 # failure function.  exits with code 255, unless specified otherwise.
 failure() {
-    echo "$1" >&2
+    [ "$1" ] && echo "$1" >&2
     exit ${2:-'255'}
 }
 
-# write output to stderr
+# write output to stderr based on specified LOG_LEVEL the first
+# parameter is the priority of the output, and everything else is what
+# is echoed to stderr
 log() {
-    echo -n "ms: " >&2
-    echo "$@" >&2
-}
+    local priority
+    local level
+    local output
+    local alllevels
+    local found=
+
+    # don't include SILENT in alllevels: it's handled separately
+    # list in decreasing verbosity (all caps).
+    # separate with $IFS explicitly, since we do some fancy footwork
+    # elsewhere.
+    alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
+
+    # translate lowers to uppers in global log level
+    LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
+
+    # just go ahead and return if the log level is silent
+    if [ "$LOG_LEVEL" = 'SILENT' ] ; then
+       return
+    fi
+
+    for level in $alllevels ; do 
+       if [ "$LOG_LEVEL" = "$level" ] ; then
+           found=true
+       fi
+    done
+    if [ -z "$found" ] ; then
+       # default to INFO:
+       LOG_LEVEL=INFO
+    fi
 
-loge() {
-    echo "$@" >&2
+    # get priority from first parameter, translating all lower to
+    # uppers
+    priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
+    shift
+
+    # scan over available levels
+    for level in $alllevels ; do
+       # output if the log level matches, set output to true
+       # this will output for all subsequent loops as well.
+       if [ "$LOG_LEVEL" = "$level" ] ; then
+           output=true
+       fi
+       if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
+           echo -n "ms: " >&2
+           echo "$@" >&2
+       fi
+    done
 }
 
 # cut out all comments(#) and blank lines from standard input
@@ -46,6 +91,97 @@ cutline() {
     head --line="$1" "$2" | tail -1
 }
 
+# this is a wrapper for doing lock functions.
+#
+# it lets us depend on either lockfile-progs (preferred) or procmail's
+# lockfile, and should
+lock() {
+    local use_lockfileprogs=true
+    local action="$1"
+    local file="$2"
+
+    if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
+       if ! ( which lockfile >/dev/null ); then
+           failure "Neither lockfile-create nor lockfile are in the path!"
+       fi
+       use_lockfileprogs=
+    fi
+    
+    case "$action" in
+       create)
+           if [ -n "$use_lockfileprogs" ] ; then
+               lockfile-create "$file" || failure "unable to lock '$file'"
+           else
+               lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
+           fi
+           ;;
+       touch)  
+           if [ -n "$use_lockfileprogs" ] ; then
+               lockfile-touch --oneshot "$file"
+           else
+               : Nothing to do here
+           fi
+           ;;
+       remove)
+           if [ -n "$use_lockfileprogs" ] ; then
+               lockfile-remove "$file"
+           else
+               rm -f "${file}.lock"
+           fi
+           ;;
+       *)
+           failure "bad argument for lock subfunction '$action'"
+    esac
+}
+
+
+# for portability, between gnu date and BSD date.
+# arguments should be:  number longunits format
+
+# e.g. advance_date 20 seconds +%F
+advance_date() {
+    local gnutry
+    local number="$1"
+    local longunits="$2"
+    local format="$3"
+    local shortunits
+
+    # try things the GNU way first 
+    if date -d "$number $longunits" "$format" >&/dev/null ; then
+       date -d "$number $longunits" "$format"
+    else
+       # otherwise, convert to (a limited version of) BSD date syntax:
+       case "$longunits" in
+           years)
+               shortunits=y
+               ;;
+           months)
+               shortunits=m
+               ;;
+           weeks)
+               shortunits=w
+               ;;
+           days)
+               shortunits=d
+               ;;
+           hours)
+               shortunits=H
+               ;;
+           minutes)
+               shortunits=M
+               ;;
+           seconds)
+               shortunits=S
+               ;;
+           *)
+               # this is a longshot, and will likely fail; oh well.
+               shortunits="$longunits"
+       esac
+       date "-v+${number}${shortunits}" "$format"
+    fi
+}
+
+
 # check that characters are in a string (in an AND fashion).
 # used for checking key capability
 # check_capability capability a [b...]
@@ -64,17 +200,120 @@ check_capability() {
     return 0
 }
 
-# convert escaped characters from gpg output back into original
-# character
-# FIXME: undo all escape character translation in with-colons gpg output
-unescape() {
-    echo "$1" | sed 's/\\x3a/:/'
+# hash of a file
+file_hash() {
+    md5sum "$1" 2> /dev/null
+}
+
+# convert escaped characters in pipeline from gpg output back into
+# original character
+# FIXME: undo all escape character translation in with-colons gpg
+# output
+gpg_unescape() {
+    sed 's/\\x3a/:/g'
+}
+
+# convert nasty chars into gpg-friendly form in pipeline
+# FIXME: escape everything, not just colons!
+gpg_escape() {
+    sed 's/:/\\x3a/g'
+}
+
+# prompt for GPG-formatted expiration, and emit result on stdout
+get_gpg_expiration() {
+    local keyExpire
+
+    keyExpire="$1"
+
+    if [ -z "$keyExpire" ]; then
+       cat >&2 <<EOF
+Please specify how long the key should be valid.
+         0 = key does not expire
+      <n>  = key expires in n days
+      <n>w = key expires in n weeks
+      <n>m = key expires in n months
+      <n>y = key expires in n years
+EOF
+       while [ -z "$keyExpire" ] ; do
+           read -p "Key is valid for? (0) " keyExpire
+           if ! test_gpg_expire ${keyExpire:=0} ; then
+               echo "invalid value" >&2
+               unset keyExpire
+           fi
+       done
+    elif ! test_gpg_expire "$keyExpire" ; then
+       failure "invalid key expiration value '$keyExpire'."
+    fi
+       
+    echo "$keyExpire"
+}
+
+passphrase_prompt() {
+    local prompt="$1"
+    local fifo="$2"
+    local PASS
+
+    if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
+       "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
+    else
+       read -s -p "$prompt" PASS
+       # Uses the builtin echo, so should not put the passphrase into
+       # the process table.  I think. --dkg
+       echo "$PASS" > "$fifo"
+    fi
+}
+
+test_gnu_dummy_s2k_extension() {
+
+# this block contains a demonstration private key that has had the
+# primary key stripped out using the GNU S2K extension known as
+# "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
+# present in cleartext, however.
+
+# openpgp2ssh will be able to deal with this based on whether the
+# local copy of GnuTLS contains read_s2k support that can handle it.
+
+# read up on that here:
+
+# http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
+
+echo "
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
+xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
+G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
+/gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
+biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
+AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
+ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
+aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
+g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
+mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
+8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
++bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
+RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
+4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
+7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
+ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
+kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
+Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
+SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
+vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
+vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
+vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
+=CR5w
+-----END PGP PRIVATE KEY BLOCK-----
+" | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
+
 }
 
 # remove all lines with specified string from specified file
 remove_line() {
     local file
     local string
+    local tempfile
 
     file="$1"
     string="$2"
@@ -83,10 +322,19 @@ remove_line() {
        return 1
     fi
 
+    if [ ! -e "$file" ] ; then
+       return 1
+    fi
+
     # if the string is in the file...
     if grep -q -F "$string" "$file" 2> /dev/null ; then
+       tempfile=$(mktemp "${file}.XXXXXXX") || \
+           failure "Unable to make temp file '${file}.XXXXXXX'"
+       
        # remove the line with the string, and return 0
-       grep -v -F "$string" "$file" | sponge "$file"
+       grep -v -F "$string" "$file" >"$tempfile"
+       cat "$tempfile" > "$file"
+       rm "$tempfile"
        return 0
     # otherwise return 1
     else
@@ -94,6 +342,30 @@ remove_line() {
     fi
 }
 
+# remove all lines with MonkeySphere strings in file
+remove_monkeysphere_lines() {
+    local file
+    local tempfile
+
+    file="$1"
+
+    if [ -z "$file" ] ; then
+       return 1
+    fi
+
+    if [ ! -e "$file" ] ; then
+       return 1
+    fi
+
+    tempfile=$(mktemp "${file}.XXXXXXX") || \
+       failure "Could not make temporary file '${file}.XXXXXXX'."
+
+    egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
+       "$file" >"$tempfile"
+    cat "$tempfile" > "$file"
+    rm "$tempfile"
+}
+
 # translate ssh-style path variables %h and %u
 translate_ssh_variables() {
     local uname
@@ -115,48 +387,54 @@ translate_ssh_variables() {
 
 # test that a string to conforms to GPG's expiration format
 test_gpg_expire() {
-    echo "$1" | egrep -q "^[0-9][mwy]?$"
+    echo "$1" | egrep -q "^[0-9]+[mwy]?$"
 }
 
 # check that a file is properly owned, and that all it's parent
 # directories are not group/other writable
 check_key_file_permissions() {
-    local user
+    local uname
     local path
+    local stat
     local access
     local gAccess
     local oAccess
 
-    # function to check that an octal corresponds to writability
+    # function to check that the given permission corresponds to writability
     is_write() {
-       [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
+       [ "$1" = "w" ]
     }
 
-    user="$1"
+    uname="$1"
     path="$2"
 
-    # return 0 is path does not exist
-    [ -e "$path" ] || return 0
+    # return 255 if cannot stat file
+    if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
+        log error "could not stat path '$path'."
+       return 255
+    fi
 
-    owner=$(stat --format '%U' "$path")
-    access=$(stat --format '%a' "$path")
-    gAccess=$(echo "$access" | cut -c2)
-    oAccess=$(echo "$access" | cut -c3)
+    owner=$(echo "$stat" | awk '{ print $3 }')
+    gAccess=$(echo "$stat" | cut -c6)
+    oAccess=$(echo "$stat" | cut -c9)
 
-    # check owner
-    if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
+    # return 1 if path has invalid owner
+    if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
+       log error "improper ownership on path '$path'."
        return 1
     fi
 
-    # check group/other writability
+    # return 2 if path has group or other writability
     if is_write "$gAccess" || is_write "$oAccess" ; then
+       log error "improper group or other writability on path '$path'."
        return 2
     fi
 
+    # return zero if all clear, or go to next path
     if [ "$path" = '/' ] ; then
        return 0
     else
-       check_key_file_permissions $(dirname "$path")
+       check_key_file_permissions "$uname" $(dirname "$path")
     fi
 }
 
@@ -244,13 +522,12 @@ gpg_fetch_userid() {
 
     userID="$1"
 
-    log -n " checking keyserver $KEYSERVER... "
+    log verbose " checking keyserver $KEYSERVER... "
     echo 1,2,3,4,5 | \
        gpg --quiet --batch --with-colons \
        --command-fd 0 --keyserver "$KEYSERVER" \
        --search ="$userID" > /dev/null 2>&1
     returnCode="$?"
-    loge "done."
 
     # if the user is the monkeysphere user, then update the
     # monkeysphere user's trustdb
@@ -272,10 +549,13 @@ gpg_fetch_userid() {
 # (see /usr/share/doc/gnupg/DETAILS.gz)
 # output is one line for every found key, in the following format:
 #
-# flag fingerprint
+# flag:sshKey
 #
 # "flag" is an acceptability flag, 0 = ok, 1 = bad
-# "fingerprint" is the fingerprint of the key
+# "sshKey" is the translated gpg key
+#
+# all log output must go to stderr, as stdout is used to pass the
+# flag:sshKey to the calling function.
 #
 # expects global variable: "MODE"
 process_user_id() {
@@ -314,13 +594,11 @@ process_user_id() {
 
     # if the gpg query return code is not 0, return 1
     if [ "$?" -ne 0 ] ; then
-        log "  - key not found."
+        log verbose " no primary keys found."
         return 1
     fi
 
     # loop over all lines in the gpg output and process.
-    # need to do it this way (as opposed to "while read...") so that
-    # variables set in loop will be visible outside of loop
     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
     while IFS=: read -r type validity keyid uidfpr usage ; do
        # process based on record type
@@ -333,21 +611,21 @@ process_user_id() {
                lastKeyOK=
                fingerprint=
 
-               log " primary key found: $keyid"
+               log verbose " primary key found: $keyid"
 
                # if overall key is not valid, skip
                if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
-                   log "  - unacceptable primary key validity ($validity)."
+                   log debug "  - unacceptable primary key validity ($validity)."
                    continue
                fi
                # if overall key is disabled, skip
                if check_capability "$usage" 'D' ; then
-                   log "  - key disabled."
+                   log debug "  - key disabled."
                    continue
                fi
                # if overall key capability is not ok, skip
                if ! check_capability "$usage" $requiredPubCapability ; then
-                   log "  - unacceptable primary key capability ($usage)."
+                   log debug "  - unacceptable primary key capability ($usage)."
                    continue
                fi
 
@@ -360,29 +638,41 @@ process_user_id() {
                fi
                ;;
            'uid') # user ids
-               # if an acceptable user ID was already found, skip
-               if [ "$uidOK" ] ; then
+               if [ "$lastKey" != pub ] ; then
+                   log verbose " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
                    continue
                fi
-               # if the user ID does not match, skip
-               if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
+               # if an acceptable user ID was already found, skip
+               if [ "$uidOK" = 'true' ] ; then
                    continue
                fi
-               # if the user ID validity is not ok, skip
-               if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
+               # if the user ID does matches...
+               if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
+                   # and the user ID validity is ok
+                   if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
+                       # mark user ID acceptable
+                       uidOK=true
+                   fi
+               else
                    continue
                fi
 
-               # mark user ID acceptable
-               uidOK=true
-
                # output a line for the primary key
                # 0 = ok, 1 = bad
                if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
-                   log "  * acceptable key found."
-                   echo "0:${fingerprint}"
+                   log verbose "  * acceptable primary key."
+                   if [ -z "$sshKey" ] ; then
+                       log error "    ! primary key could not be translated (not RSA or DSA?)."
+                   else
+                       echo "0:${sshKey}"
+                   fi
                else
-                   echo "1:${fingerprint}"
+                   log debug "  - unacceptable primary key."
+                   if [ -z "$sshKey" ] ; then
+                       log error "    ! primary key could not be translated (not RSA or DSA?)."
+                   else
+                       echo "1:${sshKey}"
+                   fi
                fi
                ;;
            'sub') # sub keys
@@ -390,7 +680,17 @@ process_user_id() {
                lastKey=sub
                lastKeyOK=
                fingerprint=
+               
+               # don't bother with sub keys if the primary key is not valid
+               if [ "$keyOK" != true ] ; then
+                   continue
+               fi
 
+               # don't bother with sub keys if no user ID is acceptable:
+               if [ "$uidOK" != true ] ; then
+                   continue
+               fi
+               
                # if sub key validity is not ok, skip
                if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
                    continue
@@ -406,58 +706,72 @@ process_user_id() {
            'fpr') # key fingerprint
                fingerprint="$uidfpr"
 
+               sshKey=$(gpg2ssh "$fingerprint")
+
                # if the last key was the pub key, skip
                if [ "$lastKey" = pub ] ; then
                    continue
                fi
-               
-               # output a line for the last subkey
+
+               # output a line for the sub key
                # 0 = ok, 1 = bad
                if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
-                   log "  * acceptable key found."
-                   echo "0:${fingerprint}"
+                   log verbose "  * acceptable sub key."
+                   if [ -z "$sshKey" ] ; then
+                       log error "    ! sub key could not be translated (not RSA or DSA?)."
+                   else
+                       echo "0:${sshKey}"
+                   fi
                else
-                   echo "1:${fingerprint}"
+                   log debug "  - unacceptable sub key."
+                   if [ -z "$sshKey" ] ; then
+                       log error "    ! sub key could not be translated (not RSA or DSA?)."
+                   else
+                       echo "1:${sshKey}"
+                   fi
                fi
                ;;
        esac
-    done
+    done | sort -t: -k1 -n -r
+    # NOTE: this last sort is important so that the "good" keys (key
+    # flag '0') come last.  This is so that they take precedence when
+    # being processed in the key files over "bad" keys (key flag '1')
 }
 
 # process a single host in the known_host file
 process_host_known_hosts() {
     local host
     local userID
+    local noKey=
     local nKeys
     local nKeysOK
     local ok
-    local keyid
+    local sshKey
     local tmpfile
 
     host="$1"
-
-    log "processing host: $host"
-
     userID="ssh://${host}"
 
+    log verbose "processing: $host"
+
     nKeys=0
     nKeysOK=0
 
-    for line in $(process_user_id "ssh://${host}") ; do
+    IFS=$'\n'
+    for line in $(process_user_id "${userID}") ; do
        # note that key was found
        nKeys=$((nKeys+1))
 
        ok=$(echo "$line" | cut -d: -f1)
-       keyid=$(echo "$line" | cut -d: -f2)
+       sshKey=$(echo "$line" | cut -d: -f2)
 
-       sshKey=$(gpg2ssh "$keyid")
         if [ -z "$sshKey" ] ; then
-            log "  ! key could not be translated."
             continue
         fi
 
-       # remove the old host key line, and note if removed
-       remove_line "$KNOWN_HOSTS" "$sshKey"
+       # remove any old host key line, and note if removed nothing is
+       # removed
+       remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
 
        # if key OK, add new host line
        if [ "$ok" -eq '0' ] ; then
@@ -468,7 +782,7 @@ process_host_known_hosts() {
            if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
                # FIXME: this is really hackish cause ssh-keygen won't
                # hash from stdin to stdout
-               tmpfile=$(mktemp)
+               tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
                ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
                ssh-keygen -H -f "$tmpfile" 2> /dev/null
                cat "$tmpfile" >> "$KNOWN_HOSTS"
@@ -476,6 +790,11 @@ process_host_known_hosts() {
            else
                ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
            fi
+
+           # log if this is a new key to the known_hosts file
+           if [ "$noKey" ] ; then
+               log info "* new key for $host added to known_hosts file."
+           fi
        fi
     done
 
@@ -500,6 +819,7 @@ update_known_hosts() {
     local nHosts
     local nHostsOK
     local nHostsBAD
+    local fileCheck
     local host
 
     # the number of hosts specified on command line
@@ -508,11 +828,13 @@ update_known_hosts() {
     nHostsOK=0
     nHostsBAD=0
 
-    # set the trap to remove any lockfiles on exit
-    trap "lockfile-remove $KNOWN_HOSTS" EXIT
+    # create a lockfile on known_hosts:
+    lock create "$KNOWN_HOSTS"
+    # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
+    trap "lock remove $KNOWN_HOSTS" EXIT
 
-    # create a lockfile on known_hosts
-    lockfile-create "$KNOWN_HOSTS"
+    # note pre update file checksum
+    fileCheck="$(file_hash "$KNOWN_HOSTS")"
 
     for host ; do
        # process the host
@@ -528,15 +850,16 @@ update_known_hosts() {
        esac
 
        # touch the lockfile, for good measure.
-       lockfile-touch --oneshot "$KNOWN_HOSTS"
+       lock touch "$KNOWN_HOSTS"
     done
 
-    # remove the lockfile
-    lockfile-remove "$KNOWN_HOSTS"
+    # remove the lockfile and the trap
+    lock remove "$KNOWN_HOSTS"
+    trap - EXIT
 
     # note if the known_hosts file was updated
-    if [ "$nHostsOK" -gt 0 -o "$nHostsBAD" -gt 0 ] ; then
-       log "known_hosts file updated."
+    if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
+       log debug "known_hosts file updated."
     fi
 
     # if an acceptable host was found, return 0
@@ -559,10 +882,15 @@ update_known_hosts() {
 process_known_hosts() {
     local hosts
 
-    log "processing known_hosts file..."
+    log debug "processing known_hosts file..."
 
     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
 
+    if [ -z "$hosts" ] ; then
+       log debug "no hosts to process."
+       return
+    fi
+
     # take all the hosts from the known_hosts file (first
     # field), grep out all the hashed hosts (lines starting
     # with '|')...
@@ -575,25 +903,24 @@ process_uid_authorized_keys() {
     local nKeys
     local nKeysOK
     local ok
-    local keyid
+    local sshKey
 
     userID="$1"
 
-    log "processing user ID: $userID"
+    log verbose "processing: $userID"
 
     nKeys=0
     nKeysOK=0
 
+    IFS=$'\n'
     for line in $(process_user_id "$userID") ; do
        # note that key was found
        nKeys=$((nKeys+1))
 
        ok=$(echo "$line" | cut -d: -f1)
-       keyid=$(echo "$line" | cut -d: -f2)
+       sshKey=$(echo "$line" | cut -d: -f2)
 
-       sshKey=$(gpg2ssh "$keyid")
         if [ -z "$sshKey" ] ; then
-            log "  ! key could not be translated."
             continue
         fi
 
@@ -631,6 +958,7 @@ update_authorized_keys() {
     local nIDs
     local nIDsOK
     local nIDsBAD
+    local fileCheck
 
     # the number of ids specified on command line
     nIDs="$#"
@@ -638,11 +966,16 @@ update_authorized_keys() {
     nIDsOK=0
     nIDsBAD=0
 
-    # set the trap to remove any lockfiles on exit
-    trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
-
     # create a lockfile on authorized_keys
-    lockfile-create "$AUTHORIZED_KEYS"
+    lock create "$AUTHORIZED_KEYS"
+    # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
+    trap "lock remove $AUTHORIZED_KEYS" EXIT
+
+    # note pre update file checksum
+    fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
+
+    # remove any monkeysphere lines from authorized_keys file
+    remove_monkeysphere_lines "$AUTHORIZED_KEYS"
 
     for userID ; do
        # process the user ID, change return code if key not found for
@@ -660,15 +993,16 @@ update_authorized_keys() {
        esac
 
        # touch the lockfile, for good measure.
-       lockfile-touch --oneshot "$AUTHORIZED_KEYS"
+       lock touch "$AUTHORIZED_KEYS"
     done
 
-    # remove the lockfile
-    lockfile-remove "$AUTHORIZED_KEYS"
+    # remove the lockfile and the trap
+    lock remove "$AUTHORIZED_KEYS"
+    trap - EXIT
 
     # note if the authorized_keys file was updated
-    if [ "$nIDsOK" -gt 0 -o "$nIDsBAD" -gt 0 ] ; then
-       log "authorized_keys file updated."
+    if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
+       log debug "authorized_keys file updated."
     fi
 
     # if an acceptable id was found, return 0
@@ -695,7 +1029,12 @@ process_authorized_user_ids() {
 
     authorizedUserIDs="$1"
 
-    log "processing authorized_user_ids file..."
+    log debug "processing authorized_user_ids file..."
+
+    if ! meat "$authorizedUserIDs" > /dev/null ; then
+       log debug " no user IDs to process."
+       return
+    fi
 
     nline=0
 
@@ -708,45 +1047,3 @@ process_authorized_user_ids() {
 
     update_authorized_keys "${userIDs[@]}"
 }
-
-# EXPERIMENTAL (unused) process userids found in authorized_keys file
-# go through line-by-line, extract monkeysphere userids from comment
-# fields, and process each userid
-# NOT WORKING
-process_authorized_keys() {
-    local authorizedKeys
-    local userID
-    local returnCode
-
-    # default return code is 0, and is set to 1 if a key for a user
-    # is not found
-    returnCode=0
-
-    authorizedKeys="$1"
-
-    # take all the monkeysphere userids from the authorized_keys file
-    # comment field (third field) that starts with "MonkeySphere uid:"
-    # FIXME: needs to handle authorized_keys options (field 0)
-    meat "$authorizedKeys" | \
-    while read -r options keytype key comment ; do
-       # if the comment field is empty, assume the third field was
-       # the comment
-       if [ -z "$comment" ] ; then
-           comment="$key"
-       fi
-
-       if echo "$comment" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}' ; then
-           continue
-       fi
-       userID=$(echo "$comment" | awk "{ print $2 }")
-       if [ -z "$userID" ] ; then
-           continue
-       fi
-
-       # process the userid
-       log "processing userid: '$userID'"
-       process_user_id "$userID" > /dev/null || returnCode=1
-    done
-
-    return "$returnCode"
-}