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