summaryrefslogtreecommitdiff
path: root/src/common
blob: 6a620807c3535450d5f539c9575c52376b442efa (plain)
  1. # -*-shell-script-*-
  2. # Shared sh functions for the monkeysphere
  3. #
  4. # Written by
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. #
  7. # Copyright 2008, released under the GPL, version 3 or later
  8. # all-caps variables are meant to be user supplied (ie. from config
  9. # file) and are considered global
  10. ########################################################################
  11. ### COMMON VARIABLES
  12. # managed directories
  13. ETC="/etc/monkeysphere"
  14. export ETC
  15. ########################################################################
  16. ### UTILITY FUNCTIONS
  17. # failure function. exits with code 255, unless specified otherwise.
  18. failure() {
  19. echo "$1" >&2
  20. exit ${2:-'255'}
  21. }
  22. # write output to stderr
  23. log() {
  24. echo -n "ms: " >&2
  25. echo "$@" >&2
  26. }
  27. loge() {
  28. echo "$@" >&2
  29. }
  30. # cut out all comments(#) and blank lines from standard input
  31. meat() {
  32. grep -v -e "^[[:space:]]*#" -e '^$' "$1"
  33. }
  34. # cut a specified line from standard input
  35. cutline() {
  36. head --line="$1" "$2" | tail -1
  37. }
  38. # check that characters are in a string (in an AND fashion).
  39. # used for checking key capability
  40. # check_capability capability a [b...]
  41. check_capability() {
  42. local usage
  43. local capcheck
  44. usage="$1"
  45. shift 1
  46. for capcheck ; do
  47. if echo "$usage" | grep -q -v "$capcheck" ; then
  48. return 1
  49. fi
  50. done
  51. return 0
  52. }
  53. # hash of a file
  54. file_hash() {
  55. md5sum "$1" 2> /dev/null
  56. }
  57. # convert escaped characters in pipeline from gpg output back into
  58. # original character
  59. # FIXME: undo all escape character translation in with-colons gpg
  60. # output
  61. gpg_unescape() {
  62. sed 's/\\x3a/:/g'
  63. }
  64. # convert nasty chars into gpg-friendly form in pipeline
  65. # FIXME: escape everything, not just colons!
  66. gpg_escape() {
  67. sed 's/:/\\x3a/g'
  68. }
  69. # prompt for GPG-formatted expiration, and emit result on stdout
  70. get_gpg_expiration() {
  71. local keyExpire=
  72. cat >&2 <<EOF
  73. Please specify how long the key should be valid.
  74. 0 = key does not expire
  75. <n> = key expires in n days
  76. <n>w = key expires in n weeks
  77. <n>m = key expires in n months
  78. <n>y = key expires in n years
  79. EOF
  80. while [ -z "$keyExpire" ] ; do
  81. read -p "Key is valid for? (0) " keyExpire
  82. if ! test_gpg_expire ${keyExpire:=0} ; then
  83. echo "invalid value" >&2
  84. unset keyExpire
  85. fi
  86. done
  87. echo "$keyExpire"
  88. }
  89. passphrase_prompt() {
  90. local prompt="$1"
  91. local fifo="$2"
  92. local PASS
  93. if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
  94. "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
  95. else
  96. read -s -p "$prompt" PASS
  97. # Uses the builtin echo, so should not put the passphrase into
  98. # the process table. I think. --dkg
  99. echo "$PASS" > "$fifo"
  100. fi
  101. }
  102. # remove all lines with specified string from specified file
  103. remove_line() {
  104. local file
  105. local string
  106. file="$1"
  107. string="$2"
  108. if [ -z "$file" -o -z "$string" ] ; then
  109. return 1
  110. fi
  111. if [ ! -e "$file" ] ; then
  112. return 1
  113. fi
  114. # if the string is in the file...
  115. if grep -q -F "$string" "$file" 2> /dev/null ; then
  116. # remove the line with the string, and return 0
  117. grep -v -F "$string" "$file" | sponge "$file"
  118. return 0
  119. # otherwise return 1
  120. else
  121. return 1
  122. fi
  123. }
  124. # remove all lines with MonkeySphere strings in file
  125. remove_monkeysphere_lines() {
  126. local file
  127. file="$1"
  128. if [ -z "$file" ] ; then
  129. return 1
  130. fi
  131. if [ ! -e "$file" ] ; then
  132. return 1
  133. fi
  134. egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
  135. "$file" | sponge "$file"
  136. }
  137. # translate ssh-style path variables %h and %u
  138. translate_ssh_variables() {
  139. local uname
  140. local home
  141. uname="$1"
  142. path="$2"
  143. # get the user's home directory
  144. userHome=$(getent passwd "$uname" | cut -d: -f6)
  145. # translate '%u' to user name
  146. path=${path/\%u/"$uname"}
  147. # translate '%h' to user home directory
  148. path=${path/\%h/"$userHome"}
  149. echo "$path"
  150. }
  151. # test that a string to conforms to GPG's expiration format
  152. test_gpg_expire() {
  153. echo "$1" | egrep -q "^[0-9]+[mwy]?$"
  154. }
  155. # check that a file is properly owned, and that all it's parent
  156. # directories are not group/other writable
  157. check_key_file_permissions() {
  158. local user
  159. local path
  160. local access
  161. local gAccess
  162. local oAccess
  163. # function to check that an octal corresponds to writability
  164. is_write() {
  165. [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
  166. }
  167. user="$1"
  168. path="$2"
  169. # return 0 is path does not exist
  170. [ -e "$path" ] || return 0
  171. owner=$(stat --format '%U' "$path")
  172. access=$(stat --format '%a' "$path")
  173. gAccess=$(echo "$access" | cut -c2)
  174. oAccess=$(echo "$access" | cut -c3)
  175. # check owner
  176. if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
  177. return 1
  178. fi
  179. # check group/other writability
  180. if is_write "$gAccess" || is_write "$oAccess" ; then
  181. return 2
  182. fi
  183. if [ "$path" = '/' ] ; then
  184. return 0
  185. else
  186. check_key_file_permissions $(dirname "$path")
  187. fi
  188. }
  189. ### CONVERSION UTILITIES
  190. # output the ssh key for a given key ID
  191. gpg2ssh() {
  192. local keyID
  193. keyID="$1"
  194. gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
  195. }
  196. # output known_hosts line from ssh key
  197. ssh2known_hosts() {
  198. local host
  199. local key
  200. host="$1"
  201. key="$2"
  202. echo -n "$host "
  203. echo -n "$key" | tr -d '\n'
  204. echo " MonkeySphere${DATE}"
  205. }
  206. # output authorized_keys line from ssh key
  207. ssh2authorized_keys() {
  208. local userID
  209. local key
  210. userID="$1"
  211. key="$2"
  212. echo -n "$key" | tr -d '\n'
  213. echo " MonkeySphere${DATE} ${userID}"
  214. }
  215. # convert key from gpg to ssh known_hosts format
  216. gpg2known_hosts() {
  217. local host
  218. local keyID
  219. host="$1"
  220. keyID="$2"
  221. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  222. # all lines in the known_hosts file. why?
  223. # NOTE: just in case, the COMMENT can be matched with the
  224. # following regexp:
  225. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  226. echo -n "$host "
  227. gpg2ssh "$keyID" | tr -d '\n'
  228. echo " MonkeySphere${DATE}"
  229. }
  230. # convert key from gpg to ssh authorized_keys format
  231. gpg2authorized_keys() {
  232. local userID
  233. local keyID
  234. userID="$1"
  235. keyID="$2"
  236. # NOTE: just in case, the COMMENT can be matched with the
  237. # following regexp:
  238. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  239. gpg2ssh "$keyID" | tr -d '\n'
  240. echo " MonkeySphere${DATE} ${userID}"
  241. }
  242. ### GPG UTILITIES
  243. # retrieve all keys with given user id from keyserver
  244. # FIXME: need to figure out how to retrieve all matching keys
  245. # (not just first N (5 in this case))
  246. gpg_fetch_userid() {
  247. local userID
  248. local returnCode
  249. if [ "$CHECK_KEYSERVER" != 'true' ] ; then
  250. return 0
  251. fi
  252. userID="$1"
  253. log -n " checking keyserver $KEYSERVER... "
  254. echo 1,2,3,4,5 | \
  255. gpg --quiet --batch --with-colons \
  256. --command-fd 0 --keyserver "$KEYSERVER" \
  257. --search ="$userID" > /dev/null 2>&1
  258. returnCode="$?"
  259. loge "done."
  260. # if the user is the monkeysphere user, then update the
  261. # monkeysphere user's trustdb
  262. if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
  263. gpg_authentication "--check-trustdb" > /dev/null 2>&1
  264. fi
  265. return "$returnCode"
  266. }
  267. ########################################################################
  268. ### PROCESSING FUNCTIONS
  269. # userid and key policy checking
  270. # the following checks policy on the returned keys
  271. # - checks that full key has appropriate valididy (u|f)
  272. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  273. # - checks that requested user ID has appropriate validity
  274. # (see /usr/share/doc/gnupg/DETAILS.gz)
  275. # output is one line for every found key, in the following format:
  276. #
  277. # flag:fingerprint
  278. #
  279. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  280. # "fingerprint" is the fingerprint of the key
  281. #
  282. # expects global variable: "MODE"
  283. process_user_id() {
  284. local userID
  285. local requiredCapability
  286. local requiredPubCapability
  287. local gpgOut
  288. local type
  289. local validity
  290. local keyid
  291. local uidfpr
  292. local usage
  293. local keyOK
  294. local uidOK
  295. local lastKey
  296. local lastKeyOK
  297. local fingerprint
  298. userID="$1"
  299. # set the required key capability based on the mode
  300. if [ "$MODE" = 'known_hosts' ] ; then
  301. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  302. elif [ "$MODE" = 'authorized_keys' ] ; then
  303. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  304. fi
  305. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  306. # fetch the user ID if necessary/requested
  307. gpg_fetch_userid "$userID"
  308. # output gpg info for (exact) userid and store
  309. gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
  310. --with-fingerprint --with-fingerprint \
  311. ="$userID" 2>/dev/null)
  312. # if the gpg query return code is not 0, return 1
  313. if [ "$?" -ne 0 ] ; then
  314. log " no primary keys found."
  315. return 1
  316. fi
  317. # loop over all lines in the gpg output and process.
  318. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  319. while IFS=: read -r type validity keyid uidfpr usage ; do
  320. # process based on record type
  321. case $type in
  322. 'pub') # primary keys
  323. # new key, wipe the slate
  324. keyOK=
  325. uidOK=
  326. lastKey=pub
  327. lastKeyOK=
  328. fingerprint=
  329. log " primary key found: $keyid"
  330. # if overall key is not valid, skip
  331. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  332. log " - unacceptable primary key validity ($validity)."
  333. continue
  334. fi
  335. # if overall key is disabled, skip
  336. if check_capability "$usage" 'D' ; then
  337. log " - key disabled."
  338. continue
  339. fi
  340. # if overall key capability is not ok, skip
  341. if ! check_capability "$usage" $requiredPubCapability ; then
  342. log " - unacceptable primary key capability ($usage)."
  343. continue
  344. fi
  345. # mark overall key as ok
  346. keyOK=true
  347. # mark primary key as ok if capability is ok
  348. if check_capability "$usage" $requiredCapability ; then
  349. lastKeyOK=true
  350. fi
  351. ;;
  352. 'uid') # user ids
  353. if [ "$lastKey" != pub ] ; then
  354. log " - got a user ID after a sub key?! user IDs should only follow primary keys!"
  355. continue
  356. fi
  357. # if an acceptable user ID was already found, skip
  358. if [ "$uidOK" = 'true' ] ; then
  359. continue
  360. fi
  361. # if the user ID does matches...
  362. if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
  363. # and the user ID validity is ok
  364. if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
  365. # mark user ID acceptable
  366. uidOK=true
  367. fi
  368. else
  369. continue
  370. fi
  371. # output a line for the primary key
  372. # 0 = ok, 1 = bad
  373. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  374. log " * acceptable primary key."
  375. if [ -z "$sshKey" ] ; then
  376. log " ! primary key could not be translated (not RSA or DSA?)."
  377. else
  378. echo "0:${sshKey}"
  379. fi
  380. else
  381. log " - unacceptable primary key."
  382. if [ -z "$sshKey" ] ; then
  383. log " ! primary key could not be translated (not RSA or DSA?)."
  384. else
  385. echo "1:${sshKey}"
  386. fi
  387. fi
  388. ;;
  389. 'sub') # sub keys
  390. # unset acceptability of last key
  391. lastKey=sub
  392. lastKeyOK=
  393. fingerprint=
  394. # don't bother with sub keys if the primary key is not valid
  395. if [ "$keyOK" != true ] ; then
  396. continue
  397. fi
  398. # don't bother with sub keys if no user ID is acceptable:
  399. if [ "$uidOK" != true ] ; then
  400. continue
  401. fi
  402. # if sub key validity is not ok, skip
  403. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  404. continue
  405. fi
  406. # if sub key capability is not ok, skip
  407. if ! check_capability "$usage" $requiredCapability ; then
  408. continue
  409. fi
  410. # mark sub key as ok
  411. lastKeyOK=true
  412. ;;
  413. 'fpr') # key fingerprint
  414. fingerprint="$uidfpr"
  415. sshKey=$(gpg2ssh "$fingerprint")
  416. # if the last key was the pub key, skip
  417. if [ "$lastKey" = pub ] ; then
  418. continue
  419. fi
  420. # output a line for the sub key
  421. # 0 = ok, 1 = bad
  422. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  423. log " * acceptable sub key."
  424. if [ -z "$sshKey" ] ; then
  425. log " ! sub key could not be translated (not RSA or DSA?)."
  426. else
  427. echo "0:${sshKey}"
  428. fi
  429. else
  430. log " - unacceptable sub key."
  431. if [ -z "$sshKey" ] ; then
  432. log " ! sub key could not be translated (not RSA or DSA?)."
  433. else
  434. echo "1:${sshKey}"
  435. fi
  436. fi
  437. ;;
  438. esac
  439. done | sort -t: -k1 -n -r
  440. # NOTE: this last sort is important so that the "good" keys (key
  441. # flag '0') come last. This is so that they take precedence when
  442. # being processed in the key files over "bad" keys (key flag '1')
  443. }
  444. # process a single host in the known_host file
  445. process_host_known_hosts() {
  446. local host
  447. local userID
  448. local nKeys
  449. local nKeysOK
  450. local ok
  451. local sshKey
  452. local tmpfile
  453. host="$1"
  454. userID="ssh://${host}"
  455. log "processing: $host"
  456. nKeys=0
  457. nKeysOK=0
  458. IFS=$'\n'
  459. for line in $(process_user_id "${userID}") ; do
  460. # note that key was found
  461. nKeys=$((nKeys+1))
  462. ok=$(echo "$line" | cut -d: -f1)
  463. sshKey=$(echo "$line" | cut -d: -f2)
  464. if [ -z "$sshKey" ] ; then
  465. continue
  466. fi
  467. # remove the old host key line, and note if removed
  468. remove_line "$KNOWN_HOSTS" "$sshKey"
  469. # if key OK, add new host line
  470. if [ "$ok" -eq '0' ] ; then
  471. # note that key was found ok
  472. nKeysOK=$((nKeysOK+1))
  473. # hash if specified
  474. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  475. # FIXME: this is really hackish cause ssh-keygen won't
  476. # hash from stdin to stdout
  477. tmpfile=$(mktemp)
  478. ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
  479. ssh-keygen -H -f "$tmpfile" 2> /dev/null
  480. cat "$tmpfile" >> "$KNOWN_HOSTS"
  481. rm -f "$tmpfile" "${tmpfile}.old"
  482. else
  483. ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
  484. fi
  485. fi
  486. done
  487. # if at least one key was found...
  488. if [ "$nKeys" -gt 0 ] ; then
  489. # if ok keys were found, return 0
  490. if [ "$nKeysOK" -gt 0 ] ; then
  491. return 0
  492. # else return 2
  493. else
  494. return 2
  495. fi
  496. # if no keys were found, return 1
  497. else
  498. return 1
  499. fi
  500. }
  501. # update the known_hosts file for a set of hosts listed on command
  502. # line
  503. update_known_hosts() {
  504. local nHosts
  505. local nHostsOK
  506. local nHostsBAD
  507. local fileCheck
  508. local host
  509. # the number of hosts specified on command line
  510. nHosts="$#"
  511. nHostsOK=0
  512. nHostsBAD=0
  513. # set the trap to remove any lockfiles on exit
  514. trap "lockfile-remove $KNOWN_HOSTS" EXIT
  515. # create a lockfile on known_hosts
  516. lockfile-create "$KNOWN_HOSTS"
  517. # note pre update file checksum
  518. fileCheck="$(file_hash "$KNOWN_HOSTS")"
  519. for host ; do
  520. # process the host
  521. process_host_known_hosts "$host"
  522. # note the result
  523. case "$?" in
  524. 0)
  525. nHostsOK=$((nHostsOK+1))
  526. ;;
  527. 2)
  528. nHostsBAD=$((nHostsBAD+1))
  529. ;;
  530. esac
  531. # touch the lockfile, for good measure.
  532. lockfile-touch --oneshot "$KNOWN_HOSTS"
  533. done
  534. # remove the lockfile
  535. lockfile-remove "$KNOWN_HOSTS"
  536. # note if the known_hosts file was updated
  537. if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
  538. log "known_hosts file updated."
  539. fi
  540. # if an acceptable host was found, return 0
  541. if [ "$nHostsOK" -gt 0 ] ; then
  542. return 0
  543. # else if no ok hosts were found...
  544. else
  545. # if no bad host were found then no hosts were found at all,
  546. # and return 1
  547. if [ "$nHostsBAD" -eq 0 ] ; then
  548. return 1
  549. # else if at least one bad host was found, return 2
  550. else
  551. return 2
  552. fi
  553. fi
  554. }
  555. # process hosts from a known_hosts file
  556. process_known_hosts() {
  557. local hosts
  558. log "processing known_hosts file..."
  559. hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
  560. if [ -z "$hosts" ] ; then
  561. log "no hosts to process."
  562. return
  563. fi
  564. # take all the hosts from the known_hosts file (first
  565. # field), grep out all the hashed hosts (lines starting
  566. # with '|')...
  567. update_known_hosts $hosts
  568. }
  569. # process uids for the authorized_keys file
  570. process_uid_authorized_keys() {
  571. local userID
  572. local nKeys
  573. local nKeysOK
  574. local ok
  575. local sshKey
  576. userID="$1"
  577. log "processing: $userID"
  578. nKeys=0
  579. nKeysOK=0
  580. IFS=$'\n'
  581. for line in $(process_user_id "$userID") ; do
  582. # note that key was found
  583. nKeys=$((nKeys+1))
  584. ok=$(echo "$line" | cut -d: -f1)
  585. sshKey=$(echo "$line" | cut -d: -f2)
  586. if [ -z "$sshKey" ] ; then
  587. continue
  588. fi
  589. # remove the old host key line
  590. remove_line "$AUTHORIZED_KEYS" "$sshKey"
  591. # if key OK, add new host line
  592. if [ "$ok" -eq '0' ] ; then
  593. # note that key was found ok
  594. nKeysOK=$((nKeysOK+1))
  595. ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
  596. fi
  597. done
  598. # if at least one key was found...
  599. if [ "$nKeys" -gt 0 ] ; then
  600. # if ok keys were found, return 0
  601. if [ "$nKeysOK" -gt 0 ] ; then
  602. return 0
  603. # else return 2
  604. else
  605. return 2
  606. fi
  607. # if no keys were found, return 1
  608. else
  609. return 1
  610. fi
  611. }
  612. # update the authorized_keys files from a list of user IDs on command
  613. # line
  614. update_authorized_keys() {
  615. local userID
  616. local nIDs
  617. local nIDsOK
  618. local nIDsBAD
  619. local fileCheck
  620. # the number of ids specified on command line
  621. nIDs="$#"
  622. nIDsOK=0
  623. nIDsBAD=0
  624. # set the trap to remove any lockfiles on exit
  625. trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
  626. # create a lockfile on authorized_keys
  627. lockfile-create "$AUTHORIZED_KEYS"
  628. # note pre update file checksum
  629. fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
  630. # remove any monkeysphere lines from authorized_keys file
  631. remove_monkeysphere_lines "$AUTHORIZED_KEYS"
  632. for userID ; do
  633. # process the user ID, change return code if key not found for
  634. # user ID
  635. process_uid_authorized_keys "$userID"
  636. # note the result
  637. case "$?" in
  638. 0)
  639. nIDsOK=$((nIDsOK+1))
  640. ;;
  641. 2)
  642. nIDsBAD=$((nIDsBAD+1))
  643. ;;
  644. esac
  645. # touch the lockfile, for good measure.
  646. lockfile-touch --oneshot "$AUTHORIZED_KEYS"
  647. done
  648. # remove the lockfile
  649. lockfile-remove "$AUTHORIZED_KEYS"
  650. # note if the authorized_keys file was updated
  651. if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
  652. log "authorized_keys file updated."
  653. fi
  654. # if an acceptable id was found, return 0
  655. if [ "$nIDsOK" -gt 0 ] ; then
  656. return 0
  657. # else if no ok ids were found...
  658. else
  659. # if no bad ids were found then no ids were found at all, and
  660. # return 1
  661. if [ "$nIDsBAD" -eq 0 ] ; then
  662. return 1
  663. # else if at least one bad id was found, return 2
  664. else
  665. return 2
  666. fi
  667. fi
  668. }
  669. # process an authorized_user_ids file for authorized_keys
  670. process_authorized_user_ids() {
  671. local line
  672. local nline
  673. local userIDs
  674. authorizedUserIDs="$1"
  675. log "processing authorized_user_ids file..."
  676. if ! meat "$authorizedUserIDs" > /dev/null ; then
  677. log "no user IDs to process."
  678. return
  679. fi
  680. nline=0
  681. # extract user IDs from authorized_user_ids file
  682. IFS=$'\n'
  683. for line in $(meat "$authorizedUserIDs") ; do
  684. userIDs["$nline"]="$line"
  685. nline=$((nline+1))
  686. done
  687. update_authorized_keys "${userIDs[@]}"
  688. }