← All posts

How Does My Authenticator App Know The Code?

What's really inside that QR code, and what those six digits are made of.

You’re on a plane, thirty thousand feet up, and you remember the one email you meant to check before boarding.

So you buy the inflight wifi for your laptop. The mail site takes your password, and then asks for the six digit code from your authenticator app.

You reach for your phone, open the authenticator app, and there’s the six digit code. And that’s when you notice the little airplane still sitting in the status bar. You forgot to turn airplane mode off after takeoff.

No bars, no wifi, so the phone had no way to get anything from the server. But the app is working anyway, and the code is sitting there, counting itself down. You type it into the laptop, and you’re back in your inbox.

So how did that happen? All these years you thought the authenticator app fetches the latest code from the server and shows it to you. But if that were true, it could never work in airplane mode.

So what’s actually happening underneath an authenticator app?

Let’s find out!

By the way, I’ve written about 2FA and the other authentication methods in an earlier article, if you want the wider picture. This one stays inside the authenticator app and its mechanism.

Nobody sends the code to your phone

Suppose you and your friend Rumi share a secret number. Only the two of you know it. You also agree on a formula for mixing that secret with any other number you’re given, and the formula itself is not a secret at all. Anybody can look it up.

Now Rumi wants to talk to you over the phone and check that you’re really you. So he says 41,207. You put your secret and 41,207 into the formula, and read out the answer. He does the same thing on his side, gets the same answer, and he’s satisfied.

Everybody nearby heard both 41,207 and your answer. Hearing the answer doesn’t reveal the secret. Using a captured answer is a different problem, which we’ll come back to.

That’s the whole idea, and the rest of this article is just detail. Both sides calculate the code independently. You submit your copy when you log in; the server doesn’t send it to your phone.

So only two things have to be true. You need the same secret, and you need to put the same number into the formula.

The secret number is the shared secret, and the number Rumi says out loud is the counter.

The counter is not a secret either. Rumi says it in the open, and everybody hears it. It only has to be the same number on both sides at the same moment. Where that number comes from is the only real difference between HOTP and TOTP.

Here it is in one picture:

Your phone
Shared secretJBSW…3PXP
Counter1
↓
Calculate
↓
996 554
The server
Shared secretJBSW…3PXP
Counter1
↓
Calculate
↓
996 554

Both sides start with the same two inputs.

1 / 3

The one moment of sharing

Your phone got the secret the day you turned 2FA on. Google made some random bytes and kept a copy next to your account. Then it drew those same bytes on the screen as a QR code, and your app read them.

That setup transferred the secret to your phone. Generating a code needs no further transfer, although backup and migration features can copy the secret later.

The app that read it could be Google Authenticator, or Authy, or 1Password, or any of the others. They all follow the same standard, so it makes no difference which one you point at the screen.

I’m using Google on both sides of this article on purpose. Google made the secret, Google Authenticator is holding it, and even those two don’t talk to each other when a code is made. (The app can back your secrets up to your Google account nowadays, but that’s only a backup. Nothing is sent anywhere to make the six digits.)

A QR code isn’t magic, by the way. It’s a picture of one short line of text. Watch how those pieces become an authenticator account:

account.example/security
Set up your authenticator

Scan this QR code with your app.

Setup QR encoding the public demonstration secret and TOTP settingsPublic demo QR
Server keeps JBSW…3PXP
Authenticator

Scan a QR code

Account labelShared secretAlgorithm settings

The website encodes your setup data in a QR. It keeps the secret too.

1 / 5
See the encoded text

otpauth://totp/Google:mim%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=Google&algorithm=SHA1&digits=6&period=30

This scannable QR contains a public 80-bit example key. Never use it to secure an account. Real enrollment needs a securely generated secret of at least 128 bits under RFC 4226.

The secret is only text. So if you scan the same QR on two phones, both phones can produce matching TOTP codes while their settings and clocks agree, without syncing each code.

And if a screenshot of that QR is sitting in your email, then whoever finds it has your second factor.

Six digits out of a counter

So what is that formula? It’s called HMAC, and it’s the same one for everybody. There is nothing to hide in it.

Let me explain it in simple words. HMAC takes a key and a message, and stirs the two of them together into a pile of bytes. With SHA-1 underneath, that pile is always twenty bytes long.

So the key is your secret, the message is the counter, and those twenty bytes are the answer you and Rumi were reading out.

The same key and the same message give the same twenty bytes on any machine. Change one bit of either, though, and the answer is not slightly different. It’s a completely new answer.

Watch what happens when just one input bit changes:

Original00000001
One bit changed00000001

Filled = changed · Empty = unchanged

0of 160 output bits changed

Keep the secret fixed. Start with counter 1.

1 / 3
What is being compared?

The lowest eight bits of a 64-bit counter are shown. The guided example compares counters 1 and 129. In experiment mode, it compares counter 1 with your chosen counter, using the same key. It shows differences, not the raw output. About half the bits tend to change; the exact count varies by input.

A one-bit change tends to flip about half of the hundred and sixty output bits. The exact count varies. That’s on purpose, and people call it the avalanche effect. Nearby counters produce very different outputs. That visual effect alone is not proof of security; predicting codes without the secret depends on the security of HMAC and the strength of the key.

The other half of it is that the formula only runs one way. It’s like stirring milk into your tea. Easy enough to do, but not something you can practically reverse to recover a strong, randomly generated secret.

With a strong, randomly generated secret, recovering it from these answers is computationally infeasible. You could try guessing secrets and checking the results, but a sufficiently large key makes that search impractical.

But written out, those twenty bytes are forty characters of gibberish, and nobody is typing that at a departure gate. So you cut a small piece out of the answer, and then you keep only the last six digits of that piece.

Let’s do one for real. The secret is the same one the demo below uses, and the counter is 1.

  1. The two inputs. The secret is JBSWY3DPEHPK3PXP, and the counter is 1.
  2. Mix them. HMAC-SHA1 gives back twenty bytes. In hex they are 853f79b6650a9e3c5a81f179a209253bcbbe7402.
  3. Find where to cut. The very last byte tells you. Here it’s 02, so start at byte number 2, counting from zero. (When that byte is a bigger one, only its last four bits count.)
  4. Cut four bytes from there. They are 79b6650a. Clear the highest bit to get a positive 31-bit number. It is already clear in this example, so the number is 2,041,996,554.
  5. Keep the last six digits. 996554. That’s the code you type.

So the code on your screen is the tail end of the answer, and nothing more. You never show the whole thing to anybody, only that little piece of it.

All of this together is HOTP, from RFC 4226. The H is for HMAC, and here the counter really is a count.

It starts at 0 on the day you scan the QR code, and it goes up by one every time you ask for a code. Your phone counts, the server counts, and the two are supposed to stay level with each other. That’s why the walkthrough above used counter 1. It was the second code this phone had ever produced.

Watch the phone and server count together, then see what happens when the phone gets ahead. The calculation is there if you want to inspect the bytes:

Automatic demonstration
Your token
Code from counter 0282 760
One press, one code
Server
Waiting for your code··· ···
CounterCalculated code
0282 760
1996 554
2602 287
3143 627
+ 6 more counters in this demo’s search window
Starts at counter 0 until a login succeeds

The token generates its first code with counter 0.

1 / 8
How these six digits are made

Counter 0 → HMAC-SHA1 → 20 bytes

2cd8e904e395a7414877aa2f8261e1243572c435

The last byte’s low four bits select offset 5. Take the highlighted bytes and clear the highest bit.

363,282,760 mod 1,000,000 = 282760

A ten-counter look-ahead is this example’s policy. The key is public demonstration data.

One code and one login keep the counters level. Extra button presses move only the phone forward.

The counter that will not stay put

That’s the trouble with counting presses. Your phone counts every one of them. The server only counts the ones that reach it.

So the server doesn’t try only one counter. It tries a few counters ahead as well, and when it finds a match, it starts the next login at the following counter.

But a window has an edge. Press the button thirty times in your pocket and you fall off it, and then you may need a recovery or resynchronisation flow. Asking for consecutive codes is one approach.

That is why HOTP mostly lives on those little hardware tokens with a button. You almost never see it in a phone app.

So what could both sides count without pressing anything, and without talking to each other?

Time is a counter you already share

Your phone knows the time. The server knows the time. Neither of them had to be told.

So cut time into thirty second blocks and count the blocks. Take the Unix timestamp, which is the number of seconds since the start of 1970, and divide it by thirty. Drop the remainder, and that number is your counter.

This counter isn’t counting anything you did. Nobody pressed a button, and it has nothing to do with the day you scanned the QR code. It’s only what the clock says, divided by thirty. Today that comes out as a number in the tens of millions. It goes up by one every thirty seconds on its own, on your phone and on the server, whether you open the app or not.

That’s TOTP, from RFC 6238, and here is the surprising part: nothing else changes. In our example, it’s the same HMAC, the same twenty bytes, and the same six digits. Only the counter comes from somewhere new.

Watch two example clocks cross a thirty-second boundary. The codes are calculated in your browser; the clock uses selected moments so we can see what changes:

One counter for every 30 seconds · example times (mm:ss)
Your phone
00:59
30–59 seconds
Counter1
996 554
Server
00:59
30–59 seconds
Counter1
996 554
Matching codes

Same time interval, same counter, same code.

1 / 5
The calculation and example clock

Counter = floor(Unix seconds ÷ 30). The clock shows minutes and seconds since the Unix epoch, using small timestamps to make the explanation readable. The hands move between selected moments, not in real time. Both sides use the same public example key with real HMAC-SHA1 calculations. The previous/current/next window is this demo’s policy, not a universal rule. A matching code still needs rate limiting and replay protection.

Then we meet the other problem. Clocks drift, so this demo checks the previous, current, and next thirty-second step. That is a ninety-second span of candidate steps, not ninety seconds of clock drift in either direction. Real servers choose their own acceptance window.

What this doesn’t protect you from

A leaked password is now useless on its own, and that’s the whole point of it.

But plenty of people hear “2FA” and think it means unphishable, and that part isn’t true. A fake login page can ask you for your password and your code, and then use both of them on the real site while you are still watching the spinner:

login-mail.example
Lookalike address · demonstration
MMail
Sign in to your mail
Password · ••••••••
What you see in your browser
YouYour browser
AttackerFake page owner
Real servermail.example

The page looks familiar. But its address is login-mail.example.

1 / 10
Why the outcomes differ

The password and OTP can be forwarded while valid. The displayed code is illustrative. A passkey assertion is bound to a relying-party ID and includes an origin that the server validates. This unrelated lookalike origin cannot request the real site’s credential. These reserved example domains send nothing.

That’s a limitation of one-time codes, and it is why passkeys exist. A passkey is tied to the site it was made for, so the fake page has nothing worth passing along.

Two more, for the people building this rather than using it:

  • The server keeps your secret in usable form. Passwords can be stored hashed. A TOTP secret can’t, because the server needs the real bytes to compute the code. So a database dump containing unprotected secrets gives an attacker everything needed to generate codes, and that is why those secrets need encrypting with a key you keep somewhere else.
  • Six digits gives only a million possible values. Checking several candidate steps increases the chance that a guess matches. Rate limit attempts and reject reuse of an already accepted time step; the thirty-second refresh is not replay protection.

And what about codes over SMS? Somebody can talk your phone company into moving your number to a new SIM, and then your codes go to them. It’s called a SIM swap. The secret in your pocket can’t be moved by a phone call to customer support.

Also, no SMS was ever going to reach you in that middle seat at thirty thousand feet.


So that’s all for today. The app doesn’t need to contact the server to generate your code, on a plane or anywhere else.

Thanks for reading! If I got something wrong, or simplified something in a way that misleads rather than helps, please tell me. I’d like to fix it.

And I’m curious: how many entries are sitting in your authenticator app right now, and do you still know which is which? Mine is a mess.

Get new articles by email.

New articles about software engineering.