working on improving log output
[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     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 ; 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 user
397     local path
398     local access
399     local gAccess
400     local oAccess
401
402     # function to check that the given permission corresponds to writability
403     is_write() {
404         [ "$1" = "w" ]
405     }
406
407     user="$1"
408     path="$2"
409
410     # return 0 is path does not exist
411     [ -e "$path" ] || return 0
412
413     owner=$(ls -l "$path" | awk '{ print $3 }')
414     gAccess=$(ls -l "$path" | cut -c6)
415     oAccess=$(ls -l "$path" | cut -c9)
416
417     # check owner
418     if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
419         return 1
420     fi
421
422     # check group/other writability
423     if is_write "$gAccess" || is_write "$oAccess" ; then
424         return 2
425     fi
426
427     if [ "$path" = '/' ] ; then
428         return 0
429     else
430         check_key_file_permissions $(dirname "$path")
431     fi
432 }
433
434 ### CONVERSION UTILITIES
435
436 # output the ssh key for a given key ID
437 gpg2ssh() {
438     local keyID
439     
440     keyID="$1"
441
442     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
443 }
444
445 # output known_hosts line from ssh key
446 ssh2known_hosts() {
447     local host
448     local key
449
450     host="$1"
451     key="$2"
452
453     echo -n "$host "
454     echo -n "$key" | tr -d '\n'
455     echo " MonkeySphere${DATE}"
456 }
457
458 # output authorized_keys line from ssh key
459 ssh2authorized_keys() {
460     local userID
461     local key
462     
463     userID="$1"
464     key="$2"
465
466     echo -n "$key" | tr -d '\n'
467     echo " MonkeySphere${DATE} ${userID}"
468 }
469
470 # convert key from gpg to ssh known_hosts format
471 gpg2known_hosts() {
472     local host
473     local keyID
474
475     host="$1"
476     keyID="$2"
477
478     # NOTE: it seems that ssh-keygen -R removes all comment fields from
479     # all lines in the known_hosts file.  why?
480     # NOTE: just in case, the COMMENT can be matched with the
481     # following regexp:
482     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
483     echo -n "$host "
484     gpg2ssh "$keyID" | tr -d '\n'
485     echo " MonkeySphere${DATE}"
486 }
487
488 # convert key from gpg to ssh authorized_keys format
489 gpg2authorized_keys() {
490     local userID
491     local keyID
492
493     userID="$1"
494     keyID="$2"
495
496     # NOTE: just in case, the COMMENT can be matched with the
497     # following regexp:
498     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
499     gpg2ssh "$keyID" | tr -d '\n'
500     echo " MonkeySphere${DATE} ${userID}"
501 }
502
503 ### GPG UTILITIES
504
505 # retrieve all keys with given user id from keyserver
506 # FIXME: need to figure out how to retrieve all matching keys
507 # (not just first N (5 in this case))
508 gpg_fetch_userid() {
509     local userID
510     local returnCode
511
512     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
513         return 0
514     fi
515
516     userID="$1"
517
518     log verbose " checking keyserver $KEYSERVER... "
519     echo 1,2,3,4,5 | \
520         gpg --quiet --batch --with-colons \
521         --command-fd 0 --keyserver "$KEYSERVER" \
522         --search ="$userID" > /dev/null 2>&1
523     returnCode="$?"
524
525     # if the user is the monkeysphere user, then update the
526     # monkeysphere user's trustdb
527     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
528         gpg_authentication "--check-trustdb" > /dev/null 2>&1
529     fi
530
531     return "$returnCode"
532 }
533
534 ########################################################################
535 ### PROCESSING FUNCTIONS
536
537 # userid and key policy checking
538 # the following checks policy on the returned keys
539 # - checks that full key has appropriate valididy (u|f)
540 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
541 # - checks that requested user ID has appropriate validity
542 # (see /usr/share/doc/gnupg/DETAILS.gz)
543 # output is one line for every found key, in the following format:
544 #
545 # flag:sshKey
546 #
547 # "flag" is an acceptability flag, 0 = ok, 1 = bad
548 # "sshKey" is the translated gpg key
549 #
550 # all log output must go to stderr, as stdout is used to pass the
551 # flag:sshKey to the calling function.
552 #
553 # expects global variable: "MODE"
554 process_user_id() {
555     local userID
556     local requiredCapability
557     local requiredPubCapability
558     local gpgOut
559     local type
560     local validity
561     local keyid
562     local uidfpr
563     local usage
564     local keyOK
565     local uidOK
566     local lastKey
567     local lastKeyOK
568     local fingerprint
569
570     userID="$1"
571
572     # set the required key capability based on the mode
573     if [ "$MODE" = 'known_hosts' ] ; then
574         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
575     elif [ "$MODE" = 'authorized_keys' ] ; then
576         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
577     fi
578     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
579
580     # fetch the user ID if necessary/requested
581     gpg_fetch_userid "$userID"
582
583     # output gpg info for (exact) userid and store
584     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
585         --with-fingerprint --with-fingerprint \
586         ="$userID" 2>/dev/null)
587
588     # if the gpg query return code is not 0, return 1
589     if [ "$?" -ne 0 ] ; then
590         log verbose " no primary keys found."
591         return 1
592     fi
593
594     # loop over all lines in the gpg output and process.
595     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
596     while IFS=: read -r type validity keyid uidfpr usage ; do
597         # process based on record type
598         case $type in
599             'pub') # primary keys
600                 # new key, wipe the slate
601                 keyOK=
602                 uidOK=
603                 lastKey=pub
604                 lastKeyOK=
605                 fingerprint=
606
607                 log verbose " primary key found: $keyid"
608
609                 # if overall key is not valid, skip
610                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
611                     log debug "  - unacceptable primary key validity ($validity)."
612                     continue
613                 fi
614                 # if overall key is disabled, skip
615                 if check_capability "$usage" 'D' ; then
616                     log debug "  - key disabled."
617                     continue
618                 fi
619                 # if overall key capability is not ok, skip
620                 if ! check_capability "$usage" $requiredPubCapability ; then
621                     log debug "  - unacceptable primary key capability ($usage)."
622                     continue
623                 fi
624
625                 # mark overall key as ok
626                 keyOK=true
627
628                 # mark primary key as ok if capability is ok
629                 if check_capability "$usage" $requiredCapability ; then
630                     lastKeyOK=true
631                 fi
632                 ;;
633             'uid') # user ids
634                 if [ "$lastKey" != pub ] ; then
635                     log verbose " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
636                     continue
637                 fi
638                 # if an acceptable user ID was already found, skip
639                 if [ "$uidOK" = 'true' ] ; then
640                     continue
641                 fi
642                 # if the user ID does matches...
643                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
644                     # and the user ID validity is ok
645                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
646                         # mark user ID acceptable
647                         uidOK=true
648                     fi
649                 else
650                     continue
651                 fi
652
653                 # output a line for the primary key
654                 # 0 = ok, 1 = bad
655                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
656                     log verbose "  * acceptable primary key."
657                     if [ -z "$sshKey" ] ; then
658                         log error "    ! primary key could not be translated (not RSA or DSA?)."
659                     else
660                         echo "0:${sshKey}"
661                     fi
662                 else
663                     log debug "  - unacceptable primary key."
664                     if [ -z "$sshKey" ] ; then
665                         log error "   ! primary key could not be translated (not RSA or DSA?)."
666                     else
667                         echo "1:${sshKey}"
668                     fi
669                 fi
670                 ;;
671             'sub') # sub keys
672                 # unset acceptability of last key
673                 lastKey=sub
674                 lastKeyOK=
675                 fingerprint=
676                 
677                 # don't bother with sub keys if the primary key is not valid
678                 if [ "$keyOK" != true ] ; then
679                     continue
680                 fi
681
682                 # don't bother with sub keys if no user ID is acceptable:
683                 if [ "$uidOK" != true ] ; then
684                     continue
685                 fi
686                 
687                 # if sub key validity is not ok, skip
688                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
689                     continue
690                 fi
691                 # if sub key capability is not ok, skip
692                 if ! check_capability "$usage" $requiredCapability ; then
693                     continue
694                 fi
695
696                 # mark sub key as ok
697                 lastKeyOK=true
698                 ;;
699             'fpr') # key fingerprint
700                 fingerprint="$uidfpr"
701
702                 sshKey=$(gpg2ssh "$fingerprint")
703
704                 # if the last key was the pub key, skip
705                 if [ "$lastKey" = pub ] ; then
706                     continue
707                 fi
708
709                 # output a line for the sub key
710                 # 0 = ok, 1 = bad
711                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
712                     log verbose "  * acceptable sub key."
713                     if [ -z "$sshKey" ] ; then
714                         log error "    ! sub key could not be translated (not RSA or DSA?)."
715                     else
716                         echo "0:${sshKey}"
717                     fi
718                 else
719                     log debug "  - unacceptable sub key."
720                     if [ -z "$sshKey" ] ; then
721                         log error "    ! sub key could not be translated (not RSA or DSA?)."
722                     else
723                         echo "1:${sshKey}"
724                     fi
725                 fi
726                 ;;
727         esac
728     done | sort -t: -k1 -n -r
729     # NOTE: this last sort is important so that the "good" keys (key
730     # flag '0') come last.  This is so that they take precedence when
731     # being processed in the key files over "bad" keys (key flag '1')
732 }
733
734 # process a single host in the known_host file
735 process_host_known_hosts() {
736     local host
737     local userID
738     local nKeys
739     local nKeysOK
740     local ok
741     local sshKey
742     local tmpfile
743
744     host="$1"
745     userID="ssh://${host}"
746
747     log verbose "processing: $host"
748
749     nKeys=0
750     nKeysOK=0
751
752     IFS=$'\n'
753     for line in $(process_user_id "${userID}") ; do
754         # note that key was found
755         nKeys=$((nKeys+1))
756
757         ok=$(echo "$line" | cut -d: -f1)
758         sshKey=$(echo "$line" | cut -d: -f2)
759
760         if [ -z "$sshKey" ] ; then
761             continue
762         fi
763
764         # remove the old host key line, and note if removed
765         remove_line "$KNOWN_HOSTS" "$sshKey"
766
767         # if key OK, add new host line
768         if [ "$ok" -eq '0' ] ; then
769             # note that key was found ok
770             nKeysOK=$((nKeysOK+1))
771
772             # hash if specified
773             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
774                 # FIXME: this is really hackish cause ssh-keygen won't
775                 # hash from stdin to stdout
776                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
777                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
778                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
779                 cat "$tmpfile" >> "$KNOWN_HOSTS"
780                 rm -f "$tmpfile" "${tmpfile}.old"
781             else
782                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
783             fi
784         fi
785     done
786
787     # if at least one key was found...
788     if [ "$nKeys" -gt 0 ] ; then
789         # if ok keys were found, return 0
790         if [ "$nKeysOK" -gt 0 ] ; then
791             return 0
792         # else return 2
793         else
794             return 2
795         fi
796     # if no keys were found, return 1
797     else
798         return 1
799     fi
800 }
801
802 # update the known_hosts file for a set of hosts listed on command
803 # line
804 update_known_hosts() {
805     local nHosts
806     local nHostsOK
807     local nHostsBAD
808     local fileCheck
809     local host
810
811     # the number of hosts specified on command line
812     nHosts="$#"
813
814     nHostsOK=0
815     nHostsBAD=0
816
817     # create a lockfile on known_hosts:
818     lock create "$KNOWN_HOSTS"
819     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
820     trap "lock remove $KNOWN_HOSTS" EXIT
821
822     # note pre update file checksum
823     fileCheck="$(file_hash "$KNOWN_HOSTS")"
824
825     for host ; do
826         # process the host
827         process_host_known_hosts "$host"
828         # note the result
829         case "$?" in
830             0)
831                 nHostsOK=$((nHostsOK+1))
832                 ;;
833             2)
834                 nHostsBAD=$((nHostsBAD+1))
835                 ;;
836         esac
837
838         # touch the lockfile, for good measure.
839         lock touch "$KNOWN_HOSTS"
840     done
841
842     # remove the lockfile and the trap
843     lock remove "$KNOWN_HOSTS"
844     trap - EXIT
845
846     # note if the known_hosts file was updated
847     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
848         log debug "known_hosts file updated."
849     fi
850
851     # if an acceptable host was found, return 0
852     if [ "$nHostsOK" -gt 0 ] ; then
853         return 0
854     # else if no ok hosts were found...
855     else
856         # if no bad host were found then no hosts were found at all,
857         # and return 1
858         if [ "$nHostsBAD" -eq 0 ] ; then
859             return 1
860         # else if at least one bad host was found, return 2
861         else
862             return 2
863         fi
864     fi
865 }
866
867 # process hosts from a known_hosts file
868 process_known_hosts() {
869     local hosts
870
871     log debug "processing known_hosts file..."
872
873     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
874
875     if [ -z "$hosts" ] ; then
876         log debug "no hosts to process."
877         return
878     fi
879
880     # take all the hosts from the known_hosts file (first
881     # field), grep out all the hashed hosts (lines starting
882     # with '|')...
883     update_known_hosts $hosts
884 }
885
886 # process uids for the authorized_keys file
887 process_uid_authorized_keys() {
888     local userID
889     local nKeys
890     local nKeysOK
891     local ok
892     local sshKey
893
894     userID="$1"
895
896     log verbose "processing: $userID"
897
898     nKeys=0
899     nKeysOK=0
900
901     IFS=$'\n'
902     for line in $(process_user_id "$userID") ; do
903         # note that key was found
904         nKeys=$((nKeys+1))
905
906         ok=$(echo "$line" | cut -d: -f1)
907         sshKey=$(echo "$line" | cut -d: -f2)
908
909         if [ -z "$sshKey" ] ; then
910             continue
911         fi
912
913         # remove the old host key line
914         remove_line "$AUTHORIZED_KEYS" "$sshKey"
915
916         # if key OK, add new host line
917         if [ "$ok" -eq '0' ] ; then
918             # note that key was found ok
919             nKeysOK=$((nKeysOK+1))
920
921             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
922         fi
923     done
924
925     # if at least one key was found...
926     if [ "$nKeys" -gt 0 ] ; then
927         # if ok keys were found, return 0
928         if [ "$nKeysOK" -gt 0 ] ; then
929             return 0
930         # else return 2
931         else
932             return 2
933         fi
934     # if no keys were found, return 1
935     else
936         return 1
937     fi
938 }
939
940 # update the authorized_keys files from a list of user IDs on command
941 # line
942 update_authorized_keys() {
943     local userID
944     local nIDs
945     local nIDsOK
946     local nIDsBAD
947     local fileCheck
948
949     # the number of ids specified on command line
950     nIDs="$#"
951
952     nIDsOK=0
953     nIDsBAD=0
954
955     # create a lockfile on authorized_keys
956     lock create "$AUTHORIZED_KEYS"
957     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
958     trap "lock remove $AUTHORIZED_KEYS" EXIT
959
960     # note pre update file checksum
961     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
962
963     # remove any monkeysphere lines from authorized_keys file
964     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
965
966     for userID ; do
967         # process the user ID, change return code if key not found for
968         # user ID
969         process_uid_authorized_keys "$userID"
970
971         # note the result
972         case "$?" in
973             0)
974                 nIDsOK=$((nIDsOK+1))
975                 ;;
976             2)
977                 nIDsBAD=$((nIDsBAD+1))
978                 ;;
979         esac
980
981         # touch the lockfile, for good measure.
982         lock touch "$AUTHORIZED_KEYS"
983     done
984
985     # remove the lockfile and the trap
986     lock remove "$AUTHORIZED_KEYS"
987     trap - EXIT
988
989     # note if the authorized_keys file was updated
990     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
991         log debug "authorized_keys file updated."
992     fi
993
994     # if an acceptable id was found, return 0
995     if [ "$nIDsOK" -gt 0 ] ; then
996         return 0
997     # else if no ok ids were found...
998     else
999         # if no bad ids were found then no ids were found at all, and
1000         # return 1
1001         if [ "$nIDsBAD" -eq 0 ] ; then
1002             return 1
1003         # else if at least one bad id was found, return 2
1004         else
1005             return 2
1006         fi
1007     fi
1008 }
1009
1010 # process an authorized_user_ids file for authorized_keys
1011 process_authorized_user_ids() {
1012     local line
1013     local nline
1014     local userIDs
1015
1016     authorizedUserIDs="$1"
1017
1018     log debug "processing authorized_user_ids file..."
1019
1020     if ! meat "$authorizedUserIDs" > /dev/null ; then
1021         log debug "no user IDs to process."
1022         return
1023     fi
1024
1025     nline=0
1026
1027     # extract user IDs from authorized_user_ids file
1028     IFS=$'\n'
1029     for line in $(meat "$authorizedUserIDs") ; do
1030         userIDs["$nline"]="$line"
1031         nline=$((nline+1))
1032     done
1033
1034     update_authorized_keys "${userIDs[@]}"
1035 }