made monkeysphere subkey-to-ssh-agent more user-friendly.
[monkeysphere.git] / src / common
1 # -*-shell-script-*-
2
3 # Shared sh functions for the monkeysphere
4 #
5 # Written by
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 #
8 # Copyright 2008, released under the GPL, version 3 or later
9
10 # all-caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
12
13 ########################################################################
14 ### COMMON VARIABLES
15
16 # managed directories
17 ETC="/etc/monkeysphere"
18 export ETC
19
20 ########################################################################
21 ### UTILITY FUNCTIONS
22
23 # failure function.  exits with code 255, unless specified otherwise.
24 failure() {
25     echo "$1" >&2
26     exit ${2:-'255'}
27 }
28
29 # write output to stderr
30 log() {
31     echo -n "ms: " >&2
32     echo "$@" >&2
33 }
34
35 loge() {
36     echo "$@" >&2
37 }
38
39 # cut out all comments(#) and blank lines from standard input
40 meat() {
41     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
42 }
43
44 # cut a specified line from standard input
45 cutline() {
46     head --line="$1" "$2" | tail -1
47 }
48
49 # check that characters are in a string (in an AND fashion).
50 # used for checking key capability
51 # check_capability capability a [b...]
52 check_capability() {
53     local usage
54     local capcheck
55
56     usage="$1"
57     shift 1
58
59     for capcheck ; do
60         if echo "$usage" | grep -q -v "$capcheck" ; then
61             return 1
62         fi
63     done
64     return 0
65 }
66
67 # hash of a file
68 file_hash() {
69     md5sum "$1" 2> /dev/null
70 }
71
72 # convert escaped characters in pipeline from gpg output back into
73 # original character
74 # FIXME: undo all escape character translation in with-colons gpg
75 # output
76 gpg_unescape() {
77     sed 's/\\x3a/:/g'
78 }
79
80 # convert nasty chars into gpg-friendly form in pipeline
81 # FIXME: escape everything, not just colons!
82 gpg_escape() {
83     sed 's/:/\\x3a/g'
84 }
85
86 # prompt for GPG-formatted expiration, and emit result on stdout
87 get_gpg_expiration() {
88     local keyExpire=
89
90     cat >&2 <<EOF
91 Please specify how long the key should be valid.
92          0 = key does not expire
93       <n>  = key expires in n days
94       <n>w = key expires in n weeks
95       <n>m = key expires in n months
96       <n>y = key expires in n years
97 EOF
98     while [ -z "$keyExpire" ] ; do
99         read -p "Key is valid for? (0) " keyExpire
100         if ! test_gpg_expire ${keyExpire:=0} ; then
101             echo "invalid value" >&2
102             unset keyExpire
103         fi
104     done
105     echo "$keyExpire"
106 }
107
108 passphrase_prompt() {
109     local prompt="$1"
110     local fifo="$2"
111     local PASS
112
113     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
114         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
115     else
116         read -s -p "$prompt" PASS
117         # Uses the builtin echo, so should not put the passphrase into
118         # the process table.  I think. --dkg
119         echo "$PASS" > "$fifo"
120     fi
121 }
122
123 test_gnu_dummy_s2k_extension() {
124
125 # this block contains a demonstration private key that has had the
126 # primary key stripped out using the GNU S2K extension known as
127 # "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
128 # present in cleartext, however.
129
130 # openpgp2ssh will be able to deal with this based on whether the
131 # local copy of GnuTLS contains read_s2k support that can handle it.
132
133 # read up on that here:
134
135 # http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
136
137 echo "
138 -----BEGIN PGP PRIVATE KEY BLOCK-----
139 Version: GnuPG v1.4.9 (GNU/Linux)
140
141 lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
142 xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
143 G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
144 /gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
145 biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
146 AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
147 ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
148 aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
149 g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
150 mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
151 8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
152 +bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
153 RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
154 4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
155 7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
156 ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
157 kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
158 Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
159 SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
160 vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
161 vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
162 vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
163 =CR5w
164 -----END PGP PRIVATE KEY BLOCK-----
165 " | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
166
167 }
168
169 # remove all lines with specified string from specified file
170 remove_line() {
171     local file
172     local string
173
174     file="$1"
175     string="$2"
176
177     if [ -z "$file" -o -z "$string" ] ; then
178         return 1
179     fi
180
181     if [ ! -e "$file" ] ; then
182         return 1
183     fi
184
185     # if the string is in the file...
186     if grep -q -F "$string" "$file" 2> /dev/null ; then
187         # remove the line with the string, and return 0
188         grep -v -F "$string" "$file" | sponge "$file"
189         return 0
190     # otherwise return 1
191     else
192         return 1
193     fi
194 }
195
196 # remove all lines with MonkeySphere strings in file
197 remove_monkeysphere_lines() {
198     local file
199
200     file="$1"
201
202     if [ -z "$file" ] ; then
203         return 1
204     fi
205
206     if [ ! -e "$file" ] ; then
207         return 1
208     fi
209
210     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
211         "$file" | sponge "$file"
212 }
213
214 # translate ssh-style path variables %h and %u
215 translate_ssh_variables() {
216     local uname
217     local home
218
219     uname="$1"
220     path="$2"
221
222     # get the user's home directory
223     userHome=$(getent passwd "$uname" | cut -d: -f6)
224
225     # translate '%u' to user name
226     path=${path/\%u/"$uname"}
227     # translate '%h' to user home directory
228     path=${path/\%h/"$userHome"}
229
230     echo "$path"
231 }
232
233 # test that a string to conforms to GPG's expiration format
234 test_gpg_expire() {
235     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
236 }
237
238 # check that a file is properly owned, and that all it's parent
239 # directories are not group/other writable
240 check_key_file_permissions() {
241     local user
242     local path
243     local access
244     local gAccess
245     local oAccess
246
247     # function to check that an octal corresponds to writability
248     is_write() {
249         [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
250     }
251
252     user="$1"
253     path="$2"
254
255     # return 0 is path does not exist
256     [ -e "$path" ] || return 0
257
258     owner=$(stat --format '%U' "$path")
259     access=$(stat --format '%a' "$path")
260     gAccess=$(echo "$access" | cut -c2)
261     oAccess=$(echo "$access" | cut -c3)
262
263     # check owner
264     if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
265         return 1
266     fi
267
268     # check group/other writability
269     if is_write "$gAccess" || is_write "$oAccess" ; then
270         return 2
271     fi
272
273     if [ "$path" = '/' ] ; then
274         return 0
275     else
276         check_key_file_permissions $(dirname "$path")
277     fi
278 }
279
280 ### CONVERSION UTILITIES
281
282 # output the ssh key for a given key ID
283 gpg2ssh() {
284     local keyID
285     
286     keyID="$1"
287
288     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
289 }
290
291 # output known_hosts line from ssh key
292 ssh2known_hosts() {
293     local host
294     local key
295
296     host="$1"
297     key="$2"
298
299     echo -n "$host "
300     echo -n "$key" | tr -d '\n'
301     echo " MonkeySphere${DATE}"
302 }
303
304 # output authorized_keys line from ssh key
305 ssh2authorized_keys() {
306     local userID
307     local key
308     
309     userID="$1"
310     key="$2"
311
312     echo -n "$key" | tr -d '\n'
313     echo " MonkeySphere${DATE} ${userID}"
314 }
315
316 # convert key from gpg to ssh known_hosts format
317 gpg2known_hosts() {
318     local host
319     local keyID
320
321     host="$1"
322     keyID="$2"
323
324     # NOTE: it seems that ssh-keygen -R removes all comment fields from
325     # all lines in the known_hosts file.  why?
326     # NOTE: just in case, the COMMENT can be matched with the
327     # following regexp:
328     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
329     echo -n "$host "
330     gpg2ssh "$keyID" | tr -d '\n'
331     echo " MonkeySphere${DATE}"
332 }
333
334 # convert key from gpg to ssh authorized_keys format
335 gpg2authorized_keys() {
336     local userID
337     local keyID
338
339     userID="$1"
340     keyID="$2"
341
342     # NOTE: just in case, the COMMENT can be matched with the
343     # following regexp:
344     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
345     gpg2ssh "$keyID" | tr -d '\n'
346     echo " MonkeySphere${DATE} ${userID}"
347 }
348
349 ### GPG UTILITIES
350
351 # retrieve all keys with given user id from keyserver
352 # FIXME: need to figure out how to retrieve all matching keys
353 # (not just first N (5 in this case))
354 gpg_fetch_userid() {
355     local userID
356     local returnCode
357
358     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
359         return 0
360     fi
361
362     userID="$1"
363
364     log -n " checking keyserver $KEYSERVER... "
365     echo 1,2,3,4,5 | \
366         gpg --quiet --batch --with-colons \
367         --command-fd 0 --keyserver "$KEYSERVER" \
368         --search ="$userID" > /dev/null 2>&1
369     returnCode="$?"
370     loge "done."
371
372     # if the user is the monkeysphere user, then update the
373     # monkeysphere user's trustdb
374     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
375         gpg_authentication "--check-trustdb" > /dev/null 2>&1
376     fi
377
378     return "$returnCode"
379 }
380
381 ########################################################################
382 ### PROCESSING FUNCTIONS
383
384 # userid and key policy checking
385 # the following checks policy on the returned keys
386 # - checks that full key has appropriate valididy (u|f)
387 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
388 # - checks that requested user ID has appropriate validity
389 # (see /usr/share/doc/gnupg/DETAILS.gz)
390 # output is one line for every found key, in the following format:
391 #
392 # flag:fingerprint
393 #
394 # "flag" is an acceptability flag, 0 = ok, 1 = bad
395 # "fingerprint" is the fingerprint of the key
396 #
397 # expects global variable: "MODE"
398 process_user_id() {
399     local userID
400     local requiredCapability
401     local requiredPubCapability
402     local gpgOut
403     local type
404     local validity
405     local keyid
406     local uidfpr
407     local usage
408     local keyOK
409     local uidOK
410     local lastKey
411     local lastKeyOK
412     local fingerprint
413
414     userID="$1"
415
416     # set the required key capability based on the mode
417     if [ "$MODE" = 'known_hosts' ] ; then
418         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
419     elif [ "$MODE" = 'authorized_keys' ] ; then
420         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
421     fi
422     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
423
424     # fetch the user ID if necessary/requested
425     gpg_fetch_userid "$userID"
426
427     # output gpg info for (exact) userid and store
428     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
429         --with-fingerprint --with-fingerprint \
430         ="$userID" 2>/dev/null)
431
432     # if the gpg query return code is not 0, return 1
433     if [ "$?" -ne 0 ] ; then
434         log " no primary keys found."
435         return 1
436     fi
437
438     # loop over all lines in the gpg output and process.
439     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
440     while IFS=: read -r type validity keyid uidfpr usage ; do
441         # process based on record type
442         case $type in
443             'pub') # primary keys
444                 # new key, wipe the slate
445                 keyOK=
446                 uidOK=
447                 lastKey=pub
448                 lastKeyOK=
449                 fingerprint=
450
451                 log " primary key found: $keyid"
452
453                 # if overall key is not valid, skip
454                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
455                     log "  - unacceptable primary key validity ($validity)."
456                     continue
457                 fi
458                 # if overall key is disabled, skip
459                 if check_capability "$usage" 'D' ; then
460                     log "  - key disabled."
461                     continue
462                 fi
463                 # if overall key capability is not ok, skip
464                 if ! check_capability "$usage" $requiredPubCapability ; then
465                     log "  - unacceptable primary key capability ($usage)."
466                     continue
467                 fi
468
469                 # mark overall key as ok
470                 keyOK=true
471
472                 # mark primary key as ok if capability is ok
473                 if check_capability "$usage" $requiredCapability ; then
474                     lastKeyOK=true
475                 fi
476                 ;;
477             'uid') # user ids
478                 if [ "$lastKey" != pub ] ; then
479                     log " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
480                     continue
481                 fi
482                 # if an acceptable user ID was already found, skip
483                 if [ "$uidOK" = 'true' ] ; then
484                     continue
485                 fi
486                 # if the user ID does matches...
487                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
488                     # and the user ID validity is ok
489                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
490                         # mark user ID acceptable
491                         uidOK=true
492                     fi
493                 else
494                     continue
495                 fi
496
497                 # output a line for the primary key
498                 # 0 = ok, 1 = bad
499                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
500                     log "  * acceptable primary key."
501                     if [ -z "$sshKey" ] ; then
502                         log "    ! primary key could not be translated (not RSA or DSA?)."
503                     else
504                         echo "0:${sshKey}"
505                     fi
506                 else
507                     log "  - unacceptable primary key."
508                     if [ -z "$sshKey" ] ; then
509                         log "   ! primary key could not be translated (not RSA or DSA?)."
510                     else
511                         echo "1:${sshKey}"
512                     fi
513                 fi
514                 ;;
515             'sub') # sub keys
516                 # unset acceptability of last key
517                 lastKey=sub
518                 lastKeyOK=
519                 fingerprint=
520                 
521                 # don't bother with sub keys if the primary key is not valid
522                 if [ "$keyOK" != true ] ; then
523                     continue
524                 fi
525
526                 # don't bother with sub keys if no user ID is acceptable:
527                 if [ "$uidOK" != true ] ; then
528                     continue
529                 fi
530                 
531                 # if sub key validity is not ok, skip
532                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
533                     continue
534                 fi
535                 # if sub key capability is not ok, skip
536                 if ! check_capability "$usage" $requiredCapability ; then
537                     continue
538                 fi
539
540                 # mark sub key as ok
541                 lastKeyOK=true
542                 ;;
543             'fpr') # key fingerprint
544                 fingerprint="$uidfpr"
545
546                 sshKey=$(gpg2ssh "$fingerprint")
547
548                 # if the last key was the pub key, skip
549                 if [ "$lastKey" = pub ] ; then
550                     continue
551                 fi
552
553                 # output a line for the sub key
554                 # 0 = ok, 1 = bad
555                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
556                     log "  * acceptable sub key."
557                     if [ -z "$sshKey" ] ; then
558                         log "    ! sub key could not be translated (not RSA or DSA?)."
559                     else
560                         echo "0:${sshKey}"
561                     fi
562                 else
563                     log "  - unacceptable sub key."
564                     if [ -z "$sshKey" ] ; then
565                         log "    ! sub key could not be translated (not RSA or DSA?)."
566                     else
567                         echo "1:${sshKey}"
568                     fi
569                 fi
570                 ;;
571         esac
572     done | sort -t: -k1 -n -r
573     # NOTE: this last sort is important so that the "good" keys (key
574     # flag '0') come last.  This is so that they take precedence when
575     # being processed in the key files over "bad" keys (key flag '1')
576 }
577
578 # process a single host in the known_host file
579 process_host_known_hosts() {
580     local host
581     local userID
582     local nKeys
583     local nKeysOK
584     local ok
585     local sshKey
586     local tmpfile
587
588     host="$1"
589     userID="ssh://${host}"
590
591     log "processing: $host"
592
593     nKeys=0
594     nKeysOK=0
595
596     IFS=$'\n'
597     for line in $(process_user_id "${userID}") ; do
598         # note that key was found
599         nKeys=$((nKeys+1))
600
601         ok=$(echo "$line" | cut -d: -f1)
602         sshKey=$(echo "$line" | cut -d: -f2)
603
604         if [ -z "$sshKey" ] ; then
605             continue
606         fi
607
608         # remove the old host key line, and note if removed
609         remove_line "$KNOWN_HOSTS" "$sshKey"
610
611         # if key OK, add new host line
612         if [ "$ok" -eq '0' ] ; then
613             # note that key was found ok
614             nKeysOK=$((nKeysOK+1))
615
616             # hash if specified
617             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
618                 # FIXME: this is really hackish cause ssh-keygen won't
619                 # hash from stdin to stdout
620                 tmpfile=$(mktemp)
621                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
622                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
623                 cat "$tmpfile" >> "$KNOWN_HOSTS"
624                 rm -f "$tmpfile" "${tmpfile}.old"
625             else
626                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
627             fi
628         fi
629     done
630
631     # if at least one key was found...
632     if [ "$nKeys" -gt 0 ] ; then
633         # if ok keys were found, return 0
634         if [ "$nKeysOK" -gt 0 ] ; then
635             return 0
636         # else return 2
637         else
638             return 2
639         fi
640     # if no keys were found, return 1
641     else
642         return 1
643     fi
644 }
645
646 # update the known_hosts file for a set of hosts listed on command
647 # line
648 update_known_hosts() {
649     local nHosts
650     local nHostsOK
651     local nHostsBAD
652     local fileCheck
653     local host
654
655     # the number of hosts specified on command line
656     nHosts="$#"
657
658     nHostsOK=0
659     nHostsBAD=0
660
661     # set the trap to remove any lockfiles on exit
662     trap "lockfile-remove $KNOWN_HOSTS" EXIT
663
664     # create a lockfile on known_hosts
665     lockfile-create "$KNOWN_HOSTS"
666
667     # note pre update file checksum
668     fileCheck="$(file_hash "$KNOWN_HOSTS")"
669
670     for host ; do
671         # process the host
672         process_host_known_hosts "$host"
673         # note the result
674         case "$?" in
675             0)
676                 nHostsOK=$((nHostsOK+1))
677                 ;;
678             2)
679                 nHostsBAD=$((nHostsBAD+1))
680                 ;;
681         esac
682
683         # touch the lockfile, for good measure.
684         lockfile-touch --oneshot "$KNOWN_HOSTS"
685     done
686
687     # remove the lockfile
688     lockfile-remove "$KNOWN_HOSTS"
689
690     # note if the known_hosts file was updated
691     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
692         log "known_hosts file updated."
693     fi
694
695     # if an acceptable host was found, return 0
696     if [ "$nHostsOK" -gt 0 ] ; then
697         return 0
698     # else if no ok hosts were found...
699     else
700         # if no bad host were found then no hosts were found at all,
701         # and return 1
702         if [ "$nHostsBAD" -eq 0 ] ; then
703             return 1
704         # else if at least one bad host was found, return 2
705         else
706             return 2
707         fi
708     fi
709 }
710
711 # process hosts from a known_hosts file
712 process_known_hosts() {
713     local hosts
714
715     log "processing known_hosts file..."
716
717     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
718
719     if [ -z "$hosts" ] ; then
720         log "no hosts to process."
721         return
722     fi
723
724     # take all the hosts from the known_hosts file (first
725     # field), grep out all the hashed hosts (lines starting
726     # with '|')...
727     update_known_hosts $hosts
728 }
729
730 # process uids for the authorized_keys file
731 process_uid_authorized_keys() {
732     local userID
733     local nKeys
734     local nKeysOK
735     local ok
736     local sshKey
737
738     userID="$1"
739
740     log "processing: $userID"
741
742     nKeys=0
743     nKeysOK=0
744
745     IFS=$'\n'
746     for line in $(process_user_id "$userID") ; do
747         # note that key was found
748         nKeys=$((nKeys+1))
749
750         ok=$(echo "$line" | cut -d: -f1)
751         sshKey=$(echo "$line" | cut -d: -f2)
752
753         if [ -z "$sshKey" ] ; then
754             continue
755         fi
756
757         # remove the old host key line
758         remove_line "$AUTHORIZED_KEYS" "$sshKey"
759
760         # if key OK, add new host line
761         if [ "$ok" -eq '0' ] ; then
762             # note that key was found ok
763             nKeysOK=$((nKeysOK+1))
764
765             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
766         fi
767     done
768
769     # if at least one key was found...
770     if [ "$nKeys" -gt 0 ] ; then
771         # if ok keys were found, return 0
772         if [ "$nKeysOK" -gt 0 ] ; then
773             return 0
774         # else return 2
775         else
776             return 2
777         fi
778     # if no keys were found, return 1
779     else
780         return 1
781     fi
782 }
783
784 # update the authorized_keys files from a list of user IDs on command
785 # line
786 update_authorized_keys() {
787     local userID
788     local nIDs
789     local nIDsOK
790     local nIDsBAD
791     local fileCheck
792
793     # the number of ids specified on command line
794     nIDs="$#"
795
796     nIDsOK=0
797     nIDsBAD=0
798
799     # set the trap to remove any lockfiles on exit
800     trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
801
802     # create a lockfile on authorized_keys
803     lockfile-create "$AUTHORIZED_KEYS"
804
805     # note pre update file checksum
806     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
807
808     # remove any monkeysphere lines from authorized_keys file
809     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
810
811     for userID ; do
812         # process the user ID, change return code if key not found for
813         # user ID
814         process_uid_authorized_keys "$userID"
815
816         # note the result
817         case "$?" in
818             0)
819                 nIDsOK=$((nIDsOK+1))
820                 ;;
821             2)
822                 nIDsBAD=$((nIDsBAD+1))
823                 ;;
824         esac
825
826         # touch the lockfile, for good measure.
827         lockfile-touch --oneshot "$AUTHORIZED_KEYS"
828     done
829
830     # remove the lockfile
831     lockfile-remove "$AUTHORIZED_KEYS"
832
833     # note if the authorized_keys file was updated
834     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
835         log "authorized_keys file updated."
836     fi
837
838     # if an acceptable id was found, return 0
839     if [ "$nIDsOK" -gt 0 ] ; then
840         return 0
841     # else if no ok ids were found...
842     else
843         # if no bad ids were found then no ids were found at all, and
844         # return 1
845         if [ "$nIDsBAD" -eq 0 ] ; then
846             return 1
847         # else if at least one bad id was found, return 2
848         else
849             return 2
850         fi
851     fi
852 }
853
854 # process an authorized_user_ids file for authorized_keys
855 process_authorized_user_ids() {
856     local line
857     local nline
858     local userIDs
859
860     authorizedUserIDs="$1"
861
862     log "processing authorized_user_ids file..."
863
864     if ! meat "$authorizedUserIDs" > /dev/null ; then
865         log "no user IDs to process."
866         return
867     fi
868
869     nline=0
870
871     # extract user IDs from authorized_user_ids file
872     IFS=$'\n'
873     for line in $(meat "$authorizedUserIDs") ; do
874         userIDs["$nline"]="$line"
875         nline=$((nline+1))
876     done
877
878     update_authorized_keys "${userIDs[@]}"
879 }