cleanup stray commas, ensure that ill-specified loglevels default to INFO
[monkeysphere.git] / src / common
1 # -*-shell-script-*-
2
3 # Shared sh functions for the monkeysphere
4 #
5 # Written by
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 #
8 # Copyright 2008, released under the GPL, version 3 or later
9
10 # all-caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
12
13 ########################################################################
14 ### COMMON VARIABLES
15
16 # managed directories
17 ETC="/etc/monkeysphere"
18 export ETC
19
20 ########################################################################
21 ### UTILITY FUNCTIONS
22
23 # failure function.  exits with code 255, unless specified otherwise.
24 failure() {
25     echo "$1" >&2
26     exit ${2:-'255'}
27 }
28
29 # write output to stderr based on specified LOG_LEVEL the first
30 # parameter is the priority of the output, and everything else is what
31 # is echoed to stderr
32 log() {
33     local priority
34     local level
35     local output
36     # don't include SILENT in alllevels: it's handled separately
37     # list in decreasing verbosity (all caps)
38     local alllevels="DEBUG INFO ERROR"
39     local found=
40
41     # translate lowers to uppers in global log level
42     LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
43
44     # just go ahead and return if the log level is silent
45     if [ "$LOG_LEVEL" = 'SILENT' ] ; then
46         return
47     fi
48
49     for level in $alllevels; do 
50         if [ "$LOG_LEVEL" = "$level" ] ; then
51             found=true
52         fi
53     done
54     if [ -z "$found" ] ; then
55         # default to INFO:
56         LOG_LEVEL=INFO
57     fi
58
59     # get priority from first parameter, translating all lower to
60     # uppers
61     priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
62     shift
63
64     # scan over available levels
65     for level in $alllevels ; do
66         # output if the log level matches, set output to true
67         # this will output for all subsequent loops as well.
68         if [ "$LOG_LEVEL" = "$level" ] ; then
69             output=true
70         fi
71         if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
72             echo -n "ms: " >&2
73             echo "$@" >&2
74         fi
75     done
76 }
77
78 # cut out all comments(#) and blank lines from standard input
79 meat() {
80     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
81 }
82
83 # cut a specified line from standard input
84 cutline() {
85     head --line="$1" "$2" | tail -1
86 }
87
88 # check that characters are in a string (in an AND fashion).
89 # used for checking key capability
90 # check_capability capability a [b...]
91 check_capability() {
92     local usage
93     local capcheck
94
95     usage="$1"
96     shift 1
97
98     for capcheck ; do
99         if echo "$usage" | grep -q -v "$capcheck" ; then
100             return 1
101         fi
102     done
103     return 0
104 }
105
106 # hash of a file
107 file_hash() {
108     md5sum "$1" 2> /dev/null
109 }
110
111 # convert escaped characters in pipeline from gpg output back into
112 # original character
113 # FIXME: undo all escape character translation in with-colons gpg
114 # output
115 gpg_unescape() {
116     sed 's/\\x3a/:/g'
117 }
118
119 # convert nasty chars into gpg-friendly form in pipeline
120 # FIXME: escape everything, not just colons!
121 gpg_escape() {
122     sed 's/:/\\x3a/g'
123 }
124
125 # prompt for GPG-formatted expiration, and emit result on stdout
126 get_gpg_expiration() {
127     local keyExpire
128
129     keyExpire="$1"
130
131     if [ -z "$keyExpire" ]; then
132         cat >&2 <<EOF
133 Please specify how long the key should be valid.
134          0 = key does not expire
135       <n>  = key expires in n days
136       <n>w = key expires in n weeks
137       <n>m = key expires in n months
138       <n>y = key expires in n years
139 EOF
140         while [ -z "$keyExpire" ] ; do
141             read -p "Key is valid for? (0) " keyExpire
142             if ! test_gpg_expire ${keyExpire:=0} ; then
143                 echo "invalid value" >&2
144                 unset keyExpire
145             fi
146         done
147     elif ! test_gpg_expire "$keyExpire" ; then
148         failure "invalid key expiration value '$keyExpire'."
149     fi
150         
151     echo "$keyExpire"
152 }
153
154 passphrase_prompt() {
155     local prompt="$1"
156     local fifo="$2"
157     local PASS
158
159     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
160         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
161     else
162         read -s -p "$prompt" PASS
163         # Uses the builtin echo, so should not put the passphrase into
164         # the process table.  I think. --dkg
165         echo "$PASS" > "$fifo"
166     fi
167 }
168
169 test_gnu_dummy_s2k_extension() {
170
171 # this block contains a demonstration private key that has had the
172 # primary key stripped out using the GNU S2K extension known as
173 # "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
174 # present in cleartext, however.
175
176 # openpgp2ssh will be able to deal with this based on whether the
177 # local copy of GnuTLS contains read_s2k support that can handle it.
178
179 # read up on that here:
180
181 # http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
182
183 echo "
184 -----BEGIN PGP PRIVATE KEY BLOCK-----
185 Version: GnuPG v1.4.9 (GNU/Linux)
186
187 lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
188 xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
189 G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
190 /gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
191 biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
192 AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
193 ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
194 aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
195 g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
196 mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
197 8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
198 +bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
199 RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
200 4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
201 7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
202 ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
203 kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
204 Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
205 SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
206 vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
207 vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
208 vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
209 =CR5w
210 -----END PGP PRIVATE KEY BLOCK-----
211 " | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
212
213 }
214
215 # remove all lines with specified string from specified file
216 remove_line() {
217     local file
218     local string
219
220     file="$1"
221     string="$2"
222
223     if [ -z "$file" -o -z "$string" ] ; then
224         return 1
225     fi
226
227     if [ ! -e "$file" ] ; then
228         return 1
229     fi
230
231     # if the string is in the file...
232     if grep -q -F "$string" "$file" 2> /dev/null ; then
233         # remove the line with the string, and return 0
234         grep -v -F "$string" "$file" | sponge "$file"
235         return 0
236     # otherwise return 1
237     else
238         return 1
239     fi
240 }
241
242 # remove all lines with MonkeySphere strings in file
243 remove_monkeysphere_lines() {
244     local file
245
246     file="$1"
247
248     if [ -z "$file" ] ; then
249         return 1
250     fi
251
252     if [ ! -e "$file" ] ; then
253         return 1
254     fi
255
256     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
257         "$file" | sponge "$file"
258 }
259
260 # translate ssh-style path variables %h and %u
261 translate_ssh_variables() {
262     local uname
263     local home
264
265     uname="$1"
266     path="$2"
267
268     # get the user's home directory
269     userHome=$(getent passwd "$uname" | cut -d: -f6)
270
271     # translate '%u' to user name
272     path=${path/\%u/"$uname"}
273     # translate '%h' to user home directory
274     path=${path/\%h/"$userHome"}
275
276     echo "$path"
277 }
278
279 # test that a string to conforms to GPG's expiration format
280 test_gpg_expire() {
281     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
282 }
283
284 # check that a file is properly owned, and that all it's parent
285 # directories are not group/other writable
286 check_key_file_permissions() {
287     local user
288     local path
289     local access
290     local gAccess
291     local oAccess
292
293     # function to check that an octal corresponds to writability
294     is_write() {
295         [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
296     }
297
298     user="$1"
299     path="$2"
300
301     # return 0 is path does not exist
302     [ -e "$path" ] || return 0
303
304     owner=$(stat --format '%U' "$path")
305     access=$(stat --format '%a' "$path")
306     gAccess=$(echo "$access" | cut -c2)
307     oAccess=$(echo "$access" | cut -c3)
308
309     # check owner
310     if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
311         return 1
312     fi
313
314     # check group/other writability
315     if is_write "$gAccess" || is_write "$oAccess" ; then
316         return 2
317     fi
318
319     if [ "$path" = '/' ] ; then
320         return 0
321     else
322         check_key_file_permissions $(dirname "$path")
323     fi
324 }
325
326 ### CONVERSION UTILITIES
327
328 # output the ssh key for a given key ID
329 gpg2ssh() {
330     local keyID
331     
332     keyID="$1"
333
334     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
335 }
336
337 # output known_hosts line from ssh key
338 ssh2known_hosts() {
339     local host
340     local key
341
342     host="$1"
343     key="$2"
344
345     echo -n "$host "
346     echo -n "$key" | tr -d '\n'
347     echo " MonkeySphere${DATE}"
348 }
349
350 # output authorized_keys line from ssh key
351 ssh2authorized_keys() {
352     local userID
353     local key
354     
355     userID="$1"
356     key="$2"
357
358     echo -n "$key" | tr -d '\n'
359     echo " MonkeySphere${DATE} ${userID}"
360 }
361
362 # convert key from gpg to ssh known_hosts format
363 gpg2known_hosts() {
364     local host
365     local keyID
366
367     host="$1"
368     keyID="$2"
369
370     # NOTE: it seems that ssh-keygen -R removes all comment fields from
371     # all lines in the known_hosts file.  why?
372     # NOTE: just in case, the COMMENT can be matched with the
373     # following regexp:
374     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
375     echo -n "$host "
376     gpg2ssh "$keyID" | tr -d '\n'
377     echo " MonkeySphere${DATE}"
378 }
379
380 # convert key from gpg to ssh authorized_keys format
381 gpg2authorized_keys() {
382     local userID
383     local keyID
384
385     userID="$1"
386     keyID="$2"
387
388     # NOTE: just in case, the COMMENT can be matched with the
389     # following regexp:
390     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
391     gpg2ssh "$keyID" | tr -d '\n'
392     echo " MonkeySphere${DATE} ${userID}"
393 }
394
395 ### GPG UTILITIES
396
397 # retrieve all keys with given user id from keyserver
398 # FIXME: need to figure out how to retrieve all matching keys
399 # (not just first N (5 in this case))
400 gpg_fetch_userid() {
401     local userID
402     local returnCode
403
404     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
405         return 0
406     fi
407
408     userID="$1"
409
410     log info " checking keyserver $KEYSERVER... "
411     echo 1,2,3,4,5 | \
412         gpg --quiet --batch --with-colons \
413         --command-fd 0 --keyserver "$KEYSERVER" \
414         --search ="$userID" > /dev/null 2>&1
415     returnCode="$?"
416
417     # if the user is the monkeysphere user, then update the
418     # monkeysphere user's trustdb
419     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
420         gpg_authentication "--check-trustdb" > /dev/null 2>&1
421     fi
422
423     return "$returnCode"
424 }
425
426 ########################################################################
427 ### PROCESSING FUNCTIONS
428
429 # userid and key policy checking
430 # the following checks policy on the returned keys
431 # - checks that full key has appropriate valididy (u|f)
432 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
433 # - checks that requested user ID has appropriate validity
434 # (see /usr/share/doc/gnupg/DETAILS.gz)
435 # output is one line for every found key, in the following format:
436 #
437 # flag:sshKey
438 #
439 # "flag" is an acceptability flag, 0 = ok, 1 = bad
440 # "sshKey" is the translated gpg key
441 #
442 # all log output must go to stderr, as stdout is used to pass the
443 # flag:sshKey to the calling function.
444 #
445 # expects global variable: "MODE"
446 process_user_id() {
447     local userID
448     local requiredCapability
449     local requiredPubCapability
450     local gpgOut
451     local type
452     local validity
453     local keyid
454     local uidfpr
455     local usage
456     local keyOK
457     local uidOK
458     local lastKey
459     local lastKeyOK
460     local fingerprint
461
462     userID="$1"
463
464     # set the required key capability based on the mode
465     if [ "$MODE" = 'known_hosts' ] ; then
466         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
467     elif [ "$MODE" = 'authorized_keys' ] ; then
468         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
469     fi
470     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
471
472     # fetch the user ID if necessary/requested
473     gpg_fetch_userid "$userID"
474
475     # output gpg info for (exact) userid and store
476     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
477         --with-fingerprint --with-fingerprint \
478         ="$userID" 2>/dev/null)
479
480     # if the gpg query return code is not 0, return 1
481     if [ "$?" -ne 0 ] ; then
482         log error " no primary keys found."
483         return 1
484     fi
485
486     # loop over all lines in the gpg output and process.
487     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
488     while IFS=: read -r type validity keyid uidfpr usage ; do
489         # process based on record type
490         case $type in
491             'pub') # primary keys
492                 # new key, wipe the slate
493                 keyOK=
494                 uidOK=
495                 lastKey=pub
496                 lastKeyOK=
497                 fingerprint=
498
499                 log error " primary key found: $keyid"
500
501                 # if overall key is not valid, skip
502                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
503                     log error "  - unacceptable primary key validity ($validity)."
504                     continue
505                 fi
506                 # if overall key is disabled, skip
507                 if check_capability "$usage" 'D' ; then
508                     log error "  - key disabled."
509                     continue
510                 fi
511                 # if overall key capability is not ok, skip
512                 if ! check_capability "$usage" $requiredPubCapability ; then
513                     log error "  - unacceptable primary key capability ($usage)."
514                     continue
515                 fi
516
517                 # mark overall key as ok
518                 keyOK=true
519
520                 # mark primary key as ok if capability is ok
521                 if check_capability "$usage" $requiredCapability ; then
522                     lastKeyOK=true
523                 fi
524                 ;;
525             'uid') # user ids
526                 if [ "$lastKey" != pub ] ; then
527                     log error " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
528                     continue
529                 fi
530                 # if an acceptable user ID was already found, skip
531                 if [ "$uidOK" = 'true' ] ; then
532                     continue
533                 fi
534                 # if the user ID does matches...
535                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
536                     # and the user ID validity is ok
537                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
538                         # mark user ID acceptable
539                         uidOK=true
540                     fi
541                 else
542                     continue
543                 fi
544
545                 # output a line for the primary key
546                 # 0 = ok, 1 = bad
547                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
548                     log error "  * acceptable primary key."
549                     if [ -z "$sshKey" ] ; then
550                         log error "    ! primary key could not be translated (not RSA or DSA?)."
551                     else
552                         echo "0:${sshKey}"
553                     fi
554                 else
555                     log error "  - unacceptable primary key."
556                     if [ -z "$sshKey" ] ; then
557                         log error "   ! primary key could not be translated (not RSA or DSA?)."
558                     else
559                         echo "1:${sshKey}"
560                     fi
561                 fi
562                 ;;
563             'sub') # sub keys
564                 # unset acceptability of last key
565                 lastKey=sub
566                 lastKeyOK=
567                 fingerprint=
568                 
569                 # don't bother with sub keys if the primary key is not valid
570                 if [ "$keyOK" != true ] ; then
571                     continue
572                 fi
573
574                 # don't bother with sub keys if no user ID is acceptable:
575                 if [ "$uidOK" != true ] ; then
576                     continue
577                 fi
578                 
579                 # if sub key validity is not ok, skip
580                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
581                     continue
582                 fi
583                 # if sub key capability is not ok, skip
584                 if ! check_capability "$usage" $requiredCapability ; then
585                     continue
586                 fi
587
588                 # mark sub key as ok
589                 lastKeyOK=true
590                 ;;
591             'fpr') # key fingerprint
592                 fingerprint="$uidfpr"
593
594                 sshKey=$(gpg2ssh "$fingerprint")
595
596                 # if the last key was the pub key, skip
597                 if [ "$lastKey" = pub ] ; then
598                     continue
599                 fi
600
601                 # output a line for the sub key
602                 # 0 = ok, 1 = bad
603                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
604                     log error "  * acceptable sub key."
605                     if [ -z "$sshKey" ] ; then
606                         log error "    ! sub key could not be translated (not RSA or DSA?)."
607                     else
608                         echo "0:${sshKey}"
609                     fi
610                 else
611                     log error "  - unacceptable sub key."
612                     if [ -z "$sshKey" ] ; then
613                         log error "    ! sub key could not be translated (not RSA or DSA?)."
614                     else
615                         echo "1:${sshKey}"
616                     fi
617                 fi
618                 ;;
619         esac
620     done | sort -t: -k1 -n -r
621     # NOTE: this last sort is important so that the "good" keys (key
622     # flag '0') come last.  This is so that they take precedence when
623     # being processed in the key files over "bad" keys (key flag '1')
624 }
625
626 # process a single host in the known_host file
627 process_host_known_hosts() {
628     local host
629     local userID
630     local nKeys
631     local nKeysOK
632     local ok
633     local sshKey
634     local tmpfile
635
636     host="$1"
637     userID="ssh://${host}"
638
639     log info "processing: $host"
640
641     nKeys=0
642     nKeysOK=0
643
644     IFS=$'\n'
645     for line in $(process_user_id "${userID}") ; do
646         # note that key was found
647         nKeys=$((nKeys+1))
648
649         ok=$(echo "$line" | cut -d: -f1)
650         sshKey=$(echo "$line" | cut -d: -f2)
651
652         if [ -z "$sshKey" ] ; then
653             continue
654         fi
655
656         # remove the old host key line, and note if removed
657         remove_line "$KNOWN_HOSTS" "$sshKey"
658
659         # if key OK, add new host line
660         if [ "$ok" -eq '0' ] ; then
661             # note that key was found ok
662             nKeysOK=$((nKeysOK+1))
663
664             # hash if specified
665             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
666                 # FIXME: this is really hackish cause ssh-keygen won't
667                 # hash from stdin to stdout
668                 tmpfile=$(mktemp)
669                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
670                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
671                 cat "$tmpfile" >> "$KNOWN_HOSTS"
672                 rm -f "$tmpfile" "${tmpfile}.old"
673             else
674                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
675             fi
676         fi
677     done
678
679     # if at least one key was found...
680     if [ "$nKeys" -gt 0 ] ; then
681         # if ok keys were found, return 0
682         if [ "$nKeysOK" -gt 0 ] ; then
683             return 0
684         # else return 2
685         else
686             return 2
687         fi
688     # if no keys were found, return 1
689     else
690         return 1
691     fi
692 }
693
694 # update the known_hosts file for a set of hosts listed on command
695 # line
696 update_known_hosts() {
697     local nHosts
698     local nHostsOK
699     local nHostsBAD
700     local fileCheck
701     local host
702
703     # the number of hosts specified on command line
704     nHosts="$#"
705
706     nHostsOK=0
707     nHostsBAD=0
708
709     # set the trap to remove any lockfiles on exit
710     trap "lockfile-remove $KNOWN_HOSTS" EXIT
711
712     # create a lockfile on known_hosts
713     lockfile-create "$KNOWN_HOSTS"
714
715     # note pre update file checksum
716     fileCheck="$(file_hash "$KNOWN_HOSTS")"
717
718     for host ; do
719         # process the host
720         process_host_known_hosts "$host"
721         # note the result
722         case "$?" in
723             0)
724                 nHostsOK=$((nHostsOK+1))
725                 ;;
726             2)
727                 nHostsBAD=$((nHostsBAD+1))
728                 ;;
729         esac
730
731         # touch the lockfile, for good measure.
732         lockfile-touch --oneshot "$KNOWN_HOSTS"
733     done
734
735     # remove the lockfile
736     lockfile-remove "$KNOWN_HOSTS"
737
738     # note if the known_hosts file was updated
739     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
740         log info "known_hosts file updated."
741     fi
742
743     # if an acceptable host was found, return 0
744     if [ "$nHostsOK" -gt 0 ] ; then
745         return 0
746     # else if no ok hosts were found...
747     else
748         # if no bad host were found then no hosts were found at all,
749         # and return 1
750         if [ "$nHostsBAD" -eq 0 ] ; then
751             return 1
752         # else if at least one bad host was found, return 2
753         else
754             return 2
755         fi
756     fi
757 }
758
759 # process hosts from a known_hosts file
760 process_known_hosts() {
761     local hosts
762
763     log info "processing known_hosts file..."
764
765     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
766
767     if [ -z "$hosts" ] ; then
768         log error "no hosts to process."
769         return
770     fi
771
772     # take all the hosts from the known_hosts file (first
773     # field), grep out all the hashed hosts (lines starting
774     # with '|')...
775     update_known_hosts $hosts
776 }
777
778 # process uids for the authorized_keys file
779 process_uid_authorized_keys() {
780     local userID
781     local nKeys
782     local nKeysOK
783     local ok
784     local sshKey
785
786     userID="$1"
787
788     log info "processing: $userID"
789
790     nKeys=0
791     nKeysOK=0
792
793     IFS=$'\n'
794     for line in $(process_user_id "$userID") ; do
795         # note that key was found
796         nKeys=$((nKeys+1))
797
798         ok=$(echo "$line" | cut -d: -f1)
799         sshKey=$(echo "$line" | cut -d: -f2)
800
801         if [ -z "$sshKey" ] ; then
802             continue
803         fi
804
805         # remove the old host key line
806         remove_line "$AUTHORIZED_KEYS" "$sshKey"
807
808         # if key OK, add new host line
809         if [ "$ok" -eq '0' ] ; then
810             # note that key was found ok
811             nKeysOK=$((nKeysOK+1))
812
813             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
814         fi
815     done
816
817     # if at least one key was found...
818     if [ "$nKeys" -gt 0 ] ; then
819         # if ok keys were found, return 0
820         if [ "$nKeysOK" -gt 0 ] ; then
821             return 0
822         # else return 2
823         else
824             return 2
825         fi
826     # if no keys were found, return 1
827     else
828         return 1
829     fi
830 }
831
832 # update the authorized_keys files from a list of user IDs on command
833 # line
834 update_authorized_keys() {
835     local userID
836     local nIDs
837     local nIDsOK
838     local nIDsBAD
839     local fileCheck
840
841     # the number of ids specified on command line
842     nIDs="$#"
843
844     nIDsOK=0
845     nIDsBAD=0
846
847     # set the trap to remove any lockfiles on exit
848     trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
849
850     # create a lockfile on authorized_keys
851     lockfile-create "$AUTHORIZED_KEYS"
852
853     # note pre update file checksum
854     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
855
856     # remove any monkeysphere lines from authorized_keys file
857     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
858
859     for userID ; do
860         # process the user ID, change return code if key not found for
861         # user ID
862         process_uid_authorized_keys "$userID"
863
864         # note the result
865         case "$?" in
866             0)
867                 nIDsOK=$((nIDsOK+1))
868                 ;;
869             2)
870                 nIDsBAD=$((nIDsBAD+1))
871                 ;;
872         esac
873
874         # touch the lockfile, for good measure.
875         lockfile-touch --oneshot "$AUTHORIZED_KEYS"
876     done
877
878     # remove the lockfile
879     lockfile-remove "$AUTHORIZED_KEYS"
880
881     # note if the authorized_keys file was updated
882     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
883         log info "authorized_keys file updated."
884     fi
885
886     # if an acceptable id was found, return 0
887     if [ "$nIDsOK" -gt 0 ] ; then
888         return 0
889     # else if no ok ids were found...
890     else
891         # if no bad ids were found then no ids were found at all, and
892         # return 1
893         if [ "$nIDsBAD" -eq 0 ] ; then
894             return 1
895         # else if at least one bad id was found, return 2
896         else
897             return 2
898         fi
899     fi
900 }
901
902 # process an authorized_user_ids file for authorized_keys
903 process_authorized_user_ids() {
904     local line
905     local nline
906     local userIDs
907
908     authorizedUserIDs="$1"
909
910     log info "processing authorized_user_ids file..."
911
912     if ! meat "$authorizedUserIDs" > /dev/null ; then
913         log error "no user IDs to process."
914         return
915     fi
916
917     nline=0
918
919     # extract user IDs from authorized_user_ids file
920     IFS=$'\n'
921     for line in $(meat "$authorizedUserIDs") ; do
922         userIDs["$nline"]="$line"
923         nline=$((nline+1))
924     done
925
926     update_authorized_keys "${userIDs[@]}"
927 }