The Time-based One-Time Password algorithm (TOTP) has always been an interesting concept for me as it allows two systems to agree on a set of numbers that change over time even while network partitioned! This opens up a broader set of devices that can serve as a second factor of authentication (something you have) aside from the traditional username-password combo (something you know).
Before diving into the specifics of TOTP, we’ll have to work our way through some of the prior art that it utilizes. The “timeline” looks something like:
Message Authentication Code or MAC are bits of information that are sent along with a message that enables us to verify whether the message hasn’t been tampered with. HMAC (Hash-based MAC) is a variant of MAC that utilizes, well, hashes to perform its job. RFC 2104 defines it as:
H(K XOR opad, H(K XOR ipad, message))
- H - The hash function of your choice (e.g. SHA-1)
- K - Secret key shared between the 2 parties
- opad - The byte
- ipad - The byte
I’m not gonna pretend to be an expert on HMAC so let me leave you this Computerphile video about it.
To implement HMAC, we’ll utilize the Web Crypto API that should be present on most modern browsers.
We’ll first have to “import” a key that specifies the shared secret and the type of hashing algorithm we are going to use (for reasons we’ll see later, we’ll go with the “SHA-1” algorithm). We will then “sign” the “message” using the derived key.
We can implement it as follows:
Up the chain is the HMAC-based One-Time Password algorithm (HOTP) which, as the name implies, is an OTP implementation using HMAC. Since we are now in the realm of OTP generation, we’ll be replacing the “message” with some arbitrary value (called a counter) that changes over time and is known (or can be derived) by both parties. RFC 4226 explains in full the details about HOTP and can be summarized as:
HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))
- Truncate - Computes the OTP from the HMAC output
- HMAC-SHA-1 - HMAC but using the SHA-1 hash function
- K - Shared key between the two parties
- C - Originally a message, but is now an increasing counter value known only to the two parties
The result we get from HMAC-SHA-1 is usually a 20-byte value, which is nowhere close to being usable as an OTP. This is where the
Truncate function comes in. Given the result from HMAC-SHA-1, it basically extracts
N (common is 6) digits dynamically as our OTP.
We can then implement HOTP as follows:
The RFC also includes test values that we can use to verify whether our implementation is correct.
The TOTP algorithm is an extension of the HOTP where instead of using a counter, we utilize time instead (Unix time to be more specific). RFC 6238 defines TOTP as follows:
TOTP = HOTP(K, T)
- K = Shared key between the two parties
- T = (Current Unix time - T0) / X
- T0 = Time to start the count (Default: 0)
- X = Time step in seconds or how long the computed OTP is valid for (Default: 30s)
Implementing TOTP simply involves creating a wrapper on top of the HOTP function such as:
let counter = Math.floor(Date.now() / 30000);
One tweak we’ll be adding to our implementation would be expecting the secret to being encoded in Base32, so we’ll have to include a decoder in our code. This way, our implementation and Google Authenticator should arrive at the same values.
Our final implementation is now:
To verify everything is working, scan the following QR code with your Google Authenticator App:
Or if you are on mobile, enter the following secret:
Then visit https://www.2faas.dev/ and enter the secret above. The OTP generated should be the same as what Google Authenticator should show!