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