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