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