fix remove_monkeysphere_line function to properly handle empty files.
[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-2009, 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 ### UTILITY FUNCTIONS
18
19 # output version info
20 version() {
21     cat "${SYSSHAREDIR}/VERSION"
22 }
23
24 # failure function.  exits with code 255, unless specified otherwise.
25 failure() {
26     [ "$1" ] && echo "$1" >&2
27     exit ${2:-'255'}
28 }
29
30 # write output to stderr based on specified LOG_LEVEL the first
31 # parameter is the priority of the output, and everything else is what
32 # is echoed to stderr.  If there is nothing else, then output comes
33 # from stdin, and is not prefaced by log prefix.
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             if [ "$1" ] ; then
79                 echo -n "ms: " >&2
80                 echo "$@" >&2
81             else
82                 cat >&2
83             fi
84         fi
85     done
86 }
87
88 # run command as monkeysphere user
89 su_monkeysphere_user() {
90     # our main goal here is to run the given command as the the
91     # monkeysphere user, but without prompting for any sort of
92     # authentication.  If this is not possible, we should just fail.
93
94     # FIXME: our current implementation is overly restrictive, because
95     # there may be some su PAM configurations that would allow su
96     # "$MONKEYSPHERE_USER" -c "$@" to Just Work without prompting,
97     # allowing specific users to invoke commands which make use of
98     # this user.
99
100     # chpst (from runit) would be nice to use, but we don't want to
101     # introduce an extra dependency just for this.  This may be a
102     # candidate for re-factoring if we switch implementation languages.
103
104     case $(id -un) in
105         # if monkeysphere user, run the command under bash
106         "$MONKEYSPHERE_USER")
107             bash -c "$@"
108             ;;
109
110          # if root, su command as monkeysphere user
111         'root')
112             su "$MONKEYSPHERE_USER" -c "$@"
113             ;;
114
115         # otherwise, fail
116         *)
117             log error "non-privileged user."
118             ;;
119     esac
120 }
121
122 # cut out all comments(#) and blank lines from standard input
123 meat() {
124     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
125 }
126
127 # cut a specified line from standard input
128 cutline() {
129     head --line="$1" "$2" | tail -1
130 }
131
132 # make a temporary directory
133 msmktempdir() {
134     mktemp -d ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
135 }
136
137 # make a temporary file
138 msmktempfile() {
139     mktemp ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
140 }
141
142 # this is a wrapper for doing lock functions.
143 #
144 # it lets us depend on either lockfile-progs (preferred) or procmail's
145 # lockfile, and should
146 lock() {
147     local use_lockfileprogs=true
148     local action="$1"
149     local file="$2"
150
151     if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
152         if ! ( which lockfile >/dev/null ); then
153             failure "Neither lockfile-create nor lockfile are in the path!"
154         fi
155         use_lockfileprogs=
156     fi
157     
158     case "$action" in
159         create)
160             if [ -n "$use_lockfileprogs" ] ; then
161                 lockfile-create "$file" || failure "unable to lock '$file'"
162             else
163                 lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
164             fi
165             log debug "lock created on '$file'."
166             ;;
167         touch)  
168             if [ -n "$use_lockfileprogs" ] ; then
169                 lockfile-touch --oneshot "$file"
170             else
171                 : Nothing to do here
172             fi
173             log debug "lock touched on '$file'."
174             ;;
175         remove)
176             if [ -n "$use_lockfileprogs" ] ; then
177                 lockfile-remove "$file"
178             else
179                 rm -f "${file}.lock"
180             fi
181             log debug "lock removed on '$file'."
182             ;;
183         *)
184             failure "bad argument for lock subfunction '$action'"
185     esac
186 }
187
188
189 # for portability, between gnu date and BSD date.
190 # arguments should be:  number longunits format
191
192 # e.g. advance_date 20 seconds +%F
193 advance_date() {
194     local gnutry
195     local number="$1"
196     local longunits="$2"
197     local format="$3"
198     local shortunits
199
200     # try things the GNU way first 
201     if date -d "$number $longunits" "$format" >/dev/null 2>&1; then
202         date -d "$number $longunits" "$format"
203     else
204         # otherwise, convert to (a limited version of) BSD date syntax:
205         case "$longunits" in
206             years)
207                 shortunits=y
208                 ;;
209             months)
210                 shortunits=m
211                 ;;
212             weeks)
213                 shortunits=w
214                 ;;
215             days)
216                 shortunits=d
217                 ;;
218             hours)
219                 shortunits=H
220                 ;;
221             minutes)
222                 shortunits=M
223                 ;;
224             seconds)
225                 shortunits=S
226                 ;;
227             *)
228                 # this is a longshot, and will likely fail; oh well.
229                 shortunits="$longunits"
230         esac
231         date "-v+${number}${shortunits}" "$format"
232     fi
233 }
234
235
236 # check that characters are in a string (in an AND fashion).
237 # used for checking key capability
238 # check_capability capability a [b...]
239 check_capability() {
240     local usage
241     local capcheck
242
243     usage="$1"
244     shift 1
245
246     for capcheck ; do
247         if echo "$usage" | grep -q -v "$capcheck" ; then
248             return 1
249         fi
250     done
251     return 0
252 }
253
254 # hash of a file
255 file_hash() {
256     md5sum "$1" 2> /dev/null
257 }
258
259 # convert escaped characters in pipeline from gpg output back into
260 # original character
261 # FIXME: undo all escape character translation in with-colons gpg
262 # output
263 gpg_unescape() {
264     sed 's/\\x3a/:/g'
265 }
266
267 # convert nasty chars into gpg-friendly form in pipeline
268 # FIXME: escape everything, not just colons!
269 gpg_escape() {
270     sed 's/:/\\x3a/g'
271 }
272
273 # prompt for GPG-formatted expiration, and emit result on stdout
274 get_gpg_expiration() {
275     local keyExpire
276
277     keyExpire="$1"
278
279     if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
280         cat >&2 <<EOF
281 Please specify how long the key should be valid.
282          0 = key does not expire
283       <n>  = key expires in n days
284       <n>w = key expires in n weeks
285       <n>m = key expires in n months
286       <n>y = key expires in n years
287 EOF
288         while [ -z "$keyExpire" ] ; do
289             read -p "Key is valid for? (0) " keyExpire
290             if ! test_gpg_expire ${keyExpire:=0} ; then
291                 echo "invalid value" >&2
292                 unset keyExpire
293             fi
294         done
295     elif ! test_gpg_expire "$keyExpire" ; then
296         failure "invalid key expiration value '$keyExpire'."
297     fi
298         
299     echo "$keyExpire"
300 }
301
302 passphrase_prompt() {
303     local prompt="$1"
304     local fifo="$2"
305     local PASS
306
307     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
308         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
309     else
310         read -s -p "$prompt" PASS
311         # Uses the builtin echo, so should not put the passphrase into
312         # the process table.  I think. --dkg
313         echo "$PASS" > "$fifo"
314     fi
315 }
316
317 # remove all lines with specified string from specified file
318 remove_line() {
319     local file
320     local string
321     local tempfile
322
323     file="$1"
324     string="$2"
325
326     if [ -z "$file" -o -z "$string" ] ; then
327         return 1
328     fi
329
330     if [ ! -e "$file" ] ; then
331         return 1
332     fi
333
334     # if the string is in the file...
335     if grep -q -F "$string" "$file" 2> /dev/null ; then
336         tempfile=$(mktemp "${file}.XXXXXXX") || \
337             failure "Unable to make temp file '${file}.XXXXXXX'"
338         
339         # remove the line with the string, and return 0
340         grep -v -F "$string" "$file" >"$tempfile"
341         cat "$tempfile" > "$file"
342         rm "$tempfile"
343         return 0
344     # otherwise return 1
345     else
346         return 1
347     fi
348 }
349
350 # remove all lines with MonkeySphere strings in file
351 remove_monkeysphere_lines() {
352     local file
353     local tempfile
354
355     file="$1"
356
357     # return error if file does not exist
358     if [ ! -e "$file" ] ; then
359         return 1
360     fi
361
362     # just return ok if the file is empty, since there aren't any
363     # lines to remove
364     if [ ! -s "$file" ] ; then
365         return 0
366     fi
367
368     tempfile=$(mktemp "${file}.XXXXXXX") || \
369         failure "Could not make temporary file '${file}.XXXXXXX'."
370
371     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
372         "$file" >"$tempfile"
373     cat "$tempfile" > "$file"
374     rm "$tempfile"
375 }
376
377 # translate ssh-style path variables %h and %u
378 translate_ssh_variables() {
379     local uname
380     local home
381
382     uname="$1"
383     path="$2"
384
385     # get the user's home directory
386     userHome=$(getent passwd "$uname" | cut -d: -f6)
387
388     # translate '%u' to user name
389     path=${path/\%u/"$uname"}
390     # translate '%h' to user home directory
391     path=${path/\%h/"$userHome"}
392
393     echo "$path"
394 }
395
396 # test that a string to conforms to GPG's expiration format
397 test_gpg_expire() {
398     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
399 }
400
401 # check that a file is properly owned, and that all it's parent
402 # directories are not group/other writable
403 check_key_file_permissions() {
404     local uname
405     local path
406     local stat
407     local access
408     local gAccess
409     local oAccess
410
411     # function to check that the given permission corresponds to writability
412     is_write() {
413         [ "$1" = "w" ]
414     }
415
416     uname="$1"
417     path="$2"
418
419     log debug "checking path permission '$path'..."
420
421     # return 255 if cannot stat file
422     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
423         log error "could not stat path '$path'."
424         return 255
425     fi
426
427     owner=$(echo "$stat" | awk '{ print $3 }')
428     gAccess=$(echo "$stat" | cut -c6)
429     oAccess=$(echo "$stat" | cut -c9)
430
431     # return 1 if path has invalid owner
432     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
433         log error "improper ownership on path '$path':"
434         log error " $owner != ($uname|root)"
435         return 1
436     fi
437
438     # return 2 if path has group or other writability
439     if is_write "$gAccess" || is_write "$oAccess" ; then
440         log error "improper group or other writability on path '$path':"
441         log error " group: $gAccess, other: $oAcess"
442         return 2
443     fi
444
445     # return zero if all clear, or go to next path
446     if [ "$path" = '/' ] ; then
447         log debug "path ok."
448         return 0
449     else
450         check_key_file_permissions "$uname" $(dirname "$path")
451     fi
452 }
453
454 ### CONVERSION UTILITIES
455
456 # output the ssh key for a given key ID
457 gpg2ssh() {
458     local keyID
459     
460     keyID="$1"
461
462     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
463 }
464
465 # output known_hosts line from ssh key
466 ssh2known_hosts() {
467     local host
468     local key
469
470     host="$1"
471     key="$2"
472
473     echo -n "$host "
474     echo -n "$key" | tr -d '\n'
475     echo " MonkeySphere${DATE}"
476 }
477
478 # output authorized_keys line from ssh key
479 ssh2authorized_keys() {
480     local userID
481     local key
482     
483     userID="$1"
484     key="$2"
485
486     echo -n "$key" | tr -d '\n'
487     echo " MonkeySphere${DATE} ${userID}"
488 }
489
490 # convert key from gpg to ssh known_hosts format
491 gpg2known_hosts() {
492     local host
493     local keyID
494
495     host="$1"
496     keyID="$2"
497
498     # NOTE: it seems that ssh-keygen -R removes all comment fields from
499     # all lines in the known_hosts file.  why?
500     # NOTE: just in case, the COMMENT can be matched with the
501     # following regexp:
502     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
503     echo -n "$host "
504     gpg2ssh "$keyID" | tr -d '\n'
505     echo " MonkeySphere${DATE}"
506 }
507
508 # convert key from gpg to ssh authorized_keys format
509 gpg2authorized_keys() {
510     local userID
511     local keyID
512
513     userID="$1"
514     keyID="$2"
515
516     # NOTE: just in case, the COMMENT can be matched with the
517     # following regexp:
518     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
519     gpg2ssh "$keyID" | tr -d '\n'
520     echo " MonkeySphere${DATE} ${userID}"
521 }
522
523 ### GPG UTILITIES
524
525 # retrieve all keys with given user id from keyserver
526 # FIXME: need to figure out how to retrieve all matching keys
527 # (not just first N (5 in this case))
528 gpg_fetch_userid() {
529     local returnCode=0
530     local userID
531
532     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
533         return 0
534     fi
535
536     userID="$1"
537
538     log verbose " checking keyserver $KEYSERVER... "
539     echo 1,2,3,4,5 | \
540         gpg --quiet --batch --with-colons \
541         --command-fd 0 --keyserver "$KEYSERVER" \
542         --search ="$userID" > /dev/null 2>&1
543     returnCode="$?"
544
545     return "$returnCode"
546 }
547
548 ########################################################################
549 ### PROCESSING FUNCTIONS
550
551 # userid and key policy checking
552 # the following checks policy on the returned keys
553 # - checks that full key has appropriate valididy (u|f)
554 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
555 # - checks that requested user ID has appropriate validity
556 # (see /usr/share/doc/gnupg/DETAILS.gz)
557 # output is one line for every found key, in the following format:
558 #
559 # flag:sshKey
560 #
561 # "flag" is an acceptability flag, 0 = ok, 1 = bad
562 # "sshKey" is the translated gpg key
563 #
564 # all log output must go to stderr, as stdout is used to pass the
565 # flag:sshKey to the calling function.
566 #
567 # expects global variable: "MODE"
568 process_user_id() {
569     local returnCode=0
570     local userID
571     local requiredCapability
572     local requiredPubCapability
573     local gpgOut
574     local type
575     local validity
576     local keyid
577     local uidfpr
578     local usage
579     local keyOK
580     local uidOK
581     local lastKey
582     local lastKeyOK
583     local fingerprint
584
585     userID="$1"
586
587     # set the required key capability based on the mode
588     if [ "$MODE" = 'known_hosts' ] ; then
589         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
590     elif [ "$MODE" = 'authorized_keys' ] ; then
591         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
592     fi
593     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
594
595     # fetch the user ID if necessary/requested
596     gpg_fetch_userid "$userID"
597
598     # output gpg info for (exact) userid and store
599     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
600         --with-fingerprint --with-fingerprint \
601         ="$userID" 2>/dev/null) || returnCode="$?"
602
603     # if the gpg query return code is not 0, return 1
604     if [ "$returnCode" -ne 0 ] ; then
605         log verbose " no primary keys found."
606         return 1
607     fi
608
609     # loop over all lines in the gpg output and process.
610     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
611     while IFS=: read -r type validity keyid uidfpr usage ; do
612         # process based on record type
613         case $type in
614             'pub') # primary keys
615                 # new key, wipe the slate
616                 keyOK=
617                 uidOK=
618                 lastKey=pub
619                 lastKeyOK=
620                 fingerprint=
621
622                 log verbose " primary key found: $keyid"
623
624                 # if overall key is not valid, skip
625                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
626                     log debug "  - unacceptable primary key validity ($validity)."
627                     continue
628                 fi
629                 # if overall key is disabled, skip
630                 if check_capability "$usage" 'D' ; then
631                     log debug "  - key disabled."
632                     continue
633                 fi
634                 # if overall key capability is not ok, skip
635                 if ! check_capability "$usage" $requiredPubCapability ; then
636                     log debug "  - unacceptable primary key capability ($usage)."
637                     continue
638                 fi
639
640                 # mark overall key as ok
641                 keyOK=true
642
643                 # mark primary key as ok if capability is ok
644                 if check_capability "$usage" $requiredCapability ; then
645                     lastKeyOK=true
646                 fi
647                 ;;
648             'uid') # user ids
649                 if [ "$lastKey" != pub ] ; then
650                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
651                     continue
652                 fi
653                 # if an acceptable user ID was already found, skip
654                 if [ "$uidOK" = 'true' ] ; then
655                     continue
656                 fi
657                 # if the user ID does matches...
658                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
659                     # and the user ID validity is ok
660                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
661                         # mark user ID acceptable
662                         uidOK=true
663                     else
664                         log debug "  - unacceptable user ID validity ($validity)."
665                     fi
666                 else
667                     continue
668                 fi
669
670                 # output a line for the primary key
671                 # 0 = ok, 1 = bad
672                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
673                     log verbose "  * acceptable primary key."
674                     if [ -z "$sshKey" ] ; then
675                         log error "    ! primary key could not be translated (not RSA?)."
676                     else
677                         echo "0:${sshKey}"
678                     fi
679                 else
680                     log debug "  - unacceptable primary key."
681                     if [ -z "$sshKey" ] ; then
682                         log debug "    ! primary key could not be translated (not RSA?)."
683                     else
684                         echo "1:${sshKey}"
685                     fi
686                 fi
687                 ;;
688             'sub') # sub keys
689                 # unset acceptability of last key
690                 lastKey=sub
691                 lastKeyOK=
692                 fingerprint=
693                 
694                 # don't bother with sub keys if the primary key is not valid
695                 if [ "$keyOK" != true ] ; then
696                     continue
697                 fi
698
699                 # don't bother with sub keys if no user ID is acceptable:
700                 if [ "$uidOK" != true ] ; then
701                     continue
702                 fi
703                 
704                 # if sub key validity is not ok, skip
705                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
706                     log debug "  - unacceptable sub key validity ($validity)."
707                     continue
708                 fi
709                 # if sub key capability is not ok, skip
710                 if ! check_capability "$usage" $requiredCapability ; then
711                     log debug "  - unacceptable sub key capability ($usage)."
712                     continue
713                 fi
714
715                 # mark sub key as ok
716                 lastKeyOK=true
717                 ;;
718             'fpr') # key fingerprint
719                 fingerprint="$uidfpr"
720
721                 sshKey=$(gpg2ssh "$fingerprint")
722
723                 # if the last key was the pub key, skip
724                 if [ "$lastKey" = pub ] ; then
725                     continue
726                 fi
727
728                 # output a line for the sub key
729                 # 0 = ok, 1 = bad
730                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
731                     log verbose "  * acceptable sub key."
732                     if [ -z "$sshKey" ] ; then
733                         log error "    ! sub key could not be translated (not RSA?)."
734                     else
735                         echo "0:${sshKey}"
736                     fi
737                 else
738                     log debug "  - unacceptable sub key."
739                     if [ -z "$sshKey" ] ; then
740                         log debug "    ! sub key could not be translated (not RSA?)."
741                     else
742                         echo "1:${sshKey}"
743                     fi
744                 fi
745                 ;;
746         esac
747     done | sort -t: -k1 -n -r
748     # NOTE: this last sort is important so that the "good" keys (key
749     # flag '0') come last.  This is so that they take precedence when
750     # being processed in the key files over "bad" keys (key flag '1')
751 }
752
753 # process a single host in the known_host file
754 process_host_known_hosts() {
755     local host
756     local userID
757     local noKey=
758     local nKeys
759     local nKeysOK
760     local ok
761     local sshKey
762     local tmpfile
763
764     # set the key processing mode
765     export MODE='known_hosts'
766
767     host="$1"
768     userID="ssh://${host}"
769
770     log verbose "processing: $host"
771
772     nKeys=0
773     nKeysOK=0
774
775     IFS=$'\n'
776     for line in $(process_user_id "${userID}") ; do
777         # note that key was found
778         nKeys=$((nKeys+1))
779
780         ok=$(echo "$line" | cut -d: -f1)
781         sshKey=$(echo "$line" | cut -d: -f2)
782
783         if [ -z "$sshKey" ] ; then
784             continue
785         fi
786
787         # remove any old host key line, and note if removed nothing is
788         # removed
789         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
790
791         # if key OK, add new host line
792         if [ "$ok" -eq '0' ] ; then
793             # note that key was found ok
794             nKeysOK=$((nKeysOK+1))
795
796             # hash if specified
797             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
798                 # FIXME: this is really hackish cause ssh-keygen won't
799                 # hash from stdin to stdout
800                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
801                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
802                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
803                 cat "$tmpfile" >> "$KNOWN_HOSTS"
804                 rm -f "$tmpfile" "${tmpfile}.old"
805             else
806                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
807             fi
808
809             # log if this is a new key to the known_hosts file
810             if [ "$noKey" ] ; then
811                 log info "* new key for $host added to known_hosts file."
812             fi
813         fi
814     done
815
816     # if at least one key was found...
817     if [ "$nKeys" -gt 0 ] ; then
818         # if ok keys were found, return 0
819         if [ "$nKeysOK" -gt 0 ] ; then
820             return 0
821         # else return 2
822         else
823             return 2
824         fi
825     # if no keys were found, return 1
826     else
827         return 1
828     fi
829 }
830
831 # update the known_hosts file for a set of hosts listed on command
832 # line
833 update_known_hosts() {
834     local returnCode=0
835     local nHosts
836     local nHostsOK
837     local nHostsBAD
838     local fileCheck
839     local host
840
841     # the number of hosts specified on command line
842     nHosts="$#"
843
844     nHostsOK=0
845     nHostsBAD=0
846
847     # touch the known_hosts file so that the file permission check
848     # below won't fail upon not finding the file
849     (umask 0022 && touch "$KNOWN_HOSTS")
850
851     # check permissions on the known_hosts file path
852     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" || failure
853
854     # create a lockfile on known_hosts:
855     lock create "$KNOWN_HOSTS"
856     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
857     trap "lock remove $KNOWN_HOSTS" EXIT
858
859     # note pre update file checksum
860     fileCheck="$(file_hash "$KNOWN_HOSTS")"
861
862     for host ; do
863         # process the host
864         process_host_known_hosts "$host" || returnCode="$?"
865         # note the result
866         case "$returnCode" in
867             0)
868                 nHostsOK=$((nHostsOK+1))
869                 ;;
870             2)
871                 nHostsBAD=$((nHostsBAD+1))
872                 ;;
873         esac
874
875         # touch the lockfile, for good measure.
876         lock touch "$KNOWN_HOSTS"
877     done
878
879     # remove the lockfile and the trap
880     lock remove "$KNOWN_HOSTS"
881     trap - EXIT
882
883     # note if the known_hosts file was updated
884     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
885         log debug "known_hosts file updated."
886     fi
887
888     # if an acceptable host was found, return 0
889     if [ "$nHostsOK" -gt 0 ] ; then
890         return 0
891     # else if no ok hosts were found...
892     else
893         # if no bad host were found then no hosts were found at all,
894         # and return 1
895         if [ "$nHostsBAD" -eq 0 ] ; then
896             return 1
897         # else if at least one bad host was found, return 2
898         else
899             return 2
900         fi
901     fi
902 }
903
904 # process hosts from a known_hosts file
905 process_known_hosts() {
906     local hosts
907
908     # exit if the known_hosts file does not exist
909     if [ ! -e "$KNOWN_HOSTS" ] ; then
910         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
911     fi
912
913     log debug "processing known_hosts file:"
914     log debug " $KNOWN_HOSTS"
915
916     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
917
918     if [ -z "$hosts" ] ; then
919         log debug "no hosts to process."
920         return
921     fi
922
923     # take all the hosts from the known_hosts file (first
924     # field), grep out all the hashed hosts (lines starting
925     # with '|')...
926     update_known_hosts $hosts
927 }
928
929 # process uids for the authorized_keys file
930 process_uid_authorized_keys() {
931     local userID
932     local nKeys
933     local nKeysOK
934     local ok
935     local sshKey
936
937     # set the key processing mode
938     export MODE='authorized_keys'
939
940     userID="$1"
941
942     log verbose "processing: $userID"
943
944     nKeys=0
945     nKeysOK=0
946
947     IFS=$'\n'
948     for line in $(process_user_id "$userID") ; do
949         # note that key was found
950         nKeys=$((nKeys+1))
951
952         ok=$(echo "$line" | cut -d: -f1)
953         sshKey=$(echo "$line" | cut -d: -f2)
954
955         if [ -z "$sshKey" ] ; then
956             continue
957         fi
958
959         # remove the old host key line
960         remove_line "$AUTHORIZED_KEYS" "$sshKey"
961
962         # if key OK, add new host line
963         if [ "$ok" -eq '0' ] ; then
964             # note that key was found ok
965             nKeysOK=$((nKeysOK+1))
966
967             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
968         fi
969     done
970
971     # if at least one key was found...
972     if [ "$nKeys" -gt 0 ] ; then
973         # if ok keys were found, return 0
974         if [ "$nKeysOK" -gt 0 ] ; then
975             return 0
976         # else return 2
977         else
978             return 2
979         fi
980     # if no keys were found, return 1
981     else
982         return 1
983     fi
984 }
985
986 # update the authorized_keys files from a list of user IDs on command
987 # line
988 update_authorized_keys() {
989     local returnCode=0
990     local userID
991     local nIDs
992     local nIDsOK
993     local nIDsBAD
994     local fileCheck
995
996     # the number of ids specified on command line
997     nIDs="$#"
998
999     nIDsOK=0
1000     nIDsBAD=0
1001
1002     log debug "updating authorized_keys file:"
1003     log debug " $AUTHORIZED_KEYS"
1004
1005     # check permissions on the authorized_keys file path
1006     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1007
1008     # create a lockfile on authorized_keys
1009     lock create "$AUTHORIZED_KEYS"
1010     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1011     trap "lock remove $AUTHORIZED_KEYS" EXIT
1012
1013     # note pre update file checksum
1014     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1015
1016     # remove any monkeysphere lines from authorized_keys file
1017     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1018
1019     for userID ; do
1020         # process the user ID, change return code if key not found for
1021         # user ID
1022         process_uid_authorized_keys "$userID" || returnCode="$?"
1023
1024         # note the result
1025         case "$returnCode" in
1026             0)
1027                 nIDsOK=$((nIDsOK+1))
1028                 ;;
1029             2)
1030                 nIDsBAD=$((nIDsBAD+1))
1031                 ;;
1032         esac
1033
1034         # touch the lockfile, for good measure.
1035         lock touch "$AUTHORIZED_KEYS"
1036     done
1037
1038     # remove the lockfile and the trap
1039     lock remove "$AUTHORIZED_KEYS"
1040
1041     # remove the trap
1042     trap - EXIT
1043
1044     # note if the authorized_keys file was updated
1045     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1046         log debug "authorized_keys file updated."
1047     fi
1048
1049     # if an acceptable id was found, return 0
1050     if [ "$nIDsOK" -gt 0 ] ; then
1051         return 0
1052     # else if no ok ids were found...
1053     else
1054         # if no bad ids were found then no ids were found at all, and
1055         # return 1
1056         if [ "$nIDsBAD" -eq 0 ] ; then
1057             return 1
1058         # else if at least one bad id was found, return 2
1059         else
1060             return 2
1061         fi
1062     fi
1063 }
1064
1065 # process an authorized_user_ids file for authorized_keys
1066 process_authorized_user_ids() {
1067     local line
1068     local nline
1069     local userIDs
1070
1071     authorizedUserIDs="$1"
1072
1073     # exit if the authorized_user_ids file is empty
1074     if [ ! -e "$authorizedUserIDs" ] ; then
1075         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1076     fi
1077
1078     log debug "processing authorized_user_ids file:"
1079     log debug " $authorizedUserIDs"
1080
1081     # check permissions on the authorized_user_ids file path
1082     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1083
1084     if ! meat "$authorizedUserIDs" > /dev/null ; then
1085         log debug " no user IDs to process."
1086         return
1087     fi
1088
1089     nline=0
1090
1091     # extract user IDs from authorized_user_ids file
1092     IFS=$'\n'
1093     for line in $(meat "$authorizedUserIDs") ; do
1094         userIDs["$nline"]="$line"
1095         nline=$((nline+1))
1096     done
1097
1098     update_authorized_keys "${userIDs[@]}"
1099 }
1100
1101 # takes a gpg key or keys on stdin, and outputs a list of
1102 # fingerprints, one per line:
1103 list_primary_fingerprints() {
1104     local fake=$(msmktempdir)
1105     GNUPGHOME="$fake" gpg --no-tty --quiet --import
1106     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1107         awk -F: '/^fpr:/{ print $10 }'
1108     rm -rf "$fake"
1109 }
1110
1111
1112 check_cruft_file() {
1113     local loc="$1"
1114     local version="$2"
1115     
1116     if [ -e "$loc" ] ; then
1117         printf "! The file '%s' is no longer used by\n  monkeysphere (as of version %s), and can be removed.\n\n" "$loc" "$version" | log info
1118     fi
1119 }
1120
1121 check_upgrade_dir() {
1122     local loc="$1"
1123     local version="$2"
1124
1125     if [ -d "$loc" ] ; then
1126         printf "The presence of directory '%s' indicates that you have\nnot yet completed a monkeysphere upgrade.\nYou should probably run the following script:\n  %s/transitions/%s\n\n" "$loc" "$SYSSHAREDIR" "$version" | log info
1127     fi
1128 }
1129
1130 ## look for cruft from old versions of the monkeysphere, and notice if
1131 ## upgrades have not been run:
1132 report_cruft() {
1133     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1134     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1135
1136     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1137     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1138
1139     local found=
1140     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1141         if [ -d "$foo" ] ; then
1142             printf "! %s\n" "$foo" | log info
1143             found=true
1144         fi
1145     done
1146     if [ "$found" ] ; then
1147         printf "The directories above are backups left over from a monkeysphere transition.\nThey may contain copies of sensitive data (host keys, certifier lists), but\nthey are no longer needed by monkeysphere.\nYou may remove them at any time.\n\n" | log info
1148     fi
1149 }