
TOTP and HOTP codes are one-time passwords computed from a secret key that your authenticator app and the login server both hold, combined with an input that changes every time. Because each side runs the same calculation on the same inputs, your phone can show the exact six digits the server expects, even in airplane mode.
This guide walks through building a code generator yourself in Python, one small step at a time. You will decode a Base32 secret, choose a counter or a clock as the moving factor, compute an HMAC-SHA1, truncate it to six digits, and then prove your work against the official RFC test vectors. It ends with the three problems that most often cause real devices to reject codes.
- Both HOTP and TOTP compute an HMAC of a shared secret and a changing input, then cut it down to digits.
- HOTP uses a counter that goes up per code; TOTP uses Unix time divided by 30, rounded down.
- The moving factor must be packed as 8 bytes, big-endian, before hashing with HMAC-SHA1.
- Dynamic truncation picks 4 bytes from the hash, masks the top bit, and takes modulo 10^6.
- Check your implementation against the RFC 4226 and RFC 6238 test vectors before trusting it.
- 0:00Intro
- 0:26What you'll build and need
- 1:11Decode the Base32 secret
- 2:17Choose the moving factor
- 3:15Compute the HMAC-SHA1
- 4:21Truncate to six digits
- 5:26Verify against the test vectors
- 6:31Fix drift, decoding and desync
- 7:26Four steps, one check
- 8:04Secret plus changing input, cut to digits
Gather the pieces before you write any code
The goal is a small function that prints the same six-digit code as Google Authenticator or any compatible app, at the same moment, given the same secret. If your output matches digit for digit, you understand the algorithm completely.
The arrangement behind it is simple. The login server creates a shared secret and hands it over exactly once, usually through the QR code you scan during setup. Your authenticator stores it. From then on, the server and the app each compute codes on their own. Nothing travels over the network when a code is generated, which is why the app works offline.
- A shared secret key, encoded as Base32 text
- Basic familiarity with HMAC: a hash function that also takes a key
- A counter (for HOTP) or a clock (for TOTP)
- Python 3 installed so you can follow along
Decode the Base32 secret into raw bytes
The secret shown on a setup screen or packed into a QR code is text, but HMAC needs bytes as its key. That text is Base32, which uses only the capital letters A to Z and the digits 2 to 7. The small alphabet lets people type a secret by hand without confusing look-alike characters.
Take the first eight characters of the RFC test secret: G E Z D G N B V. Each character maps to a 5-bit value, so G becomes 6, E becomes 4, Z becomes 25, and so on. Those eight 5-bit values make forty bits, which regroup into five bytes: 31 32 33 34 35 in hex. Read as ASCII, those bytes are simply 1 2 3 4 5.
In Python, import base64, store the secret in a string, and call b32decode on its uppercase form. The full test secret decodes to twenty ASCII bytes: the digits one through zero, twice. That is the official key used throughout the RFC examples. Remember the rule of eight in, five out: every eight Base32 characters always decode to exactly five bytes, so a secret whose length is not a multiple of eight needs equals-sign padding first.
- Convert the secret to uppercase before decoding
- Add '=' padding if the length is not a multiple of eight
- Confirm the test secret decodes to b'12345678901234567890'
import base64
s = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'
key = base64.b32decode(s.upper())
# key == b'12345678901234567890'
Choose the moving factor: a counter or a clock
The key stays fixed for good. To get a different code each time, you need a second input that changes, called the moving factor. It is the only real difference between HOTP and TOTP; everything after this step is identical.
HOTP uses a counter. It goes up by one for each code, both the server and the app keep track of it, and a code stays valid until it is used. In code, it is just an integer you store and increment, starting at zero.
TOTP uses time instead. Take the current Unix time in seconds, divide by thirty, and round down. That value, called T, changes every thirty seconds, so a new code appears on that schedule. The only requirement is a reasonably synced clock. For example, at fifty-nine seconds past the epoch, T equals one. Whether your number came from a counter or a clock, the next two steps treat it exactly the same way.
- HOTP: store an integer counter, start at 0, increment per code
- TOTP: compute T = Unix time // 30
- Check that 59 seconds gives T = 1
import time
# HOTP: you store and increment it
counter = 0
# TOTP: 30-second time steps
T = int(time.time()) // 30
Compute the HMAC-SHA1 over eight big-endian bytes
Step three is the actual cryptography: an HMAC using SHA-1, keyed with your decoded secret, computed over the moving factor. This is where many hand-written implementations go wrong, so it is worth checking every value in an interactive Python shell.
Import hmac, hashlib and struct. Set the key to the twenty ASCII bytes. Pack counter zero with struct.pack using the format string '>Q', which means an unsigned 64-bit integer with the high byte first. Its hex form is sixteen zeros. Then call hmac.new with hashlib.sha1, and the hex digest should start with cc93cf18.
The byte order matters. Counter one must become seven zero bytes followed by a one. If you pack it little-endian, you get a completely different hash and every code will be wrong. The output is always twenty bytes, or forty hex digits, because that is the fixed size of a SHA-1 result. HMAC-SHA1 is still considered safe for this purpose, even though SHA-1 is no longer recommended for many other uses.
- Pack the factor with struct.pack('>Q', value)
- Counter 0 should be 0000000000000000 in hex
- The HMAC for counter 0 should begin cc93cf18
- The digest should be exactly 20 bytes long
python3
Truncate the hash down to six readable digits
Nobody wants to type forty hex digits, so step four, dynamic truncation, picks four bytes from the hash and shrinks them into a short number. It is called dynamic because the hash itself decides where to read from, so the position changes with every code.
Here is the worked example for counter zero. The last byte of the hash is b0, and its low four bits give an offset of zero. Take four bytes starting there: cc93cf18. Clear the top bit to get 4c93cf18. Convert that to decimal: 1284755224. Finally, take it modulo 10^6, which leaves 755224, the exact code the RFC publishes for this input.
Two details trip people up. Masking the top bit keeps the number positive, so Java, JavaScript and C all agree on the result instead of stumbling over signed integers. And leading zeros count: if the modulo gives 42, the code is 000042. Without left-padding to six digits, roughly one code in ten will fail to match.
- Offset = last byte of the hash AND 0x0F
- Read 4 bytes from that offset and clear the top bit
- Take the result modulo 1,000,000
- Left-pad to six digits, so 42 becomes 000042
Verify your output against the RFC test vectors
Once all four steps are written, you do not have to guess whether they are correct. The RFC authors published test vectors: fixed secrets and inputs paired with the exact codes any correct implementation must produce.
A reference tool such as oathtool makes the comparison quick. Store the Base32 test secret in a variable, then ask for HOTP with counter zero and you should see 755224. Counter one gives 287082. For TOTP, request eight digits at fifty-nine seconds and you get 94287082. Run your own function with the same inputs and compare.
The RFC 4226 vectors cover HOTP, listing ten codes for counters zero to nine with the ASCII key one to zero, twice. Your function should match every one. The RFC 6238 vectors cover TOTP at fixed times like fifty-nine seconds, with eight-digit codes. To test against them, pass the time in as a parameter rather than reading the system clock inside your function.
- HOTP counter 0 → 755224
- HOTP counter 1 → 287082
- TOTP at 59 seconds, 8 digits → 94287082
- Make time and digit count function parameters
K=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
oathtool -b --hotp -c 0 $K
oathtool -b --hotp -c 1 $K
oathtool -b --totp -d 8 -N @59 $K
Fix clock drift, bad Base32 and counter desync
Even with passing test vectors, real devices can still reject codes in production. If the vectors pass, the algorithm is fine; what breaks is the data around it: the time, the key, or the counter. Each problem has a recognizable symptom.
Clock drift shows up as codes rejected near the edge of a thirty-second step, because the phone's clock is a few seconds off. Sync time over NTP and accept one step either side. Bad Base32 makes every code wrong, every time; uppercase the secret, strip spaces and add padding. Counter desync happens when a user presses generate without logging in, leaving the app ahead of the server. Check a look-ahead window, then resync the counter.
Keep those tolerance windows small: one step either side for TOTP and a short look-ahead for HOTP. Every extra code you accept gives an attacker one more chance to guess correctly.
- Rejections near step edges: check clock sync
- Every code wrong: check Base32 cleanup
- HOTP failing after unused codes: check look-ahead and resync
- Limit windows to the smallest size that works
Key takeaways
- HOTP and TOTP are the same algorithm with different moving factors: a counter or a time step.
- Decode the Base32 secret to bytes before using it as the HMAC key.
- Pack the moving factor as an 8-byte big-endian integer, or the hash will be wrong.
- Dynamic truncation plus modulo 10^6 produces the code; always keep leading zeros.
- RFC 4226 and RFC 6238 test vectors tell you definitively whether your code is correct.
- Most production failures come from clock drift, malformed secrets or counter desync, not the math.
Frequently asked questions
Why does my authenticator app work without an internet connection?
The secret is shared once during setup, usually via a QR code. After that, the app and the server each compute codes independently from the same secret and the same counter or time, so nothing needs to be sent over the network.
What is the difference between TOTP and HOTP?
HOTP uses a counter that increases by one for each code, and a code stays valid until it is used. TOTP uses the current Unix time divided by thirty, so a new code appears every thirty seconds. The HMAC and truncation steps are identical.
Why are my generated codes always wrong?
If every code fails every time, the secret is the usual suspect: make sure it is uppercase, has no spaces and is padded to a multiple of eight characters. Also confirm you pack the moving factor as big-endian, since little-endian produces a completely different hash.
Is HMAC-SHA1 still safe for one-time passwords?
Yes, HMAC-SHA1 is still considered safe for this use, even though plain SHA-1 is discouraged elsewhere. It is the algorithm the RFC test vectors are built on and what compatible authenticator apps expect by default.
Why do some codes get rejected right at the 30-second boundary?
That pattern points to clock drift, where the phone's clock is a few seconds off from the server. Syncing time via NTP and accepting one time step either side usually fixes it without making guessing much easier.
How do I know my TOTP implementation is correct?
Run it against the RFC 6238 test vectors, such as the eight-digit code 94287082 at fifty-nine seconds. Pass the time in as a parameter so you can test fixed moments instead of the live clock.