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