What Is a Time-Based One-Time Password, Really?
If you've ever used two-factor authentication (2FA), you've probably seen a Time-based one-time password (TOTP) in action. It's that short code that appears in your authentication app, ticking down every 30 seconds (or 60, depending on the configuration). You type it in after your password, and it proves to the server that you have access to a shared secret. But what is it, really? How does that number get generated, and why does it change with time?
TOTP stands for Time-based One-Time Password. It’s part of a family of algorithms that generate short numeric codes based on a shared secret and the current time. The idea is simple: you and the server both know a secret key. After a set amount of time, you both run a small algorithm to convert "now" into a 6-digit code. If your code matches the server’s, you’re in.
Most people interact with TOTP through apps like Google Authenticator, Authy, or 1Password. Some use hardware tokens or SMS messages that work the same way under the hood. But the interesting part is that this whole process can be implemented in just a few lines of Python.
Let’s walk through two different implementations. One uses the popular oathtool command-line utility. The other is written directly in Python and shows how the algorithm works behind the scenes.
Two Implementations of TOTP
Option 1: Using oathtool
This version wraps the system tool oathtool and delegates all the work to it. If we want to be able to implement this in Python, we should probably first learn a bit about the tool itself. What better place to start than its help output?
!oathtool --help
Usage: oathtool [OPTION]... [KEY [OTP]]...
Generate and validate OATH one-time passwords. KEY and OTP is the string '-'
to read from standard input, '@FILE' to read from indicated filename, or a hex
encoded value (not recommended on multi-user systems).
-h, --help Print help and exit
-V, --version Print version and exit
--hotp use event-based HOTP mode (default=on)
--totp[=MODE] use time-variant TOTP mode (values "SHA1",
"SHA256", or "SHA512") (default=`SHA1')
-b, --base32 use base32 encoding of KEY instead of hex
(default=off)
-c, --counter=COUNTER HOTP counter value
-s, --time-step-size=DURATION TOTP time-step duration (default=`30s')
-S, --start-time=TIME when to start counting time steps for TOTP
(default=`1970-01-01 00:00:00 UTC')
-N, --now=TIME use this time as current time for TOTP
(default=`now')
-d, --digits=DIGITS number of digits in one-time password
-w, --window=WIDTH number of additional OTPs to generate or
validate against
-v, --verbose explain what is being done (default=off)
Report bugs to: oath-toolkit-help@nongnu.org
oathtool home page: <https://www.nongnu.org/oath-toolkit/>
General help using GNU software: <https://www.gnu.org/gethelp/>
from datetime import datetime, timezone
from math import floor
timestamp = floor(datetime.now(timezone.utc).timestamp())
!oathtool --totp --digits 6 --time-step-size 30 --now '@{timestamp}' deadbeef
561440
After plugging all of that in, we receive the six digits that we would need to plug into our TOTP application for verification. This shell tool is an easy way to implement your own TOTP, and in combination with Python, we can deploy an application that makes use of TOTP behind the scenes. So our next step will be making an abstraction around this tool such that we can easily pass and receive values from it in Python.
from dataclasses import dataclass
from subprocess import check_output
from typing import Literal
@dataclass(frozen=True)
class OathToolkitTotp:
secret: int
step: Literal[30, 60] = 30
digits: Literal[6, 7, 8] = 6
def __call__(self, timestamp):
return (
check_output(
[
'oathtool',
'--totp',
'-d', f'{self.digits}',
'-s', f'{self.step}',
'-N', f'@{timestamp}',
f'{self.secret:x}',
],
text=True
)
).strip()
# need to pass hexadecimal value for parity
OathToolkitTotp(secret=0xdeadbeef, step=30, digits=6)(timestamp)
'561440'
This approach is great if you already have oathtool installed and just want a quick answer. It gives you exactly the same result as a 2FA app.
Option 2: Pure Python
Now that we have a working abstraction around our CLI tool, let's see if we can write our own native Python implementation. Between the TOTP wikipedia page and the RFC 6238 TOTP: Time-Based One-Time Password Algorithm , we have enough information to recreate this process in pure Python.
from dataclasses import dataclass
from hashlib import sha1
from hmac import new
from typing import Literal
@dataclass(frozen=True)
class Totp:
secret: int
step: Literal[30, 60] = 30
digits: Literal[6, 7, 8] = 6
def __call__(self, timestamp):
offset = int.from_bytes(
result := new(
# the length of the secret is important this pad bytes
self.secret.to_bytes(4),
interval := (timestamp // self.step).to_bytes(8),
sha1,
).digest()
) & 0b1111
return f'{(int.from_bytes(result[offset:offset+4]) & 0x7fff_ffff) % 10**self.digits:>0{self.digits}}'
Totp(secret=0xdeadbeef, step=30, digits=6)(timestamp)
'561440'
It’s compact, a little cryptic, and completely self-contained. This version shows the entire TOTP algorithm, minus the hashing internals.
How Does TOTP Work? The algorithm is pretty simple once you break it down. Here’s the idea:
Convert the current time into a number of time steps since the epoch.
Use HMAC with a shared secret and that time step to generate a hash.
Use the last nibble of the hash to choose a dynamic offset.
Pull out a 4-byte chunk from the hash starting at that offset.
Take the last 31 bits of that chunk and reduce it modulo 10^digits.
Zero pad the result to the specified number of digits
Here’s a rough sketch of that logic:
start time = epoch
digest = hmac:sha1(
secret,
floor((current time - start time) / step)
)
byte offset = last 4 bits of digest
code = (last 31 bits of digest[offset : offset + 4]) mod 10^size
Now we can verify that our two approaches produce the same results!
from datetime import datetime, timezone
from math import floor
timestamp = floor(datetime.now(timezone.utc).timestamp())
secret = 0xdeadbeef
totps = [
OathToolkitTotp(secret=secret),
Totp(secret=secret),
]
print(*(f'{t!r:<60}{t(timestamp):>06}' for t in totps), sep='\n')
OathToolkitTotp(secret=3735928559, step=30, digits=6) 561440
Totp(secret=3735928559, step=30, digits=6) 561440
If your clock is accurate and you have oathtool installed, both will produce the same 6-digit code. With that being said, I do want to hedge a bit and recommend not creating your own passcode generator unless you are very familiar with these types of tools. Relying on expert-created tooling is usually the best bet when it comes to anything security-related.
Wrap-Up
You might be surprised by how simple this is. TOTP is used in production systems across the world, and yet the core logic fits in your head. This is a reminder that real-world cryptographic systems are often built from small, understandable parts.
And once you see how it's done, you can go deeper. You could try decoding QR codes that contain TOTP secrets. You could experiment with longer codes or different time steps. You could even write your own implementation of hmac.new and hashlib.sha1, just to see what’s under the hood.
What do you think about this easy TOTP solution? Let me know on the DUTC Discord server!
That's all for this week! Until next time.