Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 11 – Tokens, OTPs and passkeys

HOTP and TOTP: one-time passwords done right

Compute HOTP and TOTP codes against the RFC test vectors, accept a clock window without allowing reuse, and keep OTP secrets and recovery codes safe.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free intro, Pro: 3 of 20 minutes free
Free: HOTP: an HMAC of a counter, cut down to six digits. With a pass: the other 8 sections and 3 interview answers, about 17 minutes.

What you will learn

  • Compute HOTP and TOTP codes and check them against the test vectors of RFC 4226 and RFC 6238
  • Verify a code inside a small time window and refuse a second use of the same step
  • Store OTP secrets and recovery codes so that a copy of the database does not hand out working codes
  • Explain why a one-time password can be relayed by a phishing page, and what to use against that

Before you start

What you’ll learn · Before you start

What you will learn

  • Compute HOTP and TOTP codes and check them against the test vectors of RFC 4226 and RFC 6238
  • Verify a code inside a small time window and refuse a second use of the same step
  • Store OTP secrets and recovery codes so that a copy of the database does not hand out working codes
  • Explain why a one-time password can be relayed by a phishing page, and what to use against that

Before you start

On this page

A six-digit code has only 1,000,000 values, so a careless login can be guessed, or can accept the same code twice. An app and a server both compute the code from a key they share, so the key itself never crosses the network at login, only a short code. HOTP: HMAC-based one-time password (RFC 4226). A short code worked out from a shared key and a counter that goes up with each use, by an HMAC-SHA-1 and a dynamic truncation., defined in RFC 4226, computes it from a counter that goes up with each use. TOTP: Time-based one-time password (RFC 6238). HOTP with the counter replaced by the number of time steps since 1970, 30 seconds each by default, so the code changes by itself., defined in RFC 6238, feeds the same function the number of 30-second steps since 1970, so the code changes by itself twice a minute.

Done right means exact arithmetic that passes the RFC test vectors, a small clock window, each code accepted once, a cap on wrong guesses, a key that is encrypted at rest and recovery codes that are stored as hashes. Even then a code is not phishing-resistant, because a look-alike page can pass a fresh code to the real site while it is valid.

HOTP: an HMAC of a counter, cut down to six digits

HOTP starts from a key that the app and the server both hold, 20 random bytes in the RFC’s examples, and a counter that both keep. The counter is written as 8 bytes, most significant first, and the key and the counter go through HMAC-SHA-1. The 20-byte result is too long to type, so RFC 4226 section 5.3 cuts it down in a step called dynamic truncation: The step of HOTP that cuts a 20-byte HMAC down to digits. The low 4 bits of the last byte give an offset, the 4 bytes there become a 31-bit number, and the code is that number modulo 10^6.. The low 4 bits of the last byte give an offset, the 4 bytes at that offset become a number of 31 bits, and the code is that number modulo 10^6 for six digits. The RFC requires at least six digits and allows seven or eight. Step through one code:

Interactive · stage by stage How one HOTP code is made Step through five stages with the numbers of counter 0 in RFC 4226, Appendix D. The key is the 20 ASCII characters 12345678901234567890.
How one HOTP code is made, step 5 of 5. Showing Write the counter, HMAC-SHA-1, Pick an offset, Take 4 bytes, Keep the last digits. In focus: Keep the last digits.Write the counter8 bytes, big-endianHMAC-SHA-120 bytes under the keyPick an offsetlow 4 bits of the last byteTake 4 bytestop bit cleared: 31 bitsKeep the last digitsnumber mod 10^6

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The counter. The first code uses counter 0. It is written as 8 bytes, most significant byte first, so counter 0 is eight zero bytes and counter 1 ends in 01.
  2. HMAC-SHA-1. The key and the counter go through HMAC-SHA-1. For counter 0 the result is cc93cf18 and so on to cde4b0: 20 bytes, far too long to type, so it is cut down.
  3. The offset. The last byte is 0xb0. Its low 4 bits are 0, so the offset is 0. The offset can be any number from 0 to 15, so the 4 bytes taken are not always the same ones.
  4. Four bytes, 31 bits. The 4 bytes at the offset are cc 93 cf 18. Clearing the top bit of the first (0xcc becomes 0x4c) leaves the 31-bit number 4c93cf18, which is 1284755224 and positive in every language.
  5. The last digits. The code is the number modulo 10^6, with leading zeros if needed: 1284755224 mod 1,000,000 = 755224, the first value in RFC 4226's table. Eight digits would give 84755224.

The program computes the ten codes of RFC 4226 Appendix D, whose key is the 20 characters 12345678901234567890, and sets them beside the RFC’s table:

HOTP for counters 0 to 9, against RFC 4226 Appendix D JavaScript · hotp-js/hotp.mjs
// HOTP of RFC 4226: an HMAC-SHA-1 of an 8-byte counter, cut down to a few decimal digits. The program computes the
// codes of Appendix D (secret "12345678901234567890", counters 0 to 9) and compares them with the RFC's table.
import { createHmac } from 'node:crypto';

const secret = Buffer.from('12345678901234567890', 'ascii'); // 20 bytes = 160 bits, the length RFC 4226 recommends

// The counter is an 8-byte big-endian integer.
function counterBytes(counter) {
  const bytes = Buffer.alloc(8);
  bytes.writeBigUInt64BE(BigInt(counter));
  return bytes;
}

// Dynamic truncation (RFC 4226 section 5.3): the low 4 bits of the last byte are an offset into the 20 bytes; the 4 bytes
// there are read as a number with the top bit cleared (31 bits); the code is that number modulo 10^digits.
function truncate(mac, digits) {
  const offset = mac[mac.length - 1] & 0x0f;
  const number = mac.readUInt32BE(offset) & 0x7fffffff;
  return { offset, number, code: String(number % 10 ** digits).padStart(digits, '0') };
}

const mac = (counter) => createHmac('sha1', secret).update(counterBytes(counter)).digest();
const hotp = (counter, digits = 6) => truncate(mac(counter), digits).code;

// One code, step by step.
const first = mac(0);
const t = truncate(first, 6);
console.log(`secret         "12345678901234567890", ${secret.length} bytes (${secret.length * 8} bits)`);
console.log(`counter        ${counterBytes(0).toString('hex')}   (8 bytes, big-endian)`);
console.log(`HMAC-SHA-1     ${first.toString('hex')}   (${first.length} bytes)`);
console.log(`offset         ${t.offset}   (the low 4 bits of the last byte, 0x${first.at(-1).toString(16)})`);
console.log(`31-bit number  ${t.number.toString(16)} = ${t.number}   (the first byte 0x${first[t.offset].toString(16)} with its top bit cleared is 0x${(first[t.offset] & 0x7f).toString(16)})`);
console.log(`6 digits       ${t.code}   (${t.number} mod 1,000,000)`);
console.log(`8 digits       ${hotp(0, 8)}   (the same number mod 100,000,000)`);
console.log('');

// RFC 4226 Appendix D, Table 2: the HOTP value for each count.
const published = ['755224', '287082', '359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
console.log('counter  code    RFC 4226 table  same');
let all = true;
published.forEach((expected, counter) => {
  const code = hotp(counter);
  all &&= code === expected;
  console.log(`${String(counter).padEnd(8)} ${code}  ${expected}          ${code === expected ? 'yes' : 'NO'}`);
});
console.log(`all ten codes equal the RFC's table: ${all}`);
console.log('');

// RFC 4226 section 5.4 gives the 20 bytes of one HMAC and works the truncation through by hand.
const sample = Buffer.from('1f8698690e02ca16618550ef7f19da8e945b555a', 'hex');
console.log(`section 5.4 sample: offset ${truncate(sample, 6).offset}, 4 bytes ${sample.subarray(10, 14).toString('hex')}, code ${truncate(sample, 6).code}   (the RFC says 872921)`);

Output

secret         "12345678901234567890", 20 bytes (160 bits)
counter        0000000000000000   (8 bytes, big-endian)
HMAC-SHA-1     cc93cf18508d94934c64b65d8ba7667fb7cde4b0   (20 bytes)
offset         0   (the low 4 bits of the last byte, 0xb0)
31-bit number  4c93cf18 = 1284755224   (the first byte 0xcc with its top bit cleared is 0x4c)
6 digits       755224   (1284755224 mod 1,000,000)
8 digits       84755224   (the same number mod 100,000,000)

counter  code    RFC 4226 table  same
0        755224  755224          yes
1        287082  287082          yes
2        359152  359152          yes
3        969429  969429          yes
4        338314  338314          yes
5        254676  254676          yes
6        287922  287922          yes
7        162583  162583          yes
8        399871  399871          yes
9        520489  520489          yes
all ten codes equal the RFC's table: true

section 5.4 sample: offset 10, 4 bytes 50ef7f19, code 872921   (the RFC says 872921)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hotp.mjs

All ten codes equal the RFC’s, and so does the hand-worked sample of section 5.4, which ends in the offset 10 and the code 872921. The app’s counter goes up each time a code is shown, whether or not the code is sent, so the server keeps its own counter and accepts a few values ahead of it, and RFC 4226 section 7.4 asks for that look-ahead to be as small as usability allows. TOTP calls this same function and changes only the counter that it passes in.

Checkpoint

Check: How an HOTP code is made

  1. Question 1 of 2 Which part of the 20-byte HMAC decides where the four bytes are taken from?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The low 4 bits of its last byte

    For counter 0 the last byte is 0xb0, so the offset is 0 and the four bytes are cc 93 cf 18. The example in section 5.4 of the RFC ends in 0x5a, so its offset is 10.

  2. Question 2 of 2 Why is the top bit of the first of the four bytes cleared?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So the result is a 31-bit number that every language reads the same way

    0xcc becomes 0x4c, so the number is 4c93cf18 = 1284755224 in any language, and 1284755224 mod 1,000,000 is 755224.

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does HOTP turn a key and a counter into a six-digit code?

    HMAC-SHA-1 of the shared key (20 random bytes in the RFC) and the counter written as 8 bytes, most significant first. Dynamic truncation (RFC 4226 section 5.3) shortens the 20-byte result: the last 4 bits of its final byte are an offset, the 4 bytes starting there form a 31-bit number, which is reduced modulo 10^6 to give the code. The RFC requires at least six digits and allows seven or eight.

  2. How do HOTP and TOTP differ?

    HOTP (RFC 4226) computes the code from a counter that rises with every use. TOTP (RFC 6238) feeds the same function a count of 30-second steps since 1970, so the code changes by itself twice a minute. In both, the shared key never crosses the network at login, only a short code.

  3. Which values check an HOTP implementation?

    RFC 4226 Appendix D: the 20-character key 12345678901234567890 and the ten codes for counters 0 to 9, all of which matched. The hand-worked sample of section 5.4 ends in offset 10 and the code 872921.

  4. Why does an HOTP server accept a few counter values ahead?

    The app's counter goes up each time a code is shown, whether or not the code is sent, so the server keeps its own counter and accepts a few values ahead of it. RFC 4226 section 7.4 asks for that look-ahead to be as small as usability allows.

  5. What does 'done right' mean for one-time passwords, and what do they still not stop?

    Exact arithmetic that passes the RFC test vectors, a small clock window, each code accepted once, a cap on wrong guesses, a key encrypted at rest and recovery codes stored as hashes. Even then a code is not phishing-resistant: a look-alike page can pass a fresh code to the real site while it is valid.

TOTP: the same function with the clock as the counter Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Verifying a code: a window, one use and a limit on guesses
    • Verify a time-based one-time password
  • Secrets: how to make, show and store them
  • Recovery codes: the way back in must not be the weak spot
  • Why a one-time password can be relayed, and what to use instead
  • Practise: compute, verify, store and refuse
  • Interview questions
    • How do HOTP and TOTP differ, and why does the server need the secret itself?
    • A review of your TOTP login finds that it accepts ten steps either side, keeps no record of used codes, takes the time from the request and never stops anyone guessing. Explain what an attacker gains and rewrite the checks.
    • A copy of your user database leaks. It holds password hashes, TOTP secrets and recovery-code hashes. Say what each one gives the finder, what you do in the first day, and what you change so that the next leak is smaller.
    • Users who have TOTP switched on are still losing accounts to fake sign-in pages. Explain how that is possible with a correct, single-use verifier, and what you ship first, next and last.
  • Key takeaways

2 diagrams · 4 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 46 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

Base32
An encoding of bytes in the 32 characters A to Z and 2 to 7 (RFC 4648). It is case-insensitive, so it suits secrets that people read and type: 20 bytes make 32 characters.
dynamic truncation
The step of HOTP that cuts a 20-byte HMAC down to digits. The low 4 bits of the last byte give an offset, the 4 bytes there become a 31-bit number, and the code is that number modulo 10^6.
HOTP
HMAC-based one-time password (RFC 4226). A short code worked out from a shared key and a counter that goes up with each use, by an HMAC-SHA-1 and a dynamic truncation.
otpauth link
A link that begins otpauth:// and carries an OTP secret, an account name and settings to an authenticator app, usually inside a QR code.
recovery code
A single-use code that a person keeps offline and uses to get back into an account when the authenticator is lost. SP 800-63B-4 counts it as a look-up secret.
time step
The length of time one TOTP code lasts, 30 seconds by default. The step number is the Unix time divided by the step length, rounded down.
TOTP
Time-based one-time password (RFC 6238). HOTP with the counter replaced by the number of time steps since 1970, 30 seconds each by default, so the code changes by itself.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.