Multi-factor authentication (MFA) forms the baseline defense against credential stuffing, phishing, and session hijacking across modern distributed systems. Among secondary authentication mechanisms, Time-Based One-Time Passwords (TOTP) represent one of the most widely deployed open standards. Specified by the Internet Engineering Task Force (IETF) in RFC 6238, TOTP governs the rotating six-digit and eight-digit numerical codes produced by authenticator applications worldwide.
Unlike proprietary push notifications or Short Message Service (SMS) verification codes, TOTP operates entirely decoupled from immediate network transport. The user agent and the authentication server calculate identical authentication tokens independently, synchronized solely by elapsed Unix time and a pre-shared cryptographic key. Understanding the cryptographic foundation of TOTP requires dissecting its underlying primitives: the transformation of continuous time into discrete steps, the internal structure of Hash-based Message Authentication Codes (HMAC), the dynamic truncation algorithm defined in RFC 4226, and the practical security trade-offs governing clock drift, lookup windows, and local browser secret parsing.
For practitioners validating token generation workflows directly, utilizing an in-browser TOTP code generator allows direct verification of secret key behavior with privacy-preserving web tools. When provisioning accounts, systems often serialize these keys into matrix barcodes, making it essential to extract TOTP secrets with our QR decoder to audit seed formats and URI query parameters.
1. Foundations of One-Time Authentication: From HOTP (RFC 4226) to TOTP (RFC 6238)
To understand TOTP, one must first examine its direct ancestor: the HMAC-Based One-Time Password algorithm (HOTP), published by the IETF in RFC 4226. HOTP was designed around an incremental event counter. In an HOTP transaction, both the client token generator and the validation server share a secret key and maintain an internal 64-bit counter variable .
Every time an authentication code is requested by the client, the local counter increments by 1 (), and a new cryptographic digest is computed. When the user submits the resulting code, the verification server computes the expected output using its stored counter . If the values match, the server increments and grants access.
The Inherent Vulnerabilities of Event-Based Counters
While mathematically sound, HOTP introduces substantial operational friction into distributed systems:
- Counter Desynchronization: If an end user activates the hardware token or taps an authenticator interface without submitting the generated code, the client counter advances while the server counter remains stationary. RFC 4226 addresses this by requiring the server to implement a look-ahead window parameter (). The server must compute and compare values across . If is too small, users who trigger tokens accidentally become locked out. If is too large, the brute-force attack surface expands linearly.
- State Storage Overhead: The verifying server must maintain mutable, synchronized counter state across distributed database replicas. High-concurrency authentication clusters require transactional row locking to prevent race conditions and token replay attempts.
The RFC 6238 Time-Step Transformation
RFC 6238 resolves the desynchronization problem by replacing the incrementing event counter with a monotonically increasing integer derived directly from physical time. The continuous physical time coordinate is discretized into discrete time steps using floor division.
The discrete counter is formally computed as:
Where:
- represents the current elapsed seconds since the Unix Epoch (1970-01-01T00:00:00Z UTC), ignoring leap seconds according to POSIX conventions.
- represents the initial epoch time offset, standardized as 0.
- defines the time step window duration in seconds, set by convention and specification to 30 seconds (though implementations occasionally support 60-second intervals).
Physical Timeline (Continuous UTC Seconds)
----------------------------------------------------------------------->
[ 1730548800 s ] [ 1730548830 s ] [ 1730548860 s ]
| | |
+--- Step Window T = 57684960 +--- Step Window T = 57684961 +--- Step T = 57684962
| | |
v v v
[ HMAC Compute: C=57684960 ] [ HMAC Compute: C=57684961 ] [ HMAC Compute: C=... ]
[ Dynamic Truncation ] [ Dynamic Truncation ] [ Dynamic Truncation ]
v v v
Token: "847291" Token: "392014" Token: "105928"Because time advances automatically on both client hardware and verification clusters, the client and server remain inherently synchronized without exchanging state. The counter serves as an 8-byte input parameter to the core HMAC function, bridging the gap between time-invariant cryptographic hashes and ephemeral authentication challenges.
2. Mathematical Formulation of HMAC-Based Token Generation
The core cryptographic engine of TOTP relies on the Hash-based Message Authentication Code construct defined in RFC 2104. HMAC provides a mechanism for verifying both data integrity and message authenticity through an underlying cryptographic hash function , evaluated in conjunction with a shared secret key .
RFC 6238 natively supports multiple hashing primitives:
- HMAC-SHA-1: The ubiquitous default specification. Despite known collision vulnerabilities in SHA-1 for digital signatures, HMAC-SHA-1 remains cryptographically secure against pseudorandom function (PRF) attacks because collisions do not expose key recovery.
- HMAC-SHA-256: Recommended for high-assurance enterprise security profiles seeking resistance against future cryptanalysis.
- HMAC-SHA-512: Employs a 64-bit word architecture matching modern 64-bit microprocessor registers.
Byte-Level Counter Formatting
Before computing the message digest, the 64-bit integer must be converted into an exact 8-byte (64-bit) big-endian (network byte order) array. If , its hexadecimal representation is 0x00000000037043E0. In memory, this must be structured as eight discrete octets:
Padding or endianness mistakes at this stage represent the most frequent failure point in custom TOTP implementations.
Internal HMAC Mechanics
The HMAC construction operates on fixed block sizes ( bytes for SHA-1 and SHA-256; bytes for SHA-512) and output lengths . The shared secret is normalized: if is longer than , it is hashed to length ; if shorter, it is zero-padded on the right up to bytes.
Two constant byte masks are defined:
- The inner pad: repeated times.
- The outer pad: repeated times.
The cryptographic digest is derived via nested hashing:
Where denotes bitwise XOR, and denotes byte concatenation.
+-----------------------------------+
| Secret Key K (Base32 Decoded) |
+-----------------------------------+
|
+---------------+---------------+
| |
v v
( K XOR ipad ) ( K XOR opad )
| |
v |
[ Concatenate with T ] |
| |
v |
Hash Function H |
| |
v v
[ Intermediate Hash ] ------> [ Concatenate with Outer ]
|
v
Hash Function H
|
v
HMAC Digest HS (20-64 Bytes)The resulting digest has a length matching the chosen hash algorithm: exactly 20 bytes for SHA-1, 32 bytes for SHA-256, and 64 bytes for SHA-512.
3. The Dynamic Truncation Algorithm (DT)
A 20-byte raw cryptographic hash output is unsuitable for direct manual entry by human operators. The Dynamic Truncation algorithm, specified in RFC 4226 Section 5.3, systematically extracts a 31-bit unsigned integer from the digest in a manner that distributes randomness uniformly while preventing integer overflow issues.
Offset Extraction
Let represent the resulting HMAC array indexed from 0 to 19 (for SHA-1). The algorithm inspects the lowest four bits (the low-order nibble) of the final byte, .
Because a 4-bit nibble spans values from to ( to ), is guaranteed to satisfy:
This mathematical guarantee ensures that reading four consecutive bytes starting at (indices ) will never exceed index 19 (), completely preventing array out-of-bounds exceptions.
31-Bit Integer Construction
Using the extracted , four contiguous bytes are assembled into a 32-bit unsigned integer using big-endian bitwise shifts. Crucially, the most significant bit (MSB) of the first byte is masked with rather than .
Masking the MSB with zeroes out the signed bit, ensuring that the integer is treated as a positive 31-bit unsigned integer across architectures. This eliminates ambiguous behavior in programming environments lacking native unsigned integer types (such as older JavaScript implementations or specific SQL engines).
Modulo Reduction
Finally, the target -digit user authentication token is derived by applying modulo arithmetic to :
For the standard 6-digit code ():
If the resulting integer is smaller than , it is left-padded with zeroes to maintain exactly characters. For example, if , it is formatted as "004921".
/**
* Constant-time dynamic truncation and TOTP code derivation in TypeScript
* compliant with RFC 4226 and RFC 6238.
*/
export function dynamicTruncate(hmacDigest: Uint8Array, digits: number = 6): string {
if (hmacDigest.length < 20) {
throw new Error("HMAC digest length must be at least 20 bytes.");
}
// Step 1: Determine the 4-bit offset from the final byte
const offset = hmacDigest[hmacDigest.length - 1] & 0x0f;
// Step 2: Extract a 31-bit big-endian unsigned integer
const binaryCode =
((hmacDigest[offset] & 0x7f) << 24) |
((hmacDigest[offset + 1] & 0xff) << 16) |
((hmacDigest[offset + 2] & 0xff) << 8) |
(hmacDigest[offset + 3] & 0xff);
// Step 3: Modulo reduction
const tokenModulo = 10 ** digits;
const tokenNumber = binaryCode % tokenModulo;
// Step 4: Pad with leading zeroes to enforce string length
return tokenNumber.toString().padStart(digits, "0");
}4. Clock Drift, Window Skew, and Transmission Latency Mitigation
In ideal environments, the client clock and the server clock align perfectly to the exact millisecond. In real-world operational deployments, physical devices experience hardware clock drift, Network Time Protocol (NTP) failures, and variable transmission latency over cellular links.
The Validation Matrix and Skew Tolerances
If an end user submits an authentication code at second 29 of a 30-second window, network transit time may deliver the payload to the server at second 31. Under strict zero-tolerance validation (), the authentication attempt would fail, creating severe user friction.
To accommodate drift and latency, RFC 6238 recommends supporting an operational verification window. A verification window of size instructs the server to calculate expected tokens across three distinct intervals:
Client Submits Token
|
v
Time Step T - 1 Time Step T Time Step T + 1
[ 00s ---- 30s ] [ 30s ---- 60s ] [ 60s ---- 90s ]
+------------------+--------------------+------------------+
| Past Window | Current Window | Future Window |
| (Acceptable) | (Nominal) | (Acceptable) |
+------------------+--------------------+------------------+
\ | /
+------------------+-------------------+
|
Evaluated by Server MatrixExpanding the window to grants an effective 90-second validation aperture. While this mitigates transient latency, expanding beyond 1 substantially inflates the probability of successful brute-force submission. For a 6-digit token, an attacker has a chance of guessing a code in a single attempt; evaluating three windows increases the per-guess success rate to .
Replay Attack Protection via State Caching
Because an expanded validation window accepts codes that were mathematically valid 30 seconds ago, an eavesdropping adversary could intercept a submitted token and replay it before the window expires.
RFC 6238 mandates that authentication servers maintain a replay cache. When a token matches step , the server must record the tuple in a transactional, high-speed cache (such as an in-memory key-value store) with a time-to-live (TTL) matching seconds.
If a subsequent request presents a token matching the same step , the server rejects the request immediately:
Receive (Token, UserID)
|
+--> For each T in { T_current - W ... T_current + W }:
ExpectedToken = CalculateTOTP(UserSecret, T)
If ConstantTimeEquals(Token, ExpectedToken):
If CacheExists(Key: UserID + ":" + T):
Return REJECT_REPLAYED_TOKEN
Else:
WriteCache(Key: UserID + ":" + T, TTL: (2W + 1) * X)
Return ACCEPT_AUTHENTICATION
|
+--> Return REJECT_INVALID_TOKENRate Limiting and Brute-Force Economics
Because the entire token domain consists of only values for 6-digit codes, applications must enforce strict threshold rate limiting. Verifying servers should enforce exponential backoff or temporary account lockouts after three to five consecutive failed attempts, preventing automated enumeration attacks within the 30-second window.
5. Secure In-Browser TOTP Execution Without Remote Exposure
Traditional developer utilities often implement cryptographic debugging tools by routing user input across backend APIs. In the context of two-factor authentication, submitting seed keys to a remote server compromises the entire multi-factor guarantee: anyone possessing the Base32 seed can generate valid tokens indefinitely.
PetexSpace addresses this challenge through complete in-browser execution. The entire cryptographic sequence (Base32 parsing, HMAC calculation, and dynamic truncation) is performed directly within the user agent's execution thread via the native Web Cryptography API (window.crypto.subtle), completely eliminating remote network transmissions.
IN-BROWSER EXECUTION ENVIRONMENT
+----------------------------------------------------------------+
| |
| +------------------------+ +--------------------------+ |
| | Base32 Encoded Seed | ---> | Zero-Allocation Decoder | |
| | "JBSWY3DPEHPK3PXP" | | Uint8Array Key Buffer | |
| +------------------------+ +--------------------------+ |
| | |
| v |
| +------------------------+ +--------------------------+ |
| | DOM HighRes Time | ---> | crypto.subtle.importKey | |
| | Floor Division (30s) | | "raw", HMAC-SHA-1 | |
| +------------------------+ +--------------------------+ |
| | | |
| v v |
| +------------------------+ +--------------------------+ |
| | 8-Byte Big-Endian | ---> | crypto.subtle.sign | |
| | ArrayBuffer Counter | | In-Memory Signing | |
| +------------------------+ +--------------------------+ |
| | |
| v |
| +--------------------------+ |
| | Dynamic Truncation | |
| | 6-Digit Output Token | |
| +--------------------------+ |
| |
| (No Network Sockets, No Analytics, No Persistent Seed Writes) |
+----------------------------------------------------------------+Web Crypto API Implementation
Modern browser environments provide constant-time cryptographic primitives implemented in native machine code via crypto.subtle. The listing below demonstrates standard-compliant, zero-transmission TOTP code generation:
/**
* Generates an RFC 6238 compliant TOTP code locally inside the browser.
* Utilizes the Web Cryptography API for high-assurance HMAC computation.
*/
export async function generateInBrowserTOTP(
keyBytes: Uint8Array,
timeStepSeconds: number = 30,
digits: number = 6
): Promise<{ token: string; remainingSeconds: number }> {
// Compute step counter from local high-resolution clock
const epochSeconds = Math.floor(Date.now() / 1000);
const timeStep = Math.floor(epochSeconds / timeStepSeconds);
const remainingSeconds = timeStepSeconds - (epochSeconds % timeStepSeconds);
// Allocate 8-byte big-endian buffer for the counter
const counterBuffer = new ArrayBuffer(8);
const counterView = new DataView(counterBuffer);
// High 32 bits
counterView.setUint32(0, Math.floor(timeStep / 0x100000000), false);
// Low 32 bits
counterView.setUint32(4, timeStep & 0xffffffff, false);
// Import raw key into Web Crypto API context
const cryptoKey = await window.crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "HMAC", hash: { name: "SHA-1" } },
false,
["sign"]
);
// Compute HMAC digest in memory
const signature = await window.crypto.subtle.sign("HMAC", cryptoKey, counterBuffer);
const digestBytes = new Uint8Array(signature);
// Apply RFC 4226 Dynamic Truncation
const token = dynamicTruncate(digestBytes, digits);
return { token, remainingSeconds };
}Threat Modeling the Client-Side Context
While client-side execution eliminates transport layer vulnerabilities and server-side compromise risks, in-browser cryptographic architectures introduce distinct threat vectors that must be actively mitigated:
- Memory Scraping and Extension Injection: Untrusted browser extensions operating with broad
<all_urls>permissions can inspect DOM contents or hook global constructors. PetexSpace mitigates this by avoiding persistent DOM reflection of raw secret keys, retaining decoded key buffers solely within ephemeral function scopes. - Ephemeral Memory Management: Garbage-collected runtimes like V8 do not guarantee immediate memory zeroization. Where possible, developers must overwrite typed array buffers with zero fill patterns (
keyBytes.fill(0)) immediately after importing the cryptographic key intocrypto.subtle. - Cross-Site Scripting (XSS): If an application executing cryptographic logic is compromised via XSS, attackers can execute arbitrary code within the user's execution context. Maintaining a strict Content Security Policy (CSP) that disallows
unsafe-evaland restricts script execution solely to trusted hashes or nonces is essential.
By enforcing privacy-preserving web tools that execute entirely within the local sandbox, developers and security engineers can verify cryptographic implementations with absolute certainty that private seed credentials remain isolated within their own devices.