fix remove_monkeysphere_lines function to just read from stdin and write to stdout
[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" != 'false' ]; 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             printf "Key is valid for? (0) " >&2
295             read keyExpire
296             if ! test_gpg_expire ${keyExpire:=0} ; then
297                 echo "invalid value" >&2
298                 unset keyExpire
299             fi
300         done
301     elif ! test_gpg_expire "$keyExpire" ; then
302         failure "invalid key expiration value '$keyExpire'."
303     fi
304         
305     echo "$keyExpire"
306 }
307
308 passphrase_prompt() {
309     local prompt="$1"
310     local fifo="$2"
311     local PASS
312
313     if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
314         printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
315         printf '(with prompt "%s")\n' "$prompt" | log debug
316         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
317     else
318         read -s -p "$prompt" PASS
319         # Uses the builtin echo, so should not put the passphrase into
320         # the process table.  I think. --dkg
321         echo "$PASS" > "$fifo"
322     fi
323 }
324
325 # remove all lines with specified string from specified file
326 remove_line() {
327     local file
328     local string
329     local tempfile
330
331     file="$1"
332     string="$2"
333
334     if [ -z "$file" -o -z "$string" ] ; then
335         return 1
336     fi
337
338     if [ ! -e "$file" ] ; then
339         return 1
340     fi
341
342     # if the string is in the file...
343     if grep -q -F "$string" "$file" 2>/dev/null ; then
344         tempfile=$(mktemp "${file}.XXXXXXX") || \
345             failure "Unable to make temp file '${file}.XXXXXXX'"
346         
347         # remove the line with the string, and return 0
348         grep -v -F "$string" "$file" >"$tempfile"
349         cat "$tempfile" > "$file"
350         rm "$tempfile"
351         return 0
352     # otherwise return 1
353     else
354         return 1
355     fi
356 }
357
358 # remove all lines with MonkeySphere strings from stdin
359 remove_monkeysphere_lines() {
360     egrep -v ' MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2} '
361 }
362
363 # translate ssh-style path variables %h and %u
364 translate_ssh_variables() {
365     local uname
366     local home
367
368     uname="$1"
369     path="$2"
370
371     # get the user's home directory
372     userHome=$(get_homedir "$uname")
373
374     # translate '%u' to user name
375     path=${path/\%u/"$uname"}
376     # translate '%h' to user home directory
377     path=${path/\%h/"$userHome"}
378
379     echo "$path"
380 }
381
382 # test that a string to conforms to GPG's expiration format
383 test_gpg_expire() {
384     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
385 }
386
387 # check that a file is properly owned, and that all it's parent
388 # directories are not group/other writable
389 check_key_file_permissions() {
390     local uname
391     local path
392
393     uname="$1"
394     path="$2"
395
396     if [ "$STRICT_MODES" = 'false' ] ; then
397         log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
398         return 0
399     fi
400     log debug "checking path permission '$path'..."
401     "${SYSSHAREDIR}/checkperms" "$uname" "$path"
402 }
403
404 # return a list of all users on the system
405 list_users() {
406     if type getent &>/dev/null ; then
407         # for linux and FreeBSD systems
408         getent passwd | cut -d: -f1
409     elif type dscl &>/dev/null ; then
410         # for Darwin systems
411         dscl localhost -list /Search/Users
412     else
413         failure "Neither getent or dscl is in the path!  Could not determine list of users."
414     fi
415 }
416
417 # take one argument, a service name.  in response, print a series of
418 # lines, each with a unique numeric port number that might be
419 # associated with that service name.  (e.g. in: "https", out: "443")
420 # if nothing is found, print nothing, and return 0.
421
422 # return 1 if there was an error in the search somehow
423 get_port_for_service() {
424
425     [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
426         failure $(printf "This is not a valid service name: '%s'" "$1")
427     if type getent &>/dev/null ; then
428         # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
429         (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
430     elif [ -r /etc/services ] ; then
431         # fall back to /etc/services for systems that don't have getent (MacOS?)
432         # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
433         awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
434     else
435         return 1
436     fi
437 }
438
439 # return the path to the home directory of a user
440 get_homedir() {
441     local uname=${1:-`whoami`}
442     eval "echo ~${uname}"
443 }
444
445 # return the primary group of a user
446 get_primary_group() {
447     local uname=${1:-`whoami`}
448     groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
449 }
450
451 ### CONVERSION UTILITIES
452
453 # output the ssh key for a given key ID
454 gpg2ssh() {
455     local keyID
456     
457     keyID="$1"
458
459     gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
460 }
461
462 # output known_hosts line from ssh key
463 ssh2known_hosts() {
464     local host
465     local port
466     local key
467
468     # FIXME this does not properly deal with IPv6 hosts using the
469     # standard port (because it's unclear whether their final
470     # colon-delimited address section is a port number or an address
471     # string)
472     host=${1%:*}
473     port=${1##*:}
474     key="$2"
475
476     # specify the host and port properly for new ssh known_hosts
477     # format
478     if [ "$port" != "$host" ] ; then
479         host="[${host}]:${port}"
480     fi
481     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
482 }
483
484 # output authorized_keys line from ssh key
485 ssh2authorized_keys() {
486     local userID="$1"
487     local key="$2"
488
489     if [[ "$AUTHORIZED_KEYS_OPTIONS" ]]; then
490         printf "%s %s MonkeySphere%s %s\n" "$AUTHORIZED_KEYS_OPTIONS" "$key" "$DATE" "$userID"
491     else
492         printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
493     fi
494 }
495
496 # convert key from gpg to ssh known_hosts format
497 gpg2known_hosts() {
498     local host
499     local keyID
500     local key
501
502     host="$1"
503     keyID="$2"
504
505     key=$(gpg2ssh "$keyID")
506
507     # NOTE: it seems that ssh-keygen -R removes all comment fields from
508     # all lines in the known_hosts file.  why?
509     # NOTE: just in case, the COMMENT can be matched with the
510     # following regexp:
511     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
512     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
513 }
514
515 # convert key from gpg to ssh authorized_keys format
516 gpg2authorized_keys() {
517     local userID
518     local keyID
519     local key
520
521     userID="$1"
522     keyID="$2"
523
524     key=$(gpg2ssh "$keyID")
525
526     # NOTE: just in case, the COMMENT can be matched with the
527     # following regexp:
528     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
529     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
530 }
531
532 ### GPG UTILITIES
533
534 # script to determine if gpg version is equal to or greater than specified version
535 is_gpg_version_greater_equal() {
536     local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
537     local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
538         | tr '.' ' ' | sort -g -k1 -k2 -k3 \
539         | tail -1 | tr ' ' '.')
540     [[ "$gpgVersion" == "$latest" ]]
541 }
542
543 # retrieve all keys with given user id from keyserver
544 # FIXME: need to figure out how to retrieve all matching keys
545 # (not just first N (5 in this case))
546 gpg_fetch_userid() {
547     local returnCode=0
548     local userID
549
550     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
551         return 0
552     fi
553
554     userID="$1"
555
556     log verbose " checking keyserver $KEYSERVER... "
557     echo 1,2,3,4,5 | \
558         gpg --quiet --batch --with-colons \
559         --command-fd 0 --keyserver "$KEYSERVER" \
560         --search ="$userID" &>/dev/null
561     returnCode="$?"
562
563     if [ "$returnCode" != 0 ] ; then
564         log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
565     fi
566
567     return "$returnCode"
568 }
569
570 ########################################################################
571 ### PROCESSING FUNCTIONS
572
573 # userid and key policy checking
574 # the following checks policy on the returned keys
575 # - checks that full key has appropriate valididy (u|f)
576 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
577 # - checks that requested user ID has appropriate validity
578 # (see /usr/share/doc/gnupg/DETAILS.gz)
579 # output is one line for every found key, in the following format:
580 #
581 # flag:sshKey
582 #
583 # "flag" is an acceptability flag, 0 = ok, 1 = bad
584 # "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
585 #
586 # all log output must go to stderr, as stdout is used to pass the
587 # flag:sshKey to the calling function.
588 process_user_id() {
589     local returnCode=0
590     local userID="$1"
591     local requiredCapability
592     local requiredPubCapability
593     local gpgOut
594     local type
595     local validity
596     local keyid
597     local uidfpr
598     local usage
599     local keyOK
600     local uidOK
601     local lastKey
602     local lastKeyOK
603     local fingerprint
604
605     # set the required key capability based on the mode
606     requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
607     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
608
609     # fetch the user ID if necessary/requested
610     gpg_fetch_userid "$userID"
611
612     # output gpg info for (exact) userid and store
613     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
614         --with-fingerprint --with-fingerprint \
615         ="$userID" 2>/dev/null) || returnCode="$?"
616
617     # if the gpg query return code is not 0, return 1
618     if [ "$returnCode" -ne 0 ] ; then
619         log verbose " no primary keys found."
620         return 1
621     fi
622
623     # loop over all lines in the gpg output and process.
624     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
625     while IFS=: read -r type validity keyid uidfpr usage ; do
626         # process based on record type
627         case $type in
628             'pub') # primary keys
629                 # new key, wipe the slate
630                 keyOK=
631                 uidOK=
632                 lastKey=pub
633                 lastKeyOK=
634                 fingerprint=
635
636                 log verbose " primary key found: $keyid"
637
638                 # if overall key is not valid, skip
639                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
640                     log debug "  - unacceptable primary key validity ($validity)."
641                     continue
642                 fi
643                 # if overall key is disabled, skip
644                 if check_capability "$usage" 'D' ; then
645                     log debug "  - key disabled."
646                     continue
647                 fi
648                 # if overall key capability is not ok, skip
649                 if ! check_capability "$usage" $requiredPubCapability ; then
650                     log debug "  - unacceptable primary key capability ($usage)."
651                     continue
652                 fi
653
654                 # mark overall key as ok
655                 keyOK=true
656
657                 # mark primary key as ok if capability is ok
658                 if check_capability "$usage" $requiredCapability ; then
659                     lastKeyOK=true
660                 fi
661                 ;;
662             'uid') # user ids
663                 if [ "$lastKey" != pub ] ; then
664                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
665                     continue
666                 fi
667                 # if an acceptable user ID was already found, skip
668                 if [ "$uidOK" = 'true' ] ; then
669                     continue
670                 fi
671                 # if the user ID does matches...
672                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
673                     # and the user ID validity is ok
674                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
675                         # mark user ID acceptable
676                         uidOK=true
677                     else
678                         log debug "  - unacceptable user ID validity ($validity)."
679                     fi
680                 else
681                     continue
682                 fi
683
684                 # output a line for the primary key
685                 # 0 = ok, 1 = bad
686                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
687                     log verbose "  * acceptable primary key."
688                     if [ -z "$sshKey" ] ; then
689                         log verbose "    ! primary key could not be translated (not RSA?)."
690                     else
691                         echo "0:${sshKey}"
692                     fi
693                 else
694                     log debug "  - unacceptable primary key."
695                     if [ -z "$sshKey" ] ; then
696                         log debug "    ! primary key could not be translated (not RSA?)."
697                     else
698                         echo "1:${sshKey}"
699                     fi
700                 fi
701                 ;;
702             'sub') # sub keys
703                 # unset acceptability of last key
704                 lastKey=sub
705                 lastKeyOK=
706                 fingerprint=
707                 
708                 # don't bother with sub keys if the primary key is not valid
709                 if [ "$keyOK" != true ] ; then
710                     continue
711                 fi
712
713                 # don't bother with sub keys if no user ID is acceptable:
714                 if [ "$uidOK" != true ] ; then
715                     continue
716                 fi
717                 
718                 # if sub key validity is not ok, skip
719                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
720                     log debug "  - unacceptable sub key validity ($validity)."
721                     continue
722                 fi
723                 # if sub key capability is not ok, skip
724                 if ! check_capability "$usage" $requiredCapability ; then
725                     log debug "  - unacceptable sub key capability ($usage)."
726                     continue
727                 fi
728
729                 # mark sub key as ok
730                 lastKeyOK=true
731                 ;;
732             'fpr') # key fingerprint
733                 fingerprint="$uidfpr"
734
735                 sshKey=$(gpg2ssh "$fingerprint")
736
737                 # if the last key was the pub key, skip
738                 if [ "$lastKey" = pub ] ; then
739                     continue
740                 fi
741
742                 # output a line for the sub key
743                 # 0 = ok, 1 = bad
744                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
745                     log verbose "  * acceptable sub key."
746                     if [ -z "$sshKey" ] ; then
747                         log error "    ! sub key could not be translated (not RSA?)."
748                     else
749                         echo "0:${sshKey}"
750                     fi
751                 else
752                     log debug "  - unacceptable sub key."
753                     if [ -z "$sshKey" ] ; then
754                         log debug "    ! sub key could not be translated (not RSA?)."
755                     else
756                         echo "1:${sshKey}"
757                     fi
758                 fi
759                 ;;
760         esac
761     done | sort -t: -k1 -n -r
762     # NOTE: this last sort is important so that the "good" keys (key
763     # flag '0') come last.  This is so that they take precedence when
764     # being processed in the key files over "bad" keys (key flag '1')
765 }
766
767 process_keys_for_file() {
768     local keyFile="$1"
769     local userID="$2"
770     local host
771     local ok
772     local sshKey
773     local noKey=
774
775     log verbose "processing: $userID"
776     log debug "key file: $keyFile"
777
778     IFS=$'\n'
779     for line in $(process_user_id "$userID") ; do
780         ok=${line%%:*}
781         sshKey=${line#*:}
782
783         if [ -z "$sshKey" ] ; then
784             continue
785         fi
786
787         # remove the old host key line
788         if [[ "$keyFile" != '-' ]] ; then
789             case "$FILE_TYPE" in
790                 ('authorized_keys')
791                     remove_line "$keyFile" "$sshKey" || noKey=true
792                     ;;
793                 ('known_hosts')
794                     host=${userID#ssh://}
795                     remove_line "$keyFile" "${host}.*${sshKey}" || noKey=true
796                     ;;
797             esac
798         fi
799
800         # if key OK, add new host line
801         if [ "$ok" -eq '0' ] ; then
802             case "$FILE_TYPE" in
803                 ('raw')
804                     echo "$sshKey" | log debug
805                     if [[ "$keyFile" == '-' ]] ; then
806                         echo "$sshKey"
807                     else
808                         echo "$sshKey" >>"$keyFile"
809                     fi
810                     ;;
811                 ('authorized_keys')
812                     ssh2authorized_keys "$userID" "$sshKey" | log debug
813                     if [[ "$keyFile" == '-' ]] ; then
814                         ssh2authorized_keys "$userID" "$sshKey"
815                     else
816                         ssh2authorized_keys "$userID" "$sshKey" >> "$keyFile"
817                     fi
818                     ;;
819                 ('known_hosts')
820                     host=${userID#ssh://}
821                     ssh2known_hosts "$host" "$sshKey" | log debug
822                     # hash if specified
823                     if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
824                         if (type ssh-keygen >/dev/null) ; then
825                             # FIXME: this is really hackish cause
826                             # ssh-keygen won't hash from stdin to
827                             # stdout
828                             tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
829                             ssh2known_hosts "$host" "$sshKey" \
830                                 > "$tmpfile"
831                             ssh-keygen -H -f "$tmpfile" 2>/dev/null
832                             if [[ "$keyFile" == '-' ]] ; then
833                                 cat "$tmpfile"
834                             else
835                                 cat "$tmpfile" >> "$keyFile"
836                             fi
837                             rm -f "$tmpfile" "${tmpfile}.old"
838                             # FIXME: we could do this without needing
839                             # ssh-keygen.  hashed known_hosts looks
840                             # like: |1|X|Y where 1 means SHA1 (nothing
841                             # else is defined in openssh sources), X
842                             # is the salt (same length as the digest
843                             # output), base64-encoded, and Y is the
844                             # digested hostname (also base64-encoded).
845                             # see hostfile.{c,h} in openssh sources.
846                         else
847                             failure "Cannot hash known_hosts as requested"
848                         fi
849
850                         # log if this is a new key to the known_hosts file
851                         if [ "$noKey" ] ; then
852                             log info "* new key will be added to known_hosts file."
853                         fi
854                     else
855                         if [[ "$keyFile" == '-' ]] ; then
856                             ssh2known_hosts "$host" "$sshKey"
857                         else
858                             ssh2known_hosts "$host" "$sshKey" >>"$keyFile"
859                         fi
860                     fi
861                     ;;
862             esac
863         fi
864     done
865 }
866
867 # process an authorized_user_ids file on stdin for authorized_keys
868 process_authorized_user_ids() {
869     local authorizedKeys="$1"
870     declare -i nline=0
871     local line
872     declare -a userIDs
873     declare -a koptions
874
875     # extract user IDs from authorized_user_ids file
876     IFS=$'\n'
877     while read line ; do
878         case "$line" in
879             ("#"*)
880                 continue
881                 ;;
882             (" "*|$'\t'*)
883                 if [[ -z ${koptions[${nline}]} ]]; then
884                     koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
885                 else
886                     koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
887                 fi
888                 ;;
889             (*)
890                 ((nline++))
891                 userIDs[${nline}]="$line"
892                 unset koptions[${nline}] || true
893                 ;;
894         esac
895     done
896
897     for i in $(seq 1 $nline); do
898         AUTHORIZED_KEYS_OPTIONS="${koptions[$i]}" FILE_TYPE='authorized_keys' process_keys_for_file "$authorizedKeys" "${userIDs[$i]}" || returnCode="$?"
899     done
900 }
901
902 # takes a gpg key or keys on stdin, and outputs a list of
903 # fingerprints, one per line:
904 list_primary_fingerprints() {
905     local fake=$(msmktempdir)
906     trap "rm -rf $fake" EXIT
907     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
908     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
909         awk -F: '/^fpr:/{ print $10 }'
910     trap - EXIT
911     rm -rf "$fake"
912 }
913
914 # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
915 # key identifier as $1, and outputs the gpg-formatted information for
916 # the requested keys from the material on stdin
917 get_cert_info() {
918     local fake=$(msmktempdir)
919     trap "rm -rf $fake" EXIT
920     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
921     GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
922     trap - EXIT
923     rm -rf "$fake"
924 }
925
926
927 check_cruft_file() {
928     local loc="$1"
929     local version="$2"
930     
931     if [ -e "$loc" ] ; then
932         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
933     fi
934 }
935
936 check_upgrade_dir() {
937     local loc="$1"
938     local version="$2"
939
940     if [ -d "$loc" ] ; then
941         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
942     fi
943 }
944
945 ## look for cruft from old versions of the monkeysphere, and notice if
946 ## upgrades have not been run:
947 report_cruft() {
948     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
949     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
950
951     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
952     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
953
954     local found=
955     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
956         if [ -d "$foo" ] ; then
957             printf "! %s\n" "$foo" | log info
958             found=true
959         fi
960     done
961     if [ "$found" ] ; then
962         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
963     fi
964 }