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:
#!/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