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