TOTP: Those 6-Digit Codes Aren't Magic
Every 30 seconds, your authenticator app generates a new 6-digit code. No internet connection needed. No server communication. How does the server know what code you're seeing? Because both sides are running the same algorithm with the same secret.
The Algorithm
TOTP (Time-Based One-Time Password, RFC 6238) works in 4 steps:
- Get current time as Unix timestamp
- Divide by time step (30 seconds by default) → this gives a counter value
- HMAC-SHA1 the counter with the shared secret → produces a 20-byte hash
- Truncate the hash to a 6-digit number
// Pseudocode
time_step = floor(unix_timestamp / 30)
hmac = HMAC-SHA1(secret, time_step)
offset = hmac[19] & 0x0f
code = (hmac[offset..offset+3] & 0x7fffffff) % 1000000
// Result: 6-digit code like "482901"Why It Works Without Internet
During setup, the server generates a random secret and shares it with your authenticator (via QR code). Both sides now have:
- The same secret key (typically 20 bytes, Base32-encoded)
- The same algorithm (HMAC-SHA1)
- The same time step (30 seconds)
- Synchronized clocks (within reasonable tolerance)
The server doesn't need to contact your phone. It just runs the same calculation with the same secret and checks if the codes match.
The QR Code
When you scan a TOTP QR code, it contains a URI like:
otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&algorithm=SHA1&digits=6&period=30| Parameter | Value | Meaning |
|---|---|---|
secret | JBSWY3DPEHPK3PXP | Base32-encoded shared secret |
issuer | MyApp | Service name shown in authenticator |
algorithm | SHA1 | HMAC algorithm (SHA1/SHA256/SHA512) |
digits | 6 | Code length (6 or 8) |
period | 30 | Time step in seconds |
Implementation in JavaScript
// Node.js TOTP verification
import { createHmac } from 'crypto';
function generateTOTP(secret: Buffer, timeStep = 30): string {
const counter = Math.floor(Date.now() / 1000 / timeStep);
// Convert counter to 8-byte big-endian buffer
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigInt64BE(BigInt(counter));
// HMAC-SHA1
const hmac = createHmac('sha1', secret).update(counterBuffer).digest();
// Dynamic truncation
const offset = hmac[hmac.length - 1] & 0x0f;
const code = (
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff)
) % 1000000;
return code.toString().padStart(6, '0');
}
// Verify with ±1 window for clock drift
function verifyTOTP(secret: Buffer, token: string): boolean {
for (let i = -1; i <= 1; i++) {
const adjustedTime = Date.now() + i * 30000;
// ... generate code at adjusted time
if (generatedCode === token) return true;
}
return false;
}Python Implementation
import hmac
import hashlib
import struct
import time
import base64
def generate_totp(secret_b32: str, time_step: int = 30) -> str:
secret = base64.b32decode(secret_b32)
counter = int(time.time()) // time_step
counter_bytes = struct.pack('>Q', counter)
hmac_hash = hmac.new(secret, counter_bytes, hashlib.sha1).digest()
offset = hmac_hash[-1] & 0x0f
code = (
struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7fffffff
) % 1000000
return f"{code:06d}"
# Example
print(generate_totp('JBSWY3DPEHPK3PXP'))
# "482901" (changes every 30 seconds)Security Considerations
- Store secrets encrypted. The TOTP secret in your database is equivalent to a password. Encrypt it at rest.
- Provide backup codes. Users lose phones. Generate 8-10 single-use backup codes during setup.
- Allow clock drift. Accept codes from the previous and next time step (±30 seconds).
- Rate limit verification. Without rate limiting, an attacker has a 1/1,000,000 chance per attempt — brute-forceable at high speed.
- Don't use SMS. TOTP (authenticator app) is more secure than SMS-based 2FA, which is vulnerable to SIM swapping.
TOTP vs HOTP
| Feature | TOTP (Time-Based) | HOTP (Counter-Based) |
|---|---|---|
| Counter | Current time / 30 | Incrementing integer |
| Expiry | Codes expire after 30s | Codes never expire |
| Sync issues | Clock drift | Counter desync |
| Security | Better (codes expire) | Less secure (replay possible) |
| Used by | Google Authenticator, Authy | YubiKey (in HOTP mode), some hardware tokens |
Generate TOTP codes: TOTP Generator — enter a secret key and see the live code with countdown timer.