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