I've been seeing OTPs ever since I was 7 years old? Or even less, not sure. Be it for Gmail logins, banking, anywhere, and of course 2FA. A code arrives on your phone, you type it in, your identity is validated, and you're in.
Until a few months ago, that was the entire mental model I had.
I joined Kite by Zerodha a few weeks ago and ran into TOTP. Same job on the surface (prove that it's you), but no SMS, no waiting on a carrier, just a rotating code sitting in their own app. I thought about it in the background for a while. Then on a random day I actually decided to understand what's going on.
What follows are the findings and explorations from that dig.
The thing that felt off
I was used to OTPs arriving. Something travels. A message shows up, I copy the digits, I'm in.
Kite's code does not travel at login time. Airplane mode still produces a valid number. That was the first real clue this wasn't "SMS with a different skin." Something local was being computed, and somehow, the server already knew what that computation should look like.
And it wasn't even a separate authenticator app. The digits show up inside Kite itself. Same TOTP logic you'd get from Google Authenticator or Authy. Different shell. The code lives in-app.
What is actually being shared
The answer starts earlier than the login screen.
When TOTP is enabled on an account, the server generates a secret key and shows it once, usually as a QR code or an equivalent enrollment step. Whatever is generating the codes on the client (Kite's own app, in this case) stores it. The server stores it. That is the only deliberate secret exchange. After enrollment, both sides hold the same static parameter K.
Everything after that is both sides independently proving they still have it.
That design sits in the OATH open authentication world. The time-based version is RFC 6238. TOTP is not a Zerodha invention. DigiLocker uses the same shape of prompt for a reason: it's the same family of math showing up in different products.
TOTP extends HOTP, the older HMAC-based one-time password scheme. HMAC stands for Hash-based Message Authentication Code: you run a normal hash function, but keyed with a secret, so only someone who has that secret can produce the same output for the same input. HOTP already mixed a shared secret with a moving counter and ran it through HMAC. TOTP keeps that idea and swaps the counter for time.
C = floor(UnixTime / 30)
OTP = HOTP(K, C)
C is which 30-second window the world is in right now. If my phone's clock and the server's clock are close enough, we derive the same C, feed it into the same public construction with the same K, and land on the same six digits.
Time is the source of uniqueness.
How a login actually works
Once enrollment is done, the check looks like this:
- I already share
Kwith the server from enrollment. - The app (Kite, on my phone) merges
Kwith the current time window and runs that HMAC construction. The truncated result is the code I see in-app. - The server does the same computation for the current window, and usually a neighbor window or two for clock drift. If what I typed matches, I'm in.
No SMS. No third-party authenticator for this flow. Just both sides computing the same short-lived code from the same seed and the same clock window.
What Kite's app and the server are both doing
Tradeoffs: TOTP vs the OTPs we grew up with
Both are second factors. Both spit out short codes. The security story is not the same.
SMS and email OTPs ride a delivery channel (cellular or internet) and they often stick around for minutes. That longer window is convenience. It is also time for a phishing page to collect the code, or for a SIM swap to redirect the message before you even see it. TOTP cuts that channel out of the login path. The code is generated on-device, usually dies in 30-60 seconds, and never needed the network to exist in the first place.
| TOTP (in-app or authenticator) | Traditional OTP (SMS / email) | |
|---|---|---|
| Delivery | Generated locally on-device | Sent over cellular or internet |
| Lifespan | ~30-60 seconds | Often 5-15 minutes |
| Phishing window | Narrow: code dies fast | Wider: longer-lived codes |
| Interception | Near zero at generation time | SIM swap, sniffing, inbox compromise |
| Network needed to generate? | No | Yes |
There are downsides too.
TOTP assumes you still have the enrolled device, or a backup of the seed. Lose the phone without recovery codes and getting back in is harder than hitting "resend SMS." The shared secret also has to live somewhere: on the server and in the app that displays the code. If either side is compromised badly enough, an attacker can mint valid codes too. Clock drift between phone and server is a real operational detail, which is why implementations usually check neighboring time windows.
Against the SMS failure modes I cared about (SIM swap, interception, long-lived codes), TOTP is still the stronger default for something like a brokerage login. "I have the enrolled app that holds the seed" beats "I received a text on a phone number."
What I took away
I expected something proprietary. It wasn't.
RFC 6238 is short. The hash function is public on purpose. The secret is K. The moving part is the time window. Truncate the HMAC output into six digits, and that is the number on the screen.
Google Authenticator, Authy, Aegis, 1Password, hardware tokens: same construction. Kite just runs that client inside its own app instead of sending you to a separate authenticator. Once I saw that, the login screen stopped looking mysterious. It was a standard showing up in an app I already use.
HOTP gave us HMAC plus a counter. TOTP made the counter equal to time. Next time a login shows rotating digits in-app, I'm not waiting for a message. Both sides are recomputing the same short-lived value from a shared secret and a shared clock.