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