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