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