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