
How TOTP Works: Shared Secrets, 30-Second Codes, and Clock Drift
An authenticator can calculate a short code from a secret shared with a server and the current time. That does not mean every clock is perfectly synchronized or every code has six digits and a 30-second period. This guide separates the conditions needed to calculate matching codes from the verifier's responsibilities, including replay prevention and rate limiting, then compares TOTP with passkeys.
Japanese original published: 2026-04-30
When two devices can calculate the same code offline
TOTP is deterministic: the same secret key, algorithm, number of digits, time-step length, starting point, and time step produce the same value. The clocks do not synchronize themselves through TOTP. Six digits and a 30-second step are common settings, but they are not the only possible settings.
Enrollment can use a QR code or a manually entered key. After enrollment, code generation itself can work offline. Sending a login request, correcting a device's clock, and backing up or synchronizing an authenticator may still require communication. Treat the enrollment QR code and setup key as credentials; do not publish or share them.
From HOTP to TOTP: replace the event counter with time
HOTP, described in RFC 4226 (2005), uses HMAC-SHA-1 with a shared key K and an eight-byte counter C, then converts the result into a short decimal code. The generator advances its counter when it generates a new code; the verifier updates its state to follow an accepted code. Generating unused codes can therefore move the two sides out of step. There is no single universally appropriate look-ahead window.
TOTP, described in RFC 6238 (2011), replaces the event counter with an integer derived from time. HMAC-SHA-256 and HMAC-SHA-512 are also options. Both RFCs are Informational documents; neither defines every operational requirement of an authentication service.
T = floor((unixSeconds - T0) / X)
T0 = 0 // Default starting point: the Unix epoch
X = 30 // Default time-step length in seconds
TOTP = HOTP-style calculation(K, T)At 12:00:00 UTC on April 30, 2026, Unix time is 1777550400 and T is 59251680. Two times in the same step give the same input; they need not be the same second. Use Unix seconds, not a local time-zone display. Even across a step boundary, the displayed code can occasionally repeat because the calculation maps many inputs into a short numeric output.
Dynamic truncation and leading zeros
Dynamic truncation selects a position in the HMAC output and extracts a nonnegative 31-bit integer. Taking that integer modulo 10 raised to the number of digits gives the code. Six digits cover 000000 through 999999. Preserve leading zeros instead of displaying the result as an ordinary integer. SHA-1 produces 20 bytes; SHA-256 and SHA-512 have different output lengths.
The following learning example runs as CommonJS in Node.js. It fixes T0 at 0 and X at 30, and accepts decoded key bytes, not a Base32 string. It does not implement secure key issuance or storage, rate limiting, replay rejection, enrollment, or recovery. This function alone is not a login authentication system.
const { createHmac } = require("node:crypto");
// Learning and test-vector checks only, not an authentication server.
function totp(key, unixSeconds, algorithm = "sha1", digits = 6) {
if (!Buffer.isBuffer(key) || key.length < 16) throw new Error("Invalid key");
if (!Number.isSafeInteger(unixSeconds) || unixSeconds < 0) throw new Error("Invalid time");
if (!["sha1", "sha256", "sha512"].includes(algorithm)) throw new Error("Invalid algorithm");
if (![6, 8].includes(digits)) throw new Error("Invalid digits");
const step = BigInt(Math.floor(unixSeconds / 30)); // T0 = 0, X = 30
const message = Buffer.alloc(8);
message.writeBigUInt64BE(step); // Eight bytes, big-endian
const digest = createHmac(algorithm, key).update(message).digest();
const offset = digest[digest.length - 1] & 15;
const value = digest.readUInt32BE(offset) & 0x7fffffff;
return String(value % (10 ** digits)).padStart(digits, "0");
}
const testKey = Buffer.from("12345678901234567890", "ascii"); // Public test key only
console.log(totp(testKey, 59, "sha1", 8)); // 94287082With this public test key and Unix time 59, the eight-digit code is 94287082 and the six-digit code is 287082. Never use this published key for a real account. Guessing one accepted six-digit candidate in one attempt has a probability of roughly one in a million, with a small bias introduced by the modulo operation. More accepted candidates and more attempts increase the risk. This is a guessing probability, not a general collision probability.
Separate clock drift, network delay, and replay prevention
RFC 6238 section 5.2 recommends allowing at most one time step for network delay. It does not require every verifier to accept the current step plus both neighbors. Clock drift is addressed separately in section 6; the verifier decides how far backward and forward to search.
As an illustration, suppose a verifier searches the current step and one step on either side. With X = 30 and server time 60 seconds, T = 2, so it checks steps 1, 2, and 3. Their generation times span 30 seconds inclusive to 120 seconds exclusive. That does not establish a uniform clock tolerance of ±30 seconds or make a submitted code valid for another 90 seconds. Position within a step, delay, and any recorded drift matter.
After an OTP has successfully authenticated a request, the verifier must not accept it again within its validity period. Concurrent requests must not both consume the same OTP: validation and the used-state update need atomic handling. A numeric match alone is not sufficient.
Short OTPs also require rate limiting. Do not present a fixed three-to-five-attempt lockout as a universal rule from the standard. Account recovery, usability, and the threat model affect the policy. A large clock discrepancy calls for resynchronization or enrollment through another trusted authentication method.
QR codes and otpauth URIs: Base32 is not encryption
Google's Key URI Format is a widely used way to transfer authenticator settings; it is not an enrollment protocol defined by RFC 6238 itself. This single-line example contains a public test key. Do not enroll it for a real account, and do not insert comments or line breaks into an actual URI.
otpauth://totp/Example:alice%40example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Example&algorithm=SHA1&digits=6&period=30The secret parameter contains the Base32-encoded key; issuer identifies the provider, while algorithm, digits, and period specify calculation settings. Base32 is an encoding, not encryption. Character count is not the same as key length in bits: these 32 characters decode to 20 bytes, or 160 bits. Being a public test key, it provides no secrecy.
RFC 4226 requires at least 128 bits of shared secret and recommends 160 bits. RFC 6238 recommends matching the HMAC output length. Generate keys with cryptographically secure randomness and assign a unique key to each prover. Do not equate the key-and-algorithm security-strength requirements in NIST SP 800-63B-4 with a text string's length or with the RFC's key-length requirement.
The Key URI document contains older notes about implementations ignoring some parameters. Those notes are not a compatibility matrix for every current app version. Test the imported algorithm, digit count, and period against known values in the specific app and version you intend to use.
Passkeys and authentication assurance levels
Passkeys are public-key credentials. WebAuthn is a W3C specification, while CTAP is specified by the FIDO Alliance. The server checks signatures with a public key rather than sharing a secret with the authenticator. RP ID and origin checks bind use of the credential to the appropriate site, unlike a manually entered TOTP code.
A device PIN or biometric check can authorize credential use. Passkeys can be synced or device-bound; not every passkey is synchronized. Phishing resistance does not promise complete protection against a compromised device, session theft, or a weak recovery path.
A NIST Authentication Assurance Level (AAL) does not follow from an algorithm name alone. The factors used with an OTP and the verifier's controls affect whether AAL1 or AAL2 requirements are met. AAL2 also requires a phishing-resistant option to be offered. Syncable authenticators cannot be used at AAL3. A device-bound key does not automatically qualify either: non-exportability, hardware protection, and the applicable verifier requirements still matter.
Check the library contract and the surrounding service
When using a library such as otpauth, speakeasy, PyOTP, pquerna/otp, totp-rs, or OTPHP, check the maintenance status and documented behavior of the version you adopt. Do not assume that every library is fully compliant or that their only differences are acceptance windows.
- Check RFC test vectors, leading zeros, 30-second boundaries, and times beyond 2038.
- Verify key representation and length, digit count, time step, and hash compatibility.
- Design rate limiting, used-code rejection, concurrent-request handling, secret storage, and recovery separately.
The sample on this page is for learning. A general password-string generator does not establish the byte length or Base32 representation of a TOTP key. Base64 and Base32 are different encodings; the Base64 converter is not a TOTP enrollment-key validator. Never paste real enrollment QR codes or keys into public verification forms, shared chats, or logs.
Verify the calculation with known values
These values use the public SHA-1 test key from RFC 6238 Appendix B: the ASCII bytes of 12345678901234567890, with T0 = 0, X = 30, and eight digits. Keep the output as eight characters, including a leading zero.
Scroll horizontally if the table does not fit.
| Unix seconds | T | Eight-digit code |
|---|---|---|
| 59 | 1 | 94287082 |
| 1111111109 | 37037036 | 07081804 |
| 20000000000 | 666666666 | 65353130 |
Source: RFC 6238 Appendix B. Matching these values checks the calculation, not the security of an entire production system. Use the corresponding RFC test keys when checking SHA-256 and SHA-512.
Keep calculation settings separate from verification policy
- Matching keys, settings, and time steps are what make codes match.
- Six digits and 30 seconds are common choices, not the only possible settings.
- Offline code generation does not imply offline login or synchronization.
- Secure key issuance and storage, rate limiting, and replay rejection are separate from the calculation function.
- The key differences between TOTP and passkeys concern how credentials are held and how authentication is bound to its destination.
References and sources
Editorial note
This article was prepared with AI assistance and reviewed by an editor before publication. It may still contain factual errors, interpretation mistakes, or outdated information. Check the cited primary sources or official documentation before making an important decision.

