BASH OpenSSL TOTP Generator

This is a Time-Based One-Time Password generated written in BASH and OpenSSL. I wrote this so I’d have a method to generate these second factor authentication codes, if my phone was damaged or stolen. I’ve tested this with LastPass, Google, and Dreamhost codes.

Download:

  • otp.sh (sig) (sha1:7bd2173b8cb8a10b42a0a08a43d27a08cae7b767)
#!/bin/bash
# otp.sh v1.0 November 15, 2014
# Copyright (c) 2014 Kenji Yoshino https://www.tidgubi.com
# This script is released under the Version 3 of the GNU General Public
# License https://www.gnu.org/licenses/gpl-3.0.txt
#
# This script implements a BASH and OpenSSL Time-Based One-Time Password
# algorithm according to RFC6238. This has been tested with codes from
# LastPass, Google, and Dreamhost.

# decodeChar $ch
# converts Encoded char to Value according to Table 3 of RFC4648 and treat
# lowercase as valid characters.
function decodeChar {
   local ch="${1:0:1}"
   # if this is an = or empty, return 0, but exit 2 to flag that this is the
   # end of the input
   if [[ "$ch" = '=' || "${#ch}" -eq 0 ]]; then
      printf '0'
      exit 2
   fi
   # convert the char to its ASCII Value
   local val=$(printf '%d' "'$ch")
   
   # shift lowercase to upper case
   if [[ $val -gt 96 ]]; then
      (( val -= 32 ))
   fi
   # shift the ASCII value to convert it to the 0-31 value
   if [[ $val -gt 55 ]]; then
      (( val -= 65))
   else
      (( val -= 24))
   fi
   
   # Verify that the decoded value is valid
   if [[ $val -gt 31 || $val -lt 0 ]]; then
      base32dErrorMsg
      exit 1
   fi
   printf "$val"
}

function base32dUsage {
   printf 'Usage: base32d.sh [ | -f ] [-h]\n'
   printf '\n'
   exit 1
}

function base32dexit {
   if [[ "$1" -eq 2 ]]; then
      printf '\n'
      exit 0
   elif [[ "$1" -eq 1 ]]; then
      base32dErrorMsg
      exit 1
   fi
}

function base32d {
   # verify that any command line parameters are valid.
   local input
   local prefix='\x'
   if [[ "$#" -gt 0 ]]; then
      if [[ "$1" = '-f' ]]; then
         if [[ "$2" = '-' ]]; then
            input=$(cat)
         elif [[ -r "$2" ]]; then
            input=$(cat "$2")
         else
            printf 'Cannot access the specified file.\n'
            exit 1
         fi
         if [[ "$3" = '-h' ]]; then
            prefix=''
         fi
      elif [[ "$1" = '-h' ]]; then
         input=$(cat)
         prefix=''
      else
         input="$1"
         if [[ "$2" = '-h' ]]; then
            prefix=''
         fi
      fi
   else
      input=$(cat)
   fi
   
   local ndx=0
   local len=${#input}
   local buffer
   local term=0
   local val

   while [ $ndx -lt $len ]; do
      # parse the next quantum (40 bits) of characters out of the input
      buffer=$(decodeChar "${input:$ndx:1}") || base32dexit $? #1
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}"); term=$? #2
      (( buffer |= val ))
      # print the first 5 + 3 bits
      printf "$prefix$(printf '%02x' $(( buffer >> 2 )) )" # 1st byte
      base32dexit $term
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}") || base32dexit $? #3
      (( buffer |= val ))
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}"); term=$? #4
      (( buffer |= val ))
      (( buffer &= 0xFFF ))
      # print 2 + 5 + 1 bits
      printf "$prefix$(printf '%02x' $(( buffer >> 4 )) )" # 2nd byte
      base32dexit $term
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}") || base32dexit $? #5
      (( buffer |= val ))
      (( buffer &= 0x1FF ))
      printf "$prefix$(printf '%02x' $(( buffer >> 1 )) )" #3rd byte
      base32dexit $term
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}") || base32dexit $? #6
      (( buffer |= val ))
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}"); term=$? #7
      (( buffer |= val ))
      (( buffer &= 0x7FF ))
      printf "$prefix$(printf '%02x' $(( buffer >> 3 )) )" # 4th byte
      base32dexit $term
      (( buffer <<= 5 ))
      (( ndx++ ))
      val=$(decodeChar "${input:$ndx:1}") || base32dexit $? #8
      (( buffer |= val ))
      printf "$prefix$(printf '%02x' $(( buffer & 0xFF )) )" # 5th byte
      (( ndx++ ))
   done
   
   printf '\n'
   exit 0
}

function hmacUsage {
   printf 'hmac.sh sha1|sha256|sha512 key [-f file]'
   exit 1
}

# hex2shell $hex [-r]
# Converts $hex to shellcode. If $hex is not byte aligned, left-pad the data
# unless -r is passed, in which case right pad the data
function hex2shell {
   hex=$1
   rtn=''
   if [[ $(( ${#hex} % 2 )) -eq 1 ]]; then
      if [[ "$2" = '-r' ]]; then
         hex="${hex}0"
      else
         hex="0$hex"
      fi
   fi
   ndx=0
   while [ $ndx -lt ${#hex} ]; do
      rtn="$rtn\x${hex:$ndx:2}"
      (( ndx += 2 ))
   done
   echo $rtn
}

# xorHex $str1 $str2 [-r]
# xors $str1 with $str2. Returns hex unless -r is passed. If -r is passed, raw
# binary is returned.
function xorHex {
   if [[ ${#1} -lt ${#2} ]]; then
      len=${#1}
   else
      len=${#2}
   fi
   
   local rtn=''
   
   ndx=0
   while [ $ndx -lt $len ]; do
      if [[ "$3" = '-r' && $(( $ndx % 2 )) -eq 0 ]]; then
         rtn="$rtn\x"
      fi
      first="0x${1:$ndx:1}"
      second="0x${2:$ndx:1}"
      
      rtn="$rtn$( printf '%x' $((first ^ second)) )"
      (( ndx++ ))
   done
   
   printf "$rtn"
}

# hmac sha1|sha256|sha512 key [-f file]
# key is a key in hex. If a file is specified with -f this will hmac the file.
function hmac {
   block_size=0 # in number of nibbles (hex string length)
   case $1 in
      sha1)
         block_size=128 #512 bits
      ;;
      sha256)
         block_size=128 #512 bits
      ;;
      sha512)
         block_size=256 #1024 bits
      ;;
      *)
         usage
      ;;
   esac

   local key=${2#0x}
   if [[ ${#key} -gt $block_size ]]; then
      key=$( printf "$( hex2shell $key )" | openssl dgst -$1 )
      key=${key##* }
   fi
   
   #pad the key with zeros and truncate to the correct length
   key="${key}0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
   key="${key:0:$block_size}"

   # determine if the hmac will process a file or stdin
   if [[ "$3" = '-f' ]]; then
      if [[ "$4" = '-' ]]; then
         text='-'
      elif [[ -r "$4" ]]; then
         text="$4"
      else
         usage
      fi
   else
      text='-'
   fi
   ipad='3636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636'
   opad='5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c'

   # perform the actual hmac operation per FIPS 198-1
   mac="$( openssl dgst -$1 <(xorHex $key $ipad -r; cat $text) )"
   mac="${mac##* }"
   mac="$( printf $(hex2shell $mac) | cat <( xorHex $key $opad -r ) - | openssl dgst -$1 )"
   printf "${mac##* }\n"
}

function otpUsage {
   printf 'otp.sh [] [-c]\n'
   printf '   This script performs minimal sanitization of  and will most\n'
   printf '   likely encounter an error if an valid secret is provided.\n'
   exit 1
}

function otp {
   local secret=''
   if [[ "$1" = '-h' ]]; then
      otpUsage
   elif [[ "$#" -eq 0 || "$1" = '-c' || "${#1}" -eq 0 ]]; then
      printf 'Enter your OTP secret: '
      read -s secret
      printf '\n'
   else
      secret="$1"
   fi
   
   if [[ "$1" = '-c' || "$2" = '-c' ]]; then
      continuous=1
   else
      continuous=0
   fi
   
   # clean the secret/key into raw base32
   secret=${secret// /}
   secret=${secret//-/}
   secret=$( base32d "$secret" -h )
   
   while true; do
      timestamp=$(( $( date -u +%s ) / 30 ))
      #((timestamp /= 30))
      # if the code will expire in 5 seconds or less, print the next code
      waitTime=$(( $( date -u +%s ) % 30 ))
      waitTime=$((30 - waitTime))
      if [[ "$waitTime" -lt 5 ]]; then
         ((timestamp++))
      fi
      
      timestamp=$( printf '%x' "$timestamp" )
      timestamp="0000000000000000$timestamp"
      timestamp="${timestamp:(-16)}"
      timestamp=$( hex2shell "$timestamp" -n )
      
      hash=$( printf "$timestamp" | hmac sha1 "$secret" )
      offset=$( printf '%d' "0x${hash:39:1}" ) # capture the last nibble of the hmac
      ((offset *= 2)) # convert the byte offset to nibble offset
      hash=${hash:$offset:8} # truncate the hash to 4 bytes starting at offset
      # grab the most significant nibble to strip the most significant bit
      msb=${hash:0:1} 
      msb=$( printf '%d' "0x$msb" )
      ((msb %= 8))
      # recombine the msb with the rest of the hash
      hash="$( printf '%x' $msb )${hash:1:7}"
      hash=$( printf '%d' "0x$hash" ) # concert from hex to decimal
      code="000000$(( hash % 1000000 ))"
      # recalculate the wait time
      waitTime=$(( $( date -u +%s ) % 30 ))
      waitTime=$((30 - waitTime))
      if [[ "$waitTime" -lt 5 ]]; then
         ((waitTime += 30))
      fi
      # print time remaining for the TOTP code followed by TOPT value
      printf "$waitTime: ${code:(-6)}\n"
      
      if [[ "$continuous" -eq 0 ]]; then
         break
      fi
      sleep "$waitTime"
   done
   
}

otp "$1" "$2" "$3" "$4"
exit 0

Leave a Reply

All comments are moderated. Comments must be in english and related to post.