#!/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}"