Skip to content
KEDBYTE
How Identity Works
Chapter
17

The One-Time Code

Part II · Proving It Is You|12,820 words|about 56 min read|Volume 2

17.0 What this chapter gives you#

  1. You will be able to explain to someone who has never heard the term what a one-time code is, and why a number that works once is worth far more than a password that works forever.
  2. You will be able to compute a one-time code by hand from a real HMAC-SHA-1 output, including the dynamic truncation step, and check your answer against the test values printed in RFC 4226 itself.
  3. You will be able to state what the time-based scheme in RFC 6238 adds to the counter-based scheme in RFC 4226, why the time step is 30 seconds by default, and what each extra accepted step costs in security.
  4. You will be able to read an otpauth provisioning link field by field, say what every parameter does, and explain why the shared secret inside it is written in base32 rather than hexadecimal.
  5. You will be able to list precisely what a server must store for each enrolled authenticator and what it must check on each login, including the one field most implementations leave out.
  6. You will be able to explain drift, resynchronization and look-ahead windows, and put a real number on how much a wider window multiplies an attacker’s chance of a lucky guess.
  7. You will be able to say why a code delivered by text message is weaker than the same code produced on the device, naming the two attacks that make it so and what the American and British regulators have done about each.
  8. You will be able to compare codes sent by text message, by e-mail and by voice call on threat model rather than on feeling, and say which of them quietly collapses into some other system’s security.
  9. You will be able to write, or review, a verification routine that stores seeds properly, refuses replays, throttles guessing and fails closed rather than open.

A password is a secret that you hand over. Once you have handed it over, it is no longer only yours, and whoever now holds it can use it tonight, next week, or in three years when the stolen file finally gets traded. That property, that a password keeps working after it has leaked, is the single largest reason password theft is worth committing. Everything in this chapter is an attempt to remove that property without removing the password.

The trick is old and simple. Instead of proving you know a secret by saying the secret out loud, you prove you know it by saying a number that only somebody who knows the secret could have produced, and you arrange for that number to be worthless a moment later. If the number is overheard, copied, phished into a fake page or read off your shoulder, the thief holds a used ticket. The secret itself never travels. That is the whole idea, and the rest of the chapter is the engineering that makes it work reliably between a device with no network connection and a server on the other side of the world.

The result is a genuine improvement and it is worth being precise about how much of one. Google published measurements in May 2019, with researchers from New York University and the University of California, San Diego, showing that adding a one-time code by text message to an account blocked 100 per cent of automated bot attacks and 96 per cent of bulk phishing attacks. It also showed the limit of the improvement in the same table: the same code blocked only 76 per cent of targeted attacks. A code that works once turns a stolen secret into a stolen moment. A stolen moment is a much smaller thing than a stolen secret, and it is still enough to lose an account, because for a great many attacks a moment is all that was ever needed.

This chapter covers the codes themselves: how they are made, how they reach your phone, how a server checks them, and what differs between a code generated on your device and one posted to you across a phone network. The attacker who sits between you and the site in real time and passes your code along belongs to the next chapter, “The Second Factor That Is Not”, which handles him properly. The chapter before this one, “Password Rules Are Mostly Wrong”, deals with the passwords these codes are bolted onto. Here we stay on the code.

The plain version#

Two identical books of numbers#

Picture a harbour with a lighthouse a few miles offshore. The harbourmaster on land needs to know that any message reaching her by radio really came from the keeper in the lighthouse, and not from somebody with a radio set and an idea. They cannot exchange anything in the moment, because anyone can listen to a radio.

So before the keeper rows out, the two of them make two identical books. Every page has a page number and a six-figure number printed on it. Page 1 says 755224, page 2 says 287082, page 3 says 359152, and there are thousands of pages. The keeper takes one book to the lighthouse and the harbourmaster keeps the other in her desk, and neither book is ever shown to anybody.

Now the keeper wants to prove who he is. He turns to the next unused page, page 1, reads out 755224 over the radio, and crosses the page out. The harbourmaster turns to her page 1, sees 755224, is satisfied, and crosses out her page too. Tomorrow he will read 287082 from page 2.

Everyone with a radio heard 755224 and it did them no good. Both books have crossed out page 1, and 755224 will never be accepted again. To use the system tomorrow the eavesdropper would need page 2, and page 2 cannot be worked out from page 1. He heard a number that was true for exactly one moment and is now rubbish.

That is a one-time code, complete. Two copies of one shared thing. A position that moves forward. A number read from that position. And a rule that a position, once used, is finished.

Why nobody actually prints the book#

Printing thousands of pages is silly, and losing the book is a disaster, so nobody does it that way. Instead, the two sides agree on one short secret, a random string of about twenty characters, and a little mixing machine that they both own.

The mixing machine takes two things, the secret and a number, and stirs them together in a way that is thorough and repeatable. Thorough means changing the number by one changes the answer completely, so page 2 tells you nothing about page 3. Repeatable means the same secret and the same number always give exactly the same answer, forever.

Now there is no book. There is a secret and a counter. Put the secret and the number 1 in to make page 1, the number 2 to make page 2. Both sides hold the same secret, both keep track of which number they are on, and both can generate any page on demand. The book was never real; only the secret and the position were.

The mixing machine has one more property that matters enormously. It only runs forwards. Someone who sees a great many of its answers cannot run it backwards to work out the secret that produced them. This is what lets the keeper shout page after page across an open radio channel for years without ever giving away the thing that generates them.

The number is squeezed down to six digits#

The mixing machine does not naturally produce six digits. It produces a long, ugly result, twenty bytes of apparent nonsense. Nobody is going to read forty hexadecimal characters into a phone. So the last step is to squeeze that long result down into something a person can type.

Squeezing is not just chopping. If you always took the first six digits, and somebody ever found a weakness in the front of the machine’s output, every code would be affected. So the designers let the output choose which part of itself gets used. The very last piece of the long result is read as a small number between 0 and 15, and that number says where to start reading. Four bytes are taken from that spot, one bit at the front is thrown away for a reason we will come to, and what remains is divided by a million; the remainder is your code.

That is worth understanding plainly, because it is the part people consistently get wrong when they write their own version. The output picks its own hiding place, so every code you have ever typed came out of a different part of a different twenty-byte blob.

Turning the page by the clock instead of by hand#

There is a problem with counting. The two books have to stay on the same page. If the keeper reads a page out and the radio drops the message, he has crossed out a page she has not, and a few of those put the two sides on different pages. Real systems solve this, and we will see exactly how, but a cleverer arrangement sidesteps most of it.

Instead of turning the page when you use it, turn the page when the clock says so. Agree that the page number is how many half-minutes have passed since a fixed moment in history. Both sides look at their watches, do the same division, and land on the same page without telling each other anything. Nobody keeps count, and nobody drifts because a message was lost.

This is the version almost everybody has met. The six digits on the phone screen sit there with a little bar draining away, and when the bar runs out the digits change, and they change because the half-minute counter ticked up by one and the mixing machine was run again with the new number. The phone is not receiving anything. It is not online. It is doing arithmetic on a secret it was given months ago and on the time.

The cost of this arrangement is that both sides now depend on their watches agreeing. A phone whose clock is a minute slow is showing yesterday’s page, as far as the server is concerned. Real systems therefore accept the current page and one on each side of it, which is a small and deliberate loosening we will price exactly later.

The whole dance, with real numbers#

Let us follow one person all the way through, and use numbers that you can check yourself against the specification.

Devika runs a pharmacy in Kochi and turns on a code app for her e-mail account. The e-mail provider generates a twenty-character secret for her. We will use the secret that the specification itself uses in its test values, the plain text “12345678901234567890”, so that every number below can be checked against the published document rather than taken on trust. A real secret would be twenty random bytes, not this.

The provider shows Devika a square pattern on the screen. She points her phone’s camera at it. The square is just a picture of a short line of text, and that text carries the secret, her e-mail address, and the provider’s name. Her phone reads it, stores the secret, and from that instant both sides hold the same twenty characters.

Now her phone starts producing codes. In the counting version, the first code is made from the secret and the number 0. Run the mixing machine on those two things and you get a twenty-byte result whose last byte says to start reading at the very beginning. Four bytes from there, one bit dropped, gives 1,284,755,224; the remainder on division by a million is 755224. The next two codes are 287082 and 359152. Those are the first three test values printed in RFC 4226, and any implementation that reproduces them from that secret is correct.

In practice Devika’s phone uses the clock version. At half past nine in the morning on 18 August 2026, by the world’s shared reference clock rather than any local one, the number of whole half-minutes since the start of 1970 was 59,568,180. Feed that number and the same secret into the same machine, and the six digits that appear on her screen are 766139. Thirty seconds later the number becomes 59,568,181 and the code becomes 318835. Thirty seconds before, it had been 105616. There is no pattern between them and no way to get from one to the next without the secret.

She types 766139 into the website. The server, which also holds her secret, works out the same half-minute number from its own clock, runs the same machine, gets 766139, and lets her in. It then writes down that half-minute 59,568,180 has been spent for this account, so that if the same six digits arrive again in the next few seconds, from someone who was watching over her shoulder, they will be refused.

What a thief actually gets#

Now the important part. Suppose somebody standing behind Devika reads 766139 off the screen.

If this were a password, they would have her account. Passwords do not expire when read: one read over a shoulder in Kochi works from a laptop in another country a month later.

Because it is a one-time code, the thief holds a number that stopped being true within thirty seconds, and which the server had already crossed out even inside that thirty seconds. To use it they would have had to be at a login page, with her address and password already typed, at that exact moment. The theft failed not because the code was secret but because it was perishable.

That is the improvement, stated plainly: a stolen secret becomes a stolen moment. And here is the honest other half, which the rest of this chapter is really about. If the thief was at a login page at that exact moment, with her password already typed, the code works perfectly for them. Making the secret perishable does nothing at all against somebody who is standing at the door when it is fresh. That attacker exists, is common, and is the subject of the next chapter.

Where the plain version stops being true#

There is no book, and the server is not a lock#

The two-identical-books picture is comfortable and it hides something important. In the picture, the harbourmaster’s book only lets her check the keeper’s numbers. In reality the shared secret is symmetric, meaning it is exactly the same secret on both sides, and it does not only let the server check codes. It lets the server make them.

The honest version: the server can produce any of your codes it likes, at any time, without your device and without your knowledge. There is no mathematics separating “can verify” from “can impersonate” in this design. That is not a bug, it is the price of a scheme that must work on a device with no network connection and a two-line display. The consequence is that the file of enrolled secrets on the server is not a file of verifiers, it is a file of credentials, and every one of them is live.

This is not theoretical. In March 2011 the company RSA, whose SecurID hardware tokens were the standard corporate one-time code device, disclosed that it had been breached. The open letter of 17 March 2011 from Art Coviello, filed with the American securities regulator, said that information had been extracted relating to SecurID and that it “could potentially be used to reduce the effectiveness of a current two-factor authentication implementation as part of a broader attack”. In May 2011 that stolen information was used in an attack on Lockheed Martin. On 6 June 2011 RSA offered token replacement or security monitoring to its more than 30,000 SecurID customers, and EMC, which owned RSA, took a charge of about 66 million United States dollars in the second quarter of 2011 to cover the fallout. The seeds were the crown jewels because the seeds are the credential.

The code is not unguessable, it is merely unlikely#

The plain version implies the six digits are secret. They are not secret in any strong sense. They are one of a million possibilities, and a million is a very small number.

If an attacker can try codes freely, six digits fall quickly. A million attempts at ten a second is under twenty-eight hours, and against a scheme that accepts a small window of codes rather than exactly one, it is faster still. The only thing standing between an attacker and a guessed code is the server counting failures and stopping.

The honest version: the security of a one-time code is not carried by the code, it is carried by the throttle. RFC 4226 gives the arithmetic in its security requirements: an attacker allowed a certain number of tries against a certain width of accepted window succeeds with probability equal to window times tries divided by ten to the power of the digit count. Six digits, five tries and a three-step window is about one chance in sixty-seven thousand per account. Against one account that is fine. Against four million accounts, with an attacker trying the same five guesses on every one, it is roughly sixty accounts opened, and he does not care which sixty.

“Valid for thirty seconds” is a comfortable lie#

The draining bar next to the digits suggests the code lives for thirty seconds. Two things spoil that.

The first is that the thirty seconds are not yours. The clock version divides all of history into fixed half-minute blocks, so if you glance at your phone one second before a block ends, your code has one second of life. That is why users so often type a code that has just died.

The second and larger reason is that servers do not accept only the current block. Almost every real deployment accepts the previous block as well, and many the next, to cover clock differences and typing time. A code the specification calls valid for one thirty-second block is in practice accepted across sixty to ninety seconds, and on badly configured systems considerably more. RFC 6238 works this example in its section on resynchronization: a thirty-second step with two steps backward gives a maximum elapsed drift of around eighty-nine seconds.

The honest version: the lifetime of a code is a policy choice made by the verifier, not a property of the code, and it is always longer than the number on the screen suggests. A wider window is not free, and later in this chapter we price it exactly.

The two clocks are not the same clock#

The plain version says both sides look at their watches. It quietly assumes the watches agree. They do not, and they drift apart for reasons nobody controls.

A phone keeps good time because it corrects itself against the network. A hardware token, a plastic fob with a battery and no radio, has a cheap crystal and drifts a few seconds a month for the five or so years of its life. A token left in a drawer for two years can be a minute out of step and will fail every attempt, though nothing is broken and the secret is intact.

The honest version: the clock version does not remove the synchronization problem, it converts it from a counting problem into a timekeeping problem, and timekeeping problems accumulate silently. Serious verifiers therefore record, per token, how far out that particular device has been running, and apply that offset on the next check. That is a real feature with a real name, and we will build it.

Where the code travels decides most of its security#

The plain version treats the code as the interesting object. In practice, once you have chosen a decent algorithm, almost the entire remaining security question is about the channel.

A code generated on the device in your hand never crosses a network until you type it. A code sent as a text message crosses the mobile phone network, a system of interconnected operators built decades ago on the assumption that operators trust each other, and reachable by anyone who can rent access to it. A code sent to your e-mail arrives in a mailbox whose security is somebody else’s problem, and which is very often the same mailbox used to reset the password the code protects. A code read out by a robot on a phone call can land in a voicemail box with a four-digit code the network set to a default nobody changed.

The honest version: the algorithm is the same in all four cases and the threat model is completely different in all four cases. Most public argument about one-time codes is really argument about delivery channels wearing the algorithm’s clothes.

One use does not mean one attacker#

The last thing the plain version hides is the most important, and it is why the chapter’s thesis stops where it does. A code that can only be used once can still be used once by the wrong person.

If somebody builds a convincing copy of your bank’s login page, takes your password, takes the code you type, and immediately passes both to the real bank on their own machine, the code is used exactly once, by them, within its life. Every rule the specification demands has been obeyed. The account is gone. That attacker is not exotic and does not require breaking any cryptography, and defending against him requires a completely different property, which is that the credential must refuse to work on the wrong website. The next chapter, “The Second Factor That Is Not”, is about exactly that gap and about which methods close it.

For this chapter the correction is simply stated. Making a secret perishable removes the value of recording it. It does not remove the value of relaying it.

The technical version#

HMAC, the one primitive underneath all of it#

Every scheme in this chapter is built on a keyed message authentication code, and specifically on HMAC, defined in RFC 2104 of February 1997 by Hugo Krawczyk, Mihir Bellare and Ran Canetti, and restated by the American standards body as FIPS 198-1 in July 2008. HMAC (a keyed hash, meaning a hash function combined with a secret key so that only a key holder can produce the right output) takes a key and a message and produces a fixed-length tag.

The construction is deliberately dull. Take the key, pad it out to the hash function’s block size, which for SHA-1 is 64 bytes. Make two versions of that padded key: one exclusive-ORed with the byte 0x36 repeated, called the inner pad, and one exclusive-ORed with the byte 0x5c repeated, called the outer pad. Hash the inner pad followed by the message, then hash the outer pad followed by that result. The output for SHA-1 is 160 bits, which is 20 bytes.

HMAC(K, m) = H( (K' xor opad) || H( (K' xor ipad) || m ) )

  K'    key padded with zeros to 64 bytes for SHA-1
  ipad  the byte 0x36 repeated 64 times
  opad  the byte 0x5c repeated 64 times
  ||    means "followed by"
  H     SHA-1, output 20 bytes

RFC 4226 fixes the hash at SHA-1, and this is the first thing a modern reader objects to. The objection is misplaced. SHA-1’s collision resistance was broken in public on 23 February 2017, when Marc Stevens, Elie Bursztein, Pierre Karpman, Ange Albertini and Yarik Markov of CWI Amsterdam and Google published two different files with the same SHA-1 hash, an attack they named SHAttered. Collision resistance is the property that you cannot find two messages with the same digest, and HMAC does not rest on it. HMAC rests on the compression function behaving like a pseudorandom function under a secret key, a property SHAttered leaves untouched. RFC 4226 anticipated the question in Appendix B, which discusses SHA-1 status, HMAC-SHA-1 status and HOTP status separately for exactly this reason.

That is the established fact. The honest caveat is a schedule rather than a break: the American National Institute of Standards and Technology announced in December 2022 that it intends SHA-1 to be phased out of federal use by 31 December 2030, and implementations reaching that far ahead should be able to run SHA-256 as well. As a matter of deployment, as of August 2026 the overwhelming majority of authenticator apps and servers still use SHA-1 for this purpose, because the provisioning format’s default says SHA-1 and because most apps ignore any instruction to do otherwise.

HOTP in exact terms: RFC 4226#

RFC 4226, “HOTP: An HMAC-Based One-Time Password Algorithm”, was published in December 2005 by David M’Raihi of VeriSign, Mihir Bellare of the University of California San Diego, Frank Hoornaert of Vasco, David Naccache of Gemplus and Ohad Ranen of Aladdin. It is an Informational RFC, not a Standards Track one, which surprises people: the algorithm that guards a large fraction of the world’s corporate logins has never been an IETF standard. It came out of the Initiative for Open Authentication, announced by VeriSign on 24 February 2004 at that year’s RSA Conference, whose purpose was to give the industry a token algorithm that was not one vendor’s private property.

Section 4 lists six requirements the algorithm had to meet, and they explain most of its shape. It had to be counter-based rather than clock-based so it could run on a cheap chip with no crystal. It had to be economical enough for a device with very little memory. It had to work on a token with no keypad. The output had to be at least six digits and numeric only, because it has to be read aloud and typed on a telephone. There had to be a usable resynchronization mechanism. And the shared secret, in the RFC’s words, “MUST be at least 128 bits”, with the document recommending 160 bits.

The algorithm itself is three lines. Let K be the shared secret, C an 8-byte counter, and Digit the number of digits wanted.

1.  HS   = HMAC-SHA-1(K, C)        20 bytes
2.  Sbits = DT(HS)                 31 bits
3.  Snum  = StToNum(Sbits)         an integer
4.  D     = Snum mod 10^Digit      the code

C is a counter, not a timestamp, and it is transmitted to the HMAC as an 8-byte big-endian integer. Counter 0 is the bytes 00 00 00 00 00 00 00 00. Counter 1 is 00 00 00 00 00 00 00 01. This is why the scheme is called HOTP, for HMAC-based one-time password, and why the device needs no clock at all: a button press increments the counter, and that is the entire state.

Dynamic truncation, worked by hand#

Step 2 is the part worth doing slowly, because it is the part that goes wrong in home-made implementations. RFC 4226 section 5.3 defines it as a function DT over the 20-byte string:

DT(String)  // String = String[0]...String[19]
  Let OffsetBits be the low-order 4 bits of String[19]
  Offset = StToNum(OffsetBits)      // 0 <= Offset <= 15
  Let P = String[Offset]...String[Offset+3]
  Return the Last 31 bits of P

Read that carefully. The last byte of the HMAC output chooses, through its low four bits, where in the output to start. Because that value is between 0 and 15, and four bytes are taken, the highest index touched is 18, so the read never runs off the end. The top bit of the four-byte value is then discarded, leaving 31 bits, and that discard exists for a purely practical reason: it guarantees the value is positive when read as a signed 32-bit integer on platforms that have no unsigned type. Java in 2005 had no unsigned int. A bit of security theatre it is not, and a bit of security it is not either.

Now the worked example, using the specification’s own test secret so you can check every step. The secret is the ASCII string “12345678901234567890”, which is the twenty bytes 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30. Take counter 0.

HMAC-SHA-1(K, 0) laid out by index:

 idx  00 01 02 03 04 05 06 07 08 09
 val  cc 93 cf 18 50 8d 94 93 4c 64

 idx  10 11 12 13 14 15 16 17 18 19
 val  b6 5d 8b a7 66 7f b7 cd e4 b0

Now the six steps of dynamic truncation, by hand.

1  last byte, at index 19, is 0xb0
2  low 4 bits of 0xb0 are 0x0, so Offset = 0
3  take bytes at 0,1,2,3   ->  cc 93 cf 18
4  drop the top bit: 0xcc AND 0x7f = 0x4c
5  P = 0x4c93cf18 = 1284755224 in decimal
6  1284755224 mod 1000000 = 755224

The code is 755224. Turn to Appendix D of RFC 4226 and the first row of the table reads 4c93cf18, 1284755224, 755224. Your arithmetic and the specification agree.

Do counter 1 for practice. The HMAC is 75a48a19 d4cbe100 644e8ac1 397eea74 7a2d33ab. The last byte is 0xab, whose low four bits are 0xb, which is 11. Bytes 11 to 14 are c1 39 7e ea. Clearing the top bit gives 0x41397eea, which is 1094287082, and the remainder on division by a million is 287082. Second row of Appendix D: 41397eea, 1094287082, 287082.

Do the specification’s own section 5.4 example too, because it exercises a different offset. Its HMAC value is 1f869869 0e02ca16 618550ef 7f19da8e 945b555a. Last byte 0x5a, low nibble 0xa, so offset 10. Bytes 10 to 13 are 50 ef 7f 19. The top bit of 0x50 is already clear, so P is 0x50ef7f19, which is 1357872921, and the code is 872921.

Here is the full Appendix D table, which is the single best test suite for any implementation you write or review.

Count Truncated hex Decimal HOTP
0 4c93cf18 1284755224 755224
1 41397eea 1094287082 287082
2 082fef30 137359152 359152
3 66ef7655 1726969429 969429
4 61c5938a 1640338314 338314
5 33c083d4 868254676 254676
6 7256c032 1918287922 287922
7 04e5b397 82162583 162583
8 2823443f 673399871 399871
9 2679dc69 645520489 520489

Two implementation traps hide in that table. Row 2 prints as 82fef30 in the RFC, which is seven hexadecimal characters because the leading zero is not shown; the value is a full four bytes and your code must treat it as such. Row 7 prints as 4e5b397 for the same reason. And every row must be zero-padded to the digit count before it is displayed or compared, or a code that happens to start with a zero will be shown as five characters and rejected. Comparing codes as integers rather than as fixed-width strings is a real and common defect.

What six digits actually buys, and the arithmetic of windows#

RFC 4226’s security analysis, in section 6 and Appendix A, reaches a formula that every operator should be able to recite. Writing s for the number of codes the server will accept at any moment, meaning the width of the look-ahead or drift window, and v for the number of attempts an attacker is allowed before being throttled, the probability of a successful guess is approximately

Sec = s * v / 10^Digit

The appendix is careful to show that this bound is achieved: HOTP’s outputs behave as uniformly and independently distributed 31-bit strings, the reduction to digits introduces only negligible bias, and brute force is therefore the best available attack. There is no shortcut. The whole of the defence is the three numbers in that formula.

Digits Window x tries Chance of a hit
6 1 x 5 1 in 200,000
6 3 x 5 1 in 66,667
6 3 x 30 1 in 11,111
6 9 x 100 1 in 1,111
8 3 x 5 1 in 6,666,667

The design tension is plain. A deployment that accepts nine time steps to placate users with bad clocks, and allows a hundred tries because nobody wrote the throttle, gives an attacker better than one chance in twelve hundred per account; across a hundred thousand accounts that is roughly ninety accounts opened by pure guessing, with no phishing and no malware. Appendix E.1 of RFC 4226 notes the remedy: eight digits moves the denominator from a million to a hundred million, which is why banking tokens so often show eight digits while consumer apps show six.

The throttle itself is specified, loosely, in section 7.3. The RFC introduces a parameter T for the maximum number of attempts and says it should be kept “as low as possible, while still ensuring that usability is not significantly impacted”, and it offers the alternative of increasing delays, waiting T times A seconds after the A-th failure. It does not name a number. NIST does: SP 800-63B revision 4, published 31 July 2025, requires rate limiting on authenticators whose secrets have under 64 bits of entropy, which a six-digit code plainly does, and caps consecutive failed attempts at 100.

TOTP in exact terms: RFC 6238#

RFC 6238, “TOTP: Time-Based One-Time Password Algorithm”, was published in May 2011 by David M’Raihi, Salah Machani of Diversinet, Mingliang Pei of Symantec and Johan Rydell of Portwise. Like RFC 4226 it is Informational. It is a very short document because it changes exactly one thing: where the counter comes from.

T = floor( (Current Unix time - T0) / X )
TOTP = HOTP(K, T)

  T0  the epoch to count from, default 0
  X   the time step in seconds, default 30

That is the entire algorithm. The counter is no longer a button press, it is the number of complete X-second intervals since T0. Because it is fed into the same HMAC as an 8-byte big-endian integer, and because the RFC explicitly requires that “the implementation MUST support a time value T larger than a 32-bit integer when it is beyond the year 2038”, the counter has room for a very long time indeed.

Why thirty seconds. RFC 6238 gives the reasoning in section 5.2 and it is a straight trade: a larger step means a wider window in which a captured code is still usable, and a smaller step means users watching a code die while they are still typing it. The document says a default step of 30 seconds is chosen as a balance between security and usability. That number is a standard default written in a specification, not a law of nature, and it is a parameter you can change; whether the app in the user’s hand will honour your change is a separate question we come to under provisioning.

The test vectors in Appendix B are the definitive conformance check, and they hide a trap that has cost implementers days. The table below gives the SHA-1 rows.

Unix time T (hex) TOTP, 8 digits
59 0000000000000001 94287082
1111111109 00000000023523EC 07081804
1111111111 00000000023523ED 14050471
1234567890 000000000273EF07 89005924
2000000000 0000000003F940AA 69279037
20000000000 0000000027BC86AA 65353130

The trap is the seed. The RFC’s prose implies the same twenty-byte secret for all three hash variants, and the SHA-256 and SHA-512 columns simply will not reproduce if you use it. Errata ID 2866 against RFC 6238, which is in Verified state, records the correction: the SHA-256 rows use a 32-byte seed and the SHA-512 rows a 64-byte one, formed by continuing the same digit pattern, because HMAC keys are conventionally taken to match the hash’s output length. Feed “12345678901234567890123456789012” for SHA-256 and the 64-byte continuation for SHA-512, and every row reproduces exactly.

Now carry our own example through. On 18 August 2026 at 09:30:00 by the shared world clock, Unix time is 1,787,045,400. Divide by 30 and take the floor: T is 59,568,180, which in hexadecimal is 38CF034 and as an 8-byte counter is 00 00 00 00 03 8C F0 34.

HMAC-SHA-1(K, 0x00000000038CF034) =
    27cb07f2 b11e6269 fb049870 90518d5b ab545915

 last byte 0x15, low nibble 5, so Offset = 5
 bytes at 5,6,7,8  ->  1e 62 69 fb
 top bit of 0x1e already clear
 P = 0x1e6269fb = 509766139
 509766139 mod 1000000 = 766139

Devika’s screen shows 766139, exactly as the plain version promised, and now you can derive it rather than believe it. The step before, T minus one, gives 105616; the step after gives 318835. Nothing about 766139 hints at either.

Before any of the arithmetic can happen, the same twenty bytes have to exist in two places. Getting them there is the least standardized part of the whole subject, and the part with the most operational risk.

The secret must be generated by the server with a cryptographically secure random source, at the length RFC 4226 recommends, which is 160 bits, or twenty bytes. It is then encoded as base32, defined in RFC 4648 of October 2006, section 6, using the twenty-six capital letters and the digits 2 to 7 with the equals sign as padding. That alphabet is chosen with care: it omits 0, 1, 8 and 9, so a zero cannot be confused with the letter O nor a one with the letter I, and it is case-insensitive, so a user can type it in lower case or read it aloud. Because 160 divides exactly by 5, a twenty-byte seed becomes exactly 32 base32 characters with no padding. Hexadecimal would need 40; base64 is shorter but case-sensitive and contains characters needing escaping in a link.

The transport format is the otpauth link, and its status matters because people assume it is a standard. It is not. The de facto specification is a wiki page in Google’s google-authenticator project titled Key Uri Format, and every authenticator app implements some reading of it. Two individual Internet-Drafts have tried to write it down properly: draft-linuxgemini-otpauth-uri-02, revised 17 February 2025 and expired on 22 August 2025, and draft-andesco-otpauth-uri-00, submitted on 19 February 2026. Neither belongs to an IETF working group nor sits on the standards track, and as of August 2026 there is no RFC for the otpauth scheme. This is a convention that half the internet depends on, not a standard.

The shape is a link with a type, a label and query parameters.

otpauth://totp/Kerala%20Mail:devika%40kmail.in
  ?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
  &issuer=Kerala%20Mail
  &algorithm=SHA1
  &digits=6
  &period=30

Written on one line with no spaces, that is about 130 characters. The type is either totp or hotp. The label is the account name, optionally prefixed by the issuer and a colon, both percent-encoded. The parameters are these, and the requirement levels are the wiki’s own words.

Parameter Level Default
secret REQUIRED none, base32
issuer STRONGLY RECOMMENDED none
algorithm OPTIONAL SHA1
digits OPTIONAL 6
period (totp) OPTIONAL 30
counter (hotp) REQUIRED none

Two warnings sit inside that table. The wiki still cites RFC 3548 for base32, obsoleted by RFC 4648 twenty years ago; the alphabet is identical, so nothing breaks, but it shows how long this page has been the specification by default. The second warning is stated on the page itself and is serious: the algorithm parameter is ignored by the Google Authenticator implementations, the period parameter is ignored, and on some platforms digits is ignored too. Provision a credential with period=60 and digits=8, have the app quietly use 30 and 6, and enrolment appears to succeed while every subsequent login fails. That is an implementation detail of particular apps, not a property of TOTP, and it is the commonest reason a correct server rejects a correct app.

The square of dots is a QR code, standardized as ISO/IEC 18004 and invented at Denso Wave in Japan in 1994 by a team led by Masahiro Hara for tracking car parts, carrying error correction at four levels that recover roughly 7, 15, 25 and 30 per cent of the symbol. It is worth saying plainly what it is: a picture of that text and nothing else. It performs no encryption, no authentication and no key exchange. Anyone who photographs the screen has the seed forever and can generate every future code. Displaying it only inside an authenticated session, never mailing it, never letting it into a support ticket and never re-displaying it after confirmation all follow from that one fact.

ENROLMENT, once
  server                                 phone
    |  1 make 20 random bytes             |
    |  2 wrap and store, mark PENDING     |
    |  3 show otpauth link as QR ----->   | 4 scan,
    |                                     |   keep seed
    |  <----- 5 one code, typed by hand --|
    |  6 check it, mark CONFIRMED         |

LOGIN, every time
  phone   T = floor(unix_time / 30)
          code = truncate(HMAC(seed, T)) mod 10^6
  user    types the six digits
  server  recompute for T-1, T, T+1
          reject any T at or below last_step
          on success, set last_step = T

Step 6 is not optional and is skipped constantly. If the server marks the credential active the moment it renders the QR code, a user who mis-scans, or who scans into an app on a phone that is then wiped, is locked out of the account with no way back except support. The credential must stay pending until the user has proved, with one working code, that the seed landed.

One last detail about the example above. The secret shown, GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ, is the base32 form of the specification’s test secret “12345678901234567890”, and you can see the problem with your own eyes: the sixteen characters GEZDGNBVGY3TQOJQ appear twice. A real seed from a real random source has no such pattern. If a provisioning link you are reviewing has visible structure, it was not randomly generated.

Resynchronization, drift and look-ahead#

Both schemes have a counter and the two sides can disagree about it. The failure modes are different and so are the fixes.

In HOTP the counter moves only when the button is pressed. Users press the button. They press it in a pocket, they press it twice, they press it to see whether the battery still works. Every stray press moves the token ahead of the server, and the server, which is still at counter 12 while the token is at counter 19, sees a code that is simply wrong.

RFC 4226 section 7.4 defines the remedy. The server holds a look-ahead parameter s and, on receiving a code, computes the expected values for counters C through C plus s. If one matches at C plus k, the user is authenticated and the server sets its counter to C plus k plus 1, so the two are synchronized again. The RFC is blunt about the cost: s “SHOULD be set as low as possible, while still ensuring that usability is not impacted”, because s sits directly in the numerator of the guessing formula, and doubling the look-ahead doubles the attacker’s chance. When a token has run beyond even a generous look-ahead, the fix is a resynchronization procedure in which the user submits two consecutive codes; two consecutive matches pin the counter exactly and cannot be produced by a guesser, and RFC 4226 sketches such a method in Appendix E.4.

The absolute rule in HOTP is that the server must never accept a counter at or below the one it has already used. Accepting a code from the past is not resynchronization, it is replay.

In TOTP the counter moves by itself and the disagreement is about clocks. A phone is usually within a second of correct because it synchronizes with the network. A hardware fob has a cheap crystal and no radio, and drifts steadily, typically a few seconds a month over a lifetime of several years, so a fob issued in 2021 can be a minute out by now with nothing wrong at all.

RFC 6238 handles this in two places. Section 5.2 says a validation system should set a policy for acceptable transmission delay and recommends “at most one time step is allowed as the network delay”. Section 6 handles genuine drift: it recommends a specific limit on how far out of step a client may be, works the example of a thirty-second step with two backward steps giving a maximum drift of around 89 seconds, and then says the part almost nobody implements. On a successful validation at an offset, the server should record that token’s detected clock drift in time steps and validate the next code against the current time adjusted by it. That turns a fob a minute slow from a permanent nuisance into a one-time correction.

Setting Window s Guess chance
Current step only 1 1 in 200,000
One back, one fwd 3 1 in 66,667
Two back, two fwd 5 1 in 40,000
Four back, four fwd 9 1 in 22,222

Those chances assume five allowed attempts and six digits. The table is the argument for narrow windows plus recorded drift, rather than wide windows and hope. RFC 6238 also warns of the case its automatic correction cannot fix: the longer a token has gone unused, the more drift has accumulated, and if it exceeds the allowed threshold the server should fall back to a separate identity check and an explicit resynchronization rather than widening the window for everybody.

Replay prevention and what the server must store#

RFC 6238 states the replay rule in section 5.2 as a hard requirement: a prover may send the same code inside a time-step window more than once, and “the verifier MUST NOT accept the second attempt of the OTP after the successful validation has been issued for the first OTP, which ensures one-time only use of an OTP”. NIST SP 800-63B revision 4 says the same thing for out-of-band authenticators in section 3.1.3, requiring that a given authentication secret be accepted as valid only once during its validity period, and it bounds that period: an out-of-band authentication is invalid unless completed within 10 minutes. The same document requires that out-of-band secrets be at least six decimal digits, and, in section 3.1.4 on single-factor one-time-password devices, that a nonce based on a real-time clock change at least once every two minutes.

Implementing “only once” means storing something. The minimum is a single integer per credential: the highest time step, or counter, that has ever been accepted. A code is refused unless its step is strictly greater than that number. This one field is the field most implementations leave out, and leaving it out is not a small bug. Without it, a code shouted across a room, read from an unlocked screen, or captured by a proxy is usable again for as long as its window lasts, which is exactly the window you widened for the users with bad clocks.

Here is a complete stored record for Devika’s credential, with every field a correct implementation needs.

{
  "credential_id": "totp_9f21c4",
  "user_id": "u_84412",
  "type": "totp",
  "wrapped_seed": "v1:kms:AQICAHh...ciphertext...",
  "algorithm": "SHA1",
  "digits": 6,
  "period": 30,
  "t0": 0,
  "steps_back": 1,
  "steps_forward": 1,
  "drift_steps": 0,
  "last_step": 59568180,
  "fails": 0,
  "max_fails": 5,
  "lock_seconds": 900,
  "created_at": "2026-08-18T09:28:11Z",
  "confirmed_at": "2026-08-18T09:30:02Z"
}

Three things about that record deserve saying out loud. The seed is wrapped, not hashed: a password can be a one-way hash because the server only checks it, but a shared seed must be recomputed from, so the best available answer is encryption under a key that is not in the database, held by a key management service or hardware security module. That is precisely what RFC 4226 section 7.5 recommends when it says to encrypt shared secrets using tamper-resistant hardware and limit access to the processes that need it. Next, last_step must be written in the same transaction that authorizes the login, or two simultaneous requests carrying the same code can both succeed. Last, the drift field is what turns an old fob from a support call into a silent correction.

A verification routine, line by line#

Here is the whole server side, in Python, small enough to read in full. It generates codes, checks them, refuses replays, applies recorded drift, throttles failures and compares in constant time.

import hmac, hashlib, struct, time

def hotp(key: bytes, counter: int, digits: int = 6) -> str:
    msg = struct.pack(">Q", counter)
    h = hmac.new(key, msg, hashlib.sha1).digest()
    off = h[19] & 0x0F
    raw = struct.unpack(">I", h[off:off + 4])[0]
    return str((raw & 0x7FFFFFFF) % 10 ** digits).zfill(digits)

Four things in six lines. The counter goes in as 8 bytes big-endian. The offset comes from the low nibble of the last byte. The top bit is cleared. The result is zero-padded to a fixed width so that a code beginning with zero still compares equal.

def record_failure(cred, now):
    cred["fails"] += 1
    if cred["fails"] >= cred["max_fails"]:
        cred["locked_until"] = now + cred["lock_seconds"]
    return False, cred

def verify_totp(cred, code, now=None, unwrap=None):
    now = int(time.time()) if now is None else int(now)
    if cred.get("locked_until", 0) > now:
        return False, cred
    if cred.get("confirmed_at") is None:
        return False, cred
    if len(code) != cred["digits"] or not code.isdigit():
        return record_failure(cred, now)

    raw = (now - cred["t0"]) // cred["period"]
    base = raw - cred["drift_steps"]
    key = unwrap(cred["wrapped_seed"])

    lo = -cred["steps_back"]
    hi = cred["steps_forward"]
    for d in range(lo, hi + 1):
        cand = base + d
        if cand <= cred["last_step"]:
            continue
        want = hotp(key, cand, cred["digits"])
        if hmac.compare_digest(want, code):
            cred["last_step"] = cand
            cred["drift_steps"] -= d
            cred["fails"] = 0
            return True, cred
    return record_failure(cred, now)

Walk it. A locked credential fails immediately, before any comparison, so a locked-out attacker learns nothing from timing. An unconfirmed credential fails, which is what makes enrolment safe. A code of the wrong length or shape counts as a failure, because letting malformed input skip the counter is a free retry. The candidate step starts from the raw clock and is shifted by whatever drift this token has shown before. Any candidate at or below last_step is skipped without comparison, which is the replay defence and costs nothing. The comparison uses compare_digest rather than the equality operator, so the time taken does not leak how many leading digits were right. On success the drift record moves by minus d, so a token that ran one step behind today is met one step behind tomorrow, and the window is not consumed by a fault the server could have corrected.

What the routine deliberately does not do is as important. It never widens its own window on failure. It never falls back to accepting the previous code “just this once”. It never logs the submitted code, because a log line containing a live code is a live credential sitting in a log aggregator. And it fails closed: if unwrapping the seed raises, no code is accepted.

Codes sent to you: SMS, the signalling network and the SIM#

Everything so far has assumed the code is computed on a device you hold. A large fraction of the one-time codes actually typed in the world are not computed on your device at all. They are generated on the server and posted to you as a text message. NIST calls this an out-of-band authenticator, meaning the secret travels to you over a channel separate from the one you are logging in on, and its security depends on that channel in a way the algorithm never can.

The algorithm usually stops mattering. Most SMS code systems do not run TOTP at all; they generate a random six-digit value, store it with an expiry, and send it. That is a perfectly reasonable design. The whole question is who else can read the message, and there are two structural answers.

The first is the signalling network. Mobile networks coordinate through Signalling System No. 7, a family of protocols designed from the 1970s onwards for a world of a few dozen state telephone monopolies who had every reason to trust one another. Addresses on that network are called Global Titles. A party with signalling access can, among other things, tell the network that a subscriber is roaming on its equipment, at which point messages destined for that subscriber are delivered to it. There is no password on this. There is only the assumption that whoever is on the network belongs there.

That stopped being an assumption in public in 2017. The German newspaper Sueddeutsche Zeitung reported that criminals had used signalling access, obtained through a foreign mobile operator’s network, to intercept the mobile transaction authentication numbers that German banks send by text to confirm transfers, and had emptied accounts. The attack ran in two stages: first ordinary phishing or malware to obtain the online banking credentials, then the signalling manoeuvre to have the confirmation message delivered somewhere else. O2-Telefonica confirmed to the newspaper that some of its German customers had been targeted in this way in mid-January 2017. The banking code did exactly what it was designed to do; it was simply delivered to the wrong person.

The regulatory response has been slow and is only now arriving. On 22 April 2025 the British regulator Ofcom published a statement banning the leasing of Global Titles to third parties by operators that hold UK mobile numbers, banning third parties from creating or using Global Titles from sub-allocated numbers, and banning the creation and use of Global Titles from unallocated numbers. New leasing arrangements were prohibited immediately; existing arrangements had to end by 22 April 2026, with two specific uses excepted until 22 October 2026. Ofcom’s Natalie Black said that “leased Global Titles are one of the most significant and persistent sources of malicious signalling”, and industry commentators described the measure as the first of its kind by a national regulator. Note the dates: the interception technique was demonstrated publicly in the middle of the 2010s, exploited against real bank accounts in 2017, and the first serious supply-side restriction takes full effect in 2026.

The second structural answer is simpler and needs no signalling access at all: take over the number. In a SIM swap the attacker persuades the mobile operator that they are you and that your number should be moved to a SIM card in their possession. Your phone loses service and every code goes to them.

How hard is that? It was measured. In “An Empirical Study of Wireless Carrier Authentication for SIM Swaps”, presented at the Sixteenth Symposium on Usable Privacy and Security in 2020, Kevin Lee, Benjamin Kaiser, Jonathan Mayer and Arvind Narayanan of Princeton University opened prepaid accounts with five American carriers, AT&T, T-Mobile, Tracfone, US Mobile and Verizon Wireless, and then tried to SIM-swap their own accounts using only the kind of information an attacker would plausibly have. Running from May through July of 2019, they succeeded in 39 of 50 attempts. They also reverse-engineered the authentication policies of over 140 websites offering phone-based authentication and found 17 on which an account could be compromised on a SIM swap alone, including services where both the second factor and the password-reset path went to the same phone number, so no password was needed at all.

That study was cited by the American Federal Communications Commission when it finally acted. In Report and Order FCC 23-95, adopted on 15 November 2023 in WC Docket No. 21-341, the Commission amended its customer proprietary network information and number portability rules at 47 CFR 64.2010(h) and 47 CFR 52.37. Carriers must authenticate securely before executing a SIM change, and specifically must not rely on readily available biographical information, account information, recent payment information or call detail information. They must notify the customer immediately, before the change takes effect; offer a free account lock against SIM changes and port-outs; train staff on fraudulent requests; and keep three years of records of SIM change requests and the authentication measures used. The order was published at 88 FR 85794 on 8 December 2023 and took effect on 8 January 2024, except for the information-collection provisions, whose compliance date the Commission set at 8 July 2024 or approval under the Paperwork Reduction Act, whichever came later.

Reported SIM swap volumes in the United States, as complaints to the FBI’s Internet Crime Complaint Center, look like this. These are complaints filed with one reporting centre in one country, not a measure of how often the attack happens.

Year Complaints Reported losses
2023 1,075 48.8 million USD
2024 982 26.0 million USD
2025 971 17.4 million USD

The consequences are not confined to individuals. On 1 August 2018 Reddit disclosed a breach of its systems and wrote that “SMS-based authentication is not nearly as secure as we would hope, and the main attack was via SMS intercept”, after an attacker got past employees’ text-message second factors. How that attacker got there, what it costs, and where SIM swap sits among second factors generally are the next chapter’s subject.

What NIST actually said, in 2016 and what it says now#

There is a widely repeated claim that in 2016 the American standards body banned SMS for two-factor authentication. It did not, and the real story is more useful than the myth.

In July 2016 NIST released a public preview draft of Special Publication 800-63B. Section 5.1.3.2 of that draft carried a note saying that out-of-band authentication using the public switched telephone network, meaning SMS or voice, was deprecated and was being considered for removal in future editions of the guideline. Deprecated, in standards language, means still permitted but marked for removal. The technology press, including The Register on 24 July 2016, reported it as the end of SMS codes. The wording shifted slightly between the July preview and the August draft, but the substance was the same.

Then the final document, published in June 2017, dropped the word deprecated entirely. In its place NIST introduced a category called RESTRICTED, and put PSTN-delivered out-of-band authentication in it. That was not a retreat and it was not an endorsement; it was a different regulatory instrument. A restricted authenticator may be used, but the organization using it must do specific work to keep using it.

The current text is SP 800-63B revision 4, “Digital Identity Guidelines: Authentication and Authenticator Management”, published on 31 July 2025. Out-of-band devices are section 3.1.3, and section 3.1.3.3, “Authentication Using the Public Switched Telephone Network”, says that use of the PSTN for out-of-band verification is restricted and shall satisfy the requirements of section 3.2.9. It adds three things worth quoting in substance. Setting or changing the pre-registered telephone number is treated as the binding of a new authenticator, so it must go through the full binding process rather than a settings page. Verifiers should consider risk indicators, and it names them: device swap, SIM change, number porting and other abnormal behaviour, before using the PSTN to deliver a secret. And NIST states that it may adjust the restricted status of PSTN out-of-band authentication as the threat landscape evolves.

Section 3.2.9 is the part that gives the word restricted its force. Restricted authenticators, it says, remain necessary for some government-to-public applications, and at the time of publication there is exactly one: the PSTN used for out-of-band authentication. Accepting a restricted authenticator requires the implementing organization to assess, understand and accept the associated risks, and if the relying party determines the risk to any party is unacceptable, the restricted authenticator shall not be used and an alternative type shall be used instead.

The honest summary, as of August 2026: SMS codes are not banned by NIST and never were. They are the single named member of a category that requires a documented acceptance of risk and attention to SIM-change signals before each send. Experts genuinely disagree about the next step. One camp holds that SMS delivery should be retired outright, because its weaknesses are structural and cannot be patched at the application layer. The other holds that for large consumer populations it is the only second factor a majority will enable, so removing it moves those users to no second factor rather than a better one. Google’s 2019 figures, 100 per cent of automated bots and 96 per cent of bulk phishing stopped but only 76 per cent of targeted attacks, are quoted in support of both positions.

E-mail codes, voice codes, and the family beyond HOTP and TOTP#

An e-mail code has a threat model that is easy to state and uncomfortable to look at: it is exactly the security of the mailbox and nothing more. If the mailbox is protected by a password alone, a password on the site plus a code in the mailbox is not two factors, it is one factor asked twice. Worse, on most services the mailbox is also the password-reset channel, so an attacker who reaches it needs no code at all. There is a quieter problem too. Mail delivery is unpredictable, so services widen e-mail code validity to ten or fifteen minutes to reduce support tickets, right at or beyond the ten-minute limit NIST sets, and the code then sits readable in the mailbox for as long as the mailbox is kept. Anyone who has ever had access, or who quietly installed a forwarding rule, keeps receiving them.

A voice code is a text-to-speech reading of the same digits over a telephone call. It inherits every weakness of the SMS channel, since it uses the same numbering and the same signalling, and adds two of its own. The first is voicemail: if the call is not answered, many services leave the code on the answering service, and voicemail systems have historically been reachable from outside with a short personal identification number that many subscribers never changed from the network default. At DEF CON 26 in August 2018, Martin Vigo demonstrated exactly this chain in a talk called “Compromising online accounts by cracking voicemail systems”, brute-forcing voicemail access and then triggering services to call and read codes into it. The second is call forwarding, a network service that in some markets is set with less scrutiny than a SIM change requires, and which sends every voice code elsewhere while text messages keep arriving normally, so the victim sees nothing wrong.

Voice codes are not simply worse, and it is dishonest to present them that way. They are the only channel available to some users with visual impairments, to landline-only subscribers, and in places with poor text delivery. The correct engineering position is that they are a restricted channel that should be offered, configured never to leave a message on an unanswered call, and never used as the last remaining recovery path.

Set against those, the device-generated schemes look like this.

Channel Where the secret lives Chief structural risk
TOTP app Your phone and server Server seed file, phishing
Hardware fob Chip and server Vendor seed file, drift
SMS code Server, then network Signalling, SIM swap
E-mail code Server, then mailbox Mailbox is the same factor
Voice code Server, then network Voicemail, call forwarding

Beyond HOTP and TOTP the same family has one more member worth knowing. RFC 6287, “OCRA: OATH Challenge-Response Algorithm”, published in June 2011 by David M’Raihi, Johan Rydell, Siddharth Bajaj, Salah Machani and David Naccache, generalizes HOTP so that the input is not only a counter. An OCRA computation is described by a suite string of the form Algorithm, CryptoFunction, DataInput, for example OCRA-1:HOTP-SHA512-8:C-QN08-PSHA1, which means version 1, HMAC-SHA-512 truncated to eight digits, over a counter, an eight-digit numeric challenge and a SHA-1 hash of a PIN. The data input can also include session information and a timestamp.

That challenge field is more important than it looks, and it points at the limit of this whole chapter. If the server can put a value into the code’s input, it can put the details of the transaction there: the amount, the last four digits of the destination account. The device displays what it is about to authorize and produces a code valid only for that transaction, so a code obtained by a relaying attacker is useless for a different payment. This is transaction signing, sometimes called what-you-see-is-what-you-sign, and it is the first real answer to the attacker who passes your code along in real time. It is not the complete answer; binding a credential to the website it is talking to, rather than to the transaction, is the next chapter’s material.

Two proprietary variants are worth naming, and both are implementation details of single products rather than properties of the idea. RSA SecurID tokens do not run HOTP or TOTP; they use RSA’s own algorithm over a factory-loaded seed and a clock, refreshing at a fixed interval, conventionally 60 seconds. Yubico’s own OTP mode, distinct from the FIDO functions on the same hardware, emits a 44-character string in a keyboard-safe alphabet, of which the first 12 characters identify the device and the rest are an encrypted counter and random block. Neither validates against an RFC 4226 implementation.

Where does all of this leave a practitioner in August 2026. A device-generated code following RFC 6238 with a 160-bit seed, a narrow window, per-token drift correction, a stored last-used step and a real throttle is a sound, cheap, offline-capable second factor that defeats every attacker who is merely replaying something they recorded. It does not defeat the attacker standing in the middle at the moment you type, and no amount of care in the parameters will change that, because the code has no idea which website it is being typed into. That property is called origin binding and belongs to the credentials in the next chapter. A one-time code is a large improvement on a password, honestly earned, and a long way from safe.

17.98 Common wrong ideas#

Wrong: The six digits are secret, so guessing them is hopeless. Right: Six digits is one chance in a million per try, and RFC 4226’s own analysis gives the success probability as window times attempts divided by ten to the power of the digit count, so the security is carried entirely by the server’s throttle and the narrowness of its window.

Wrong: A time-based code is valid for thirty seconds. Right: Thirty seconds is the step size, but the lifetime is a policy set by the verifier, and nearly every deployment accepts the neighbouring steps too, so sixty to ninety seconds is normal; RFC 6238 section 6 works an example giving a maximum elapsed drift of around eighty-nine seconds.

Wrong: The server stores your authenticator secret hashed, the way it stores a password. Right: It cannot, because it must recompute your codes from the secret, so the seed is stored recoverably and the only real protection is encryption under a key held outside the database, exactly as RFC 4226 section 7.5 recommends.

Wrong: NIST banned SMS two-factor authentication in 2016. Right: The July 2016 preview draft of SP 800-63B marked telephone-network out-of-band authentication as deprecated, the final June 2017 document dropped that word and created the RESTRICTED category instead, and revision 4 of 31 July 2025 still permits it under section 3.1.3.3 provided the risk is documented and accepted under section 3.2.9.

Wrong: HOTP is broken because it uses SHA-1. Right: The 2017 SHAttered result broke SHA-1’s collision resistance, which HMAC does not depend on; HMAC-SHA-1 remains sound here, RFC 4226 Appendix B analyses exactly this question, and NIST’s intention to retire SHA-1 by 31 December 2030 is a migration schedule rather than a break.

Wrong: Scanning a QR code is a secure way to deliver the shared secret. Right: The QR code is only a picture of a line of text containing the seed in base32, so anyone who photographs the screen, receives a screenshot or finds it in a support ticket holds a credential that generates every future code.

Wrong: Dynamic truncation means taking the first or last six digits of the hash. Right: The low four bits of the twentieth byte select an offset between 0 and 15, four bytes are read from there, the top bit is cleared to keep the value positive on platforms without unsigned integers, and only then is the remainder taken and zero-padded to fixed width.

Wrong: A wider drift window is a harmless kindness to users with bad clocks. Right: The window sits in the numerator of the guessing formula, so accepting nine steps instead of three triples every attacker’s chance across every account at once; the fix is a narrow window plus the per-token drift offset RFC 6238 section 6 describes and almost nobody implements.

Wrong: A code sent to your e-mail address is a second factor. Right: It is exactly the security of the mailbox, and on most services the mailbox is also the password-reset channel, so an attacker who reaches it needs no code at all; a password plus an e-mail code is usually one factor asked twice.

Wrong: Turning on one-time codes makes an account phishing-proof. Right: A fake login page can take the code and hand it to the real site within its window, which is one use by the wrong person and breaks no rule in the specification; the fix is a credential that refuses to work on the wrong website, which is the next chapter’s subject.

17.99 Chapter summary in 20 lines#

  1. A one-time code proves knowledge of a shared secret without transmitting it, and stops working the moment it has been used, which turns a stolen secret into a stolen moment.
  2. Both sides hold the same secret, so the scheme is symmetric and the server can generate your codes as easily as it can check them.
  3. RFC 4226 of December 2005 defines HOTP as HMAC-SHA-1 over an 8-byte counter, dynamically truncated and reduced by remainder to at least six digits.
  4. The shared secret must be at least 128 bits by that specification, with 160 bits recommended, and both RFC 4226 and RFC 6238 are Informational documents rather than internet standards.
  5. Dynamic truncation reads the low four bits of the last byte as an offset, takes the four bytes there, clears the top bit and takes the remainder on division by ten to the power of the digit count.
  6. Worked on the specification’s own test secret, counter 0 gives HMAC cc93cf18508d94934c64b65d8ba7667fb7cde4b0, offset 0, value 1284755224 and the code 755224, matching Appendix D exactly.
  7. RFC 6238 of May 2011 replaces the counter with the number of complete time steps since an epoch, defaulting to a step of 30 seconds and an epoch of the start of 1970.
  8. The 30-second step is documented as a deliberate balance between security and usability, not a physical constant, and it is a parameter many authenticator apps silently ignore.
  9. Errata ID 2866 against RFC 6238 records that the SHA-256 and SHA-512 test vectors use 32-byte and 64-byte seeds, without which those rows cannot be reproduced.
  10. At Unix time 1,787,045,400 the time step is 59,568,180 and the test secret yields 766139, with 105616 before it and 318835 after it and no relationship between them.
  11. Provisioning is carried by the otpauth link, whose only real specification is a wiki page, with two individual Internet-Drafts and no RFC as of August 2026.
  12. Base32 from RFC 4648 encodes the seed because its alphabet omits the confusable characters, is case-insensitive, and turns twenty bytes into exactly thirty-two characters with no padding.
  13. The QR code carries no security whatever; it is a picture of the link, and possession of it is possession of the credential.
  14. HOTP drifts because buttons get pressed, and is repaired by a look-ahead window whose width multiplies every attacker’s chance of guessing.
  15. TOTP drifts because clocks disagree, and the correct repair is a narrow window plus a recorded per-token offset, which RFC 6238 section 6 sets out.
  16. Replay prevention requires storing the highest accepted step or counter per credential and refusing anything at or below it, a rule RFC 6238 makes mandatory and implementations routinely omit.
  17. NIST SP 800-63B revision 4 of 31 July 2025 requires out-of-band secrets of at least six digits, valid for no more than ten minutes and accepted only once.
  18. Codes sent over the telephone network are exposed to signalling interception, demonstrated against German bank accounts in 2017, and to SIM swap, which succeeded in 39 of 50 controlled attempts in a 2020 Princeton study.
  19. Regulators have moved slowly: the FCC adopted SIM-swap rules as FCC 23-95 on 15 November 2023, and Ofcom’s ban on leasing Global Titles, stated on 22 April 2025, only bites on existing leases from 22 April 2026.
  20. A one-time code defeats every attacker who merely recorded something, and defeats none who is relaying it live, because the code has no way to know which website it is being typed into.

Chapter sources: RFC 4226, “HOTP: An HMAC-Based One-Time Password Algorithm”, M’Raihi, Bellare, Hoornaert, Naccache and Ranen, Informational, December 2005, especially section 4 requirements R1 to R6, section 5.3 dynamic truncation, section 5.4’s worked example, sections 7.3 to 7.5, Appendix A.4.2, Appendix B, Appendix D test values and Appendix E.1; RFC 6238, “TOTP: Time-Based One-Time Password Algorithm”, M’Raihi, Machani, Pei and Rydell, Informational, May 2011, sections 4, 5.1, 5.2 and 6 with the Appendix B test vectors, and RFC Errata ID 2866, Verified, on the SHA-256 and SHA-512 seed lengths; RFC 6287, “OCRA: OATH Challenge-Response Algorithm”, June 2011; RFC 2104, February 1997, and FIPS 198-1, July 2008, on HMAC; RFC 4648, October 2006, section 6, on base32; RFC 1760, February 1995, and RFC 2289, February 1998, on S/KEY-style one-time passwords; Leslie Lamport, “Password Authentication with Insecure Communication”, Communications of the ACM, volume 24, number 11, November 1981; NIST Special Publication 800-63B revision 4, “Digital Identity Guidelines: Authentication and Authenticator Management”, 31 July 2025, sections 3.1.2, 3.1.3, 3.1.3.3, 3.1.4 and 3.2.9, with the July 2016 preview draft section 5.1.3.2 and the final June 2017 edition, and NIST’s December 2022 announcement retiring SHA-1 by 31 December 2030; Stevens, Bursztein, Karpman, Albertini and Markov, “The first collision for full SHA-1”, CWI Amsterdam and Google, 23 February 2017; the Key Uri Format page of the google-authenticator project, with Internet-Drafts draft-linuxgemini-otpauth-uri-02 of 17 February 2025 and draft-andesco-otpauth-uri-00 of 19 February 2026, neither having IETF standing; ISO/IEC 18004 for QR codes, originated at Denso Wave in 1994; the Initiative for Open Authentication, announced 24 February 2004 at RSA Conference 2004; the open letter to RSA customers from Art Coviello of 17 March 2011, the June 2011 token replacement offer and EMC’s reported 66 million United States dollar second-quarter charge; Sueddeutsche Zeitung’s 2017 reporting of SS7 interception of German mobile transaction authentication numbers, confirmed by O2-Telefonica; Ofcom, “Global Titles and Mobile Network Security”, statement of 22 April 2025, with existing leases ending 22 April 2026 and two excepted uses ending 22 October 2026; Lee, Kaiser, Mayer and Narayanan, “An Empirical Study of Wireless Carrier Authentication for SIM Swaps”, SOUPS 2020, experiments run May to July 2019; FCC Report and Order FCC 23-95, “Protecting Consumers from SIM Swap and Port-Out Fraud”, WC Docket No. 21-341, adopted 15 November 2023, published at 88 FR 85794 on 8 December 2023, effective 8 January 2024, amending 47 CFR 64.2010(h) and 47 CFR 52.37; FBI Internet Crime Complaint Center annual reports for 2024 and 2025; Reddit’s security incident announcement of 1 August 2018; Thomas and Moscicki, “New research: How effective is basic account hygiene at preventing hijacking”, Google Security Blog, 17 May 2019; Martin Vigo, “Compromising online accounts by cracking voicemail systems”, DEF CON 26, August 2018.