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