#!/bin/env bash # Copyright (C) 2016 Thomas Zink # Copyright (C) 2013 Richard Monk # Originally from # https://post-office.corp.redhat.com/mailman/private/memo-list\ # /2013-February/msg00116.html # Copyright (C) 2013-2014 Matěj Cepl # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall # be included in all copies or substantial portions of the # Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR # A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. function digitalRoot() { # Requires an input a string # JavaScript equivalent (for comparison): # # function digitalRoot(inNo) { # var cipher_sum = Array.reduce(inNo, function(prev, cur) { # return prev + cur.charCodeAt(0); # }, 0); # return cipher_sum % 10; # } local sum=0 local n=$1 local i=0 while [ $i -lt ${#n} ] ; do ord=$(printf '%s' "${n:i:1}" | od -An -td | tr -d '[:blank:]') sum=$(( sum + ord )) # calculate sum of digits i=$(( i + 1 )) done echo -n $(( sum % 10 )) } function usage() { echo "usage: $(basename "$0") [tokentype] [secret]" echo echo "Create OTP configurations for linOPT, mod_authn_otp, and Yubikey." echo "The configurations and qrcode are output to stdout." echo "If [secret] is provided, it is used as input for the configurations," echo "otherwise a random secret is generated." echo "If a Yubikey is inserted prior to running, this program tries to write a" echo "hotp configuration directly to the Yubikey." echo "" echo "username: [issuer:]username[@[domain]]" echo " using @ without domain uses" echo " host's FQDN as domain" echo "" echo "Options:" echo " tokentype: hotp | totp, default: totp" echo " secret: a hex encoded secret key, random if omitted" echo "" } # globals # set output type of qrcode for displaying on shell, use environment-variable if set # [ ANSI ANSI256 ASCII ASCIIi UTF8 ANSIUTF8 ] if [ -z "${QRTYPE}" ]; then QRTYPE=ANSI fi # set commands _qrencode="$(command -v qrencode 2>/dev/null)" _ykinfo="$(command -v ykinfo 2>/dev/null)" _ykp="$(command -v ykpersonalize 2>/dev/null)" _ykman="$(command -v ykman 2>/dev/null)" # check and parse parameters # check if "$1" is defined or "help" is requested [ -z "$1" ] || [[ "$1" =~ (-h|--help) ]] && { usage; exit 0; } # $1 username name="$1" issuer='' domain='' # parse variable "name", set issuer if supplied if [[ "${name}" =~ ^(.*):(.*)$ ]]; then issuer=${BASH_REMATCH[1]} name=${BASH_REMATCH[2]} fi # parse variable "name", set domain if supplied if [[ "${name}" =~ ^(.*)@(.*)$ ]]; then name=${BASH_REMATCH[1]} domain=${BASH_REMATCH[2]} fi # construct "user" if [ -z "${domain}" ]; then user="${name}" else user="${name}@${domain}" fi # $2 get token type type="$2" case "$type" in totp) tokentype="totp" algorithm="HOTP/T30" ;; hotp) tokentype="hotp" algorithm="HOTP" ;; *) echo "INFO: Bad or no token type specified, using TOTP." >&2 tokentype="totp" algorithm="HOTP/T30" ;; esac # $3 check for hexkey from user-input or generate a new one hexkey="$3" if [ -z "${hexkey}" ]; then echo "INFO: No secret provided, generating random secret." >&2 # create url-safe base32 with a length of 40 characters hexkey="$(head -c 1024 /dev/urandom | tr -d '\0')" hexkey="$(printf '%s' "${hexkey}" | base32 | tr -dc '[:alpha:][digit]' | od -tx1 -An | tr -d '[:space:]' | head -c 40 | tr -d '\n')" fi # check hexkey if [[ ! "${hexkey}" =~ ^[A-Fa-f0-9]*$ ]]; then echo "ERROR: Invalid secret, must be hex encoded." >&2 exit 1 fi # get base32 of hexkey b32key="$(printf '%s' "${hexkey}" | xxd -r -ps | base32)" # calculate checksum using "repeated digital sum" b32checksum=$(digitalRoot "${b32key}") if [ -z "${b32checksum}" ]; then echo "ERROR: Invalid secret, checksum cannot be calculated" >&2 exit 1 fi # construct "uri" if [ -z "${issuer}" ]; then uri="otpauth://${tokentype}/${user}?secret=${b32key}" else uri="otpauth://${tokentype}/${issuer}:${user}?secret=${b32key}&issuer=${issuer}" fi # display keys and uri echo echo "Key in Hex: ${hexkey}" echo "Key in b32: ${b32key} (checksum: ${b32checksum})" echo echo "URI: ${uri}" # display QR Code on shell, if qrencode exists [ -x "$_qrencode" ] && $_qrencode --type ${QRTYPE} "${uri}"; echo # setup Yubikey, if tokentype is HOTP if [ "${tokentype}" == "hotp" ]; then # check if yubikey inserted using ykinfo # this is unfortunately extremely unreliable and prone to usb errors # keep command substitution to minimum if [ -x "$_ykinfo" ]; then ykhex="$($_ykinfo -qH 2>/dev/null)" else echo "INFO: 'ykinfo' not found. Cannot probe yubikey." >&2 fi if [ -n "${ykhex}" ] && [ -x "${_ykp}" ]; then echo "INFO: yubikey found." >&2 # find first free slot slot1=$($_ykinfo -q1) slot2=$($_ykinfo -q2) if [ "${slot1}" -eq "0" ]; then slot="1" echo "INFO: found empty slot ${slot}." >&2 elif [ "${slot2}" -eq "0" ]; then slot="2" echo "INFO: found empty slot ${slot}." >&2 else # no free slot found, ask user for slot slot="" echo "INFO: no empty slot found." >&2 while true; do read -r -p "Select Yubikey slot (Warning, will be overwritten). (1/2): " slot case "$slot" in 1 ) break;; 2 ) break;; * ) echo "Select either 1 or 2."; esac done fi # ask if option use numeric keypad should be used numkey="" while true; do read -r -p "Do you want to use numeric keypad? (y/n): " yn case "${yn}" in [Yy]* ) numkey="-ouse-numeric-keypad"; break;; [Nn]* ) numkey=""; break;; * ) echo "Answer y or n."; esac done # write configuration to key echo "INFO: Running ykpersonalize -${slot} -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr ${numkey} -a${hexkey}" >&2 "$_ykp" -"${slot}" -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr "${numkey}" -a"${hexkey}" else echo "Yubikey setup w/o option to use numeric keypad (-1: slot 1, -2: slot 2):" echo "ykpersonalize -1 -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr -a${hexkey}" echo "ykpersonalize -1 -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr -ouse-numeric-keypad -a${hexkey}" echo "ykpersonalize -2 -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr -a${hexkey}" echo "ykpersonalize -2 -ooath-hotp -ooath-imf=0 -ofixed= -oappend-cr -ouse-numeric-keypad -a${hexkey}" fi fi # setup yubikey for totp if [ "${tokentype}" == "totp" ]; then # check if yubikey inserted using ykinfo # this is unfortunately extremely unreliable and prone to usb errors # keep command substitution to minimum if [ -x "$_ykinfo" ]; then ykhex="$($_ykinfo -qH 2>/dev/null)" else echo "INFO: 'ykinfo' not found. Cannot probe yubikey." >&2 fi if [ -n "${ykhex}" ] && [ -x "${_ykman}" ]; then echo "INFO: yubikey found." >&2 echo "INFO: Running $_ykman oath accounts uri ${uri}" >&2 $_ykman oath accounts uri "${uri}" else echo "Yubikey setup:" echo "ykman oath accounts uri \"${uri}\"" fi fi echo "" echo "users.oath / otp.users configuration:" echo "${algorithm} ${name} - ${hexkey}"