Table of Contents

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:

  1. Convert the current time into a number of time steps since the epoch.

  2. Use HMAC with a shared secret and that time step to generate a hash.

  3. Use the last nibble of the hash to choose a dynamic offset.

  4. Pull out a 4-byte chunk from the hash starting at that offset.

  5. Take the last 31 bits of that chunk and reduce it modulo 10^digits.

  6. 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.

Table of Contents
Table of Contents