A colleague of mine has just posted a shell script for calculating TOTP secrets. From my POV, using a Python virtual env is overkill, and I was pretty sure that you could do it with bash and a few extra utilities as well. The bash script would probably even be faster (albeit not necessarily more secure, unless you’d gotten all the quotes right). There’s a “proper” shell implementation by Rich Felker on Github, which I didn’t read before implementing a bash-non-posix solution.

Let’s first have a look at the relevant RFCs: The TOTP RFC is a very short read. Basically it tells us that we’re using HOTP with the number of 30-second intervals that has elapsed since the Unix epoch. On to HOTP!

Calculating HOTP is somewhat more involved, but still not incredibly so. First, let’s calculate the counter by dividing the unix timestamp by 30. Also, let’s base32-decode our key, as that’s what’s generally provided by webpages. The base32 decode gives us binary output that we need to encode with xxd -p in order to store it in a variable.

The HOTP RFC specifies that the counter is a 8-byte unsigned integer. This is how we arrived at the printf stanza in the first line, which tells printf to output the value of the counter with 16 hex digits, which is 8 bytes.

Then, we hash using openssl (what else?). To convert our counter to binary, we use xxd -r -p, which seems like the most common CLI tool that can convert hex to binary data. We also need to be careful to not accidentally introduce newlines via echo, which means we need to pass the -n flag whenever we echo something for processing. OpenSSL does hmac-sha1 with the opensl mac subcommand. We provide our hex-encoded key via the hexkey option, and the counter via a binary input stream. I did the most debugging here, as I initially forgot to convert the counter to binary, and hashed the ASCII representation instead. After some mucking around with xxd, I finally got it.

Now that we’ve calculated the hash with OpenSSL, we need to do some extraction. HOTP requires extraction of a different substring of the hash depending on the last four bits of the hash. Since we have the hash as a hexdump, where every letter corresponds to 4 bits, we can get the offset by taking the last character of our hexdump and converting it into a number. I had not previously known of Bash’s ability to specify the base of variables in arithmetic expressions, but it came in very handy here. You can specify a base-16 number by prepending a 16# to it in an arithmetic expression. We then extract the right substring from our hash. Since we’re working on nybbles1, we have to multiply our offset by two to get the correct bytes. We take 8 nybbles, interpret them as a base-16 number, mask off the highest bit, and presto!

And here’s the source (not POSIX, but slightly easier to read than the POSIX variant linked above):

count="$(printf '%.16x' $(($(date +%s)/30)))"
hexkey="$(echo -n "$1" | base32 -d | xxd -p)"
hash="$(echo -n "$count" | xxd -r -p | openssl mac -digest sha1 -macopt hexkey:"$hexkey" HMAC)"
extracted="${hash:$((offset * 2)):8}"
echo "$(((16#$extracted & 16#7fffffff) % 1000000))"

  1. We have title! ↩︎