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