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