<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Two-Factor-Authentication | Toolbox Guides &amp; Articles</title><link>https://blog.toolbox.vishnudigital.com/tags/two-factor-authentication/</link><atom:link href="https://blog.toolbox.vishnudigital.com/tags/two-factor-authentication/index.xml" rel="self" type="application/rss+xml"/><description>Two-Factor-Authentication</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0000</lastBuildDate><image><url>https://blog.toolbox.vishnudigital.com/media/icon.svg</url><title>Two-Factor-Authentication</title><link>https://blog.toolbox.vishnudigital.com/tags/two-factor-authentication/</link></image><item><title>How TOTP 2FA Codes Work: RFC 6238 Math, Base32 Secrets, and Clock Drift</title><link>https://blog.toolbox.vishnudigital.com/how-totp-2fa-codes-work-rfc-6238-guide/</link><pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate><guid>https://blog.toolbox.vishnudigital.com/how-totp-2fa-codes-work-rfc-6238-guide/</guid><description>
&lt;aside class="comic-bubble not-prose my-8 p-6 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d]" aria-label="AEO Quick Answer &amp; Specification Dossier"&gt;
&lt;div class="flex items-center justify-between gap-3 mb-3"&gt;
&lt;div class="flex items-center gap-2"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[11px] font-black tracking-wider"&gt;QUICK ANSWER&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold text-[#2c221a]/70 dark:text-[#ede6db]/70"&gt;RFC SPECIFICATION SUMMARY&lt;/span&gt;
&lt;/div&gt;
&lt;span class="hidden sm:inline-flex items-center gap-1.5 text-[11px] font-bold font-mono px-2 py-0.5 rounded border border-emerald-700 dark:border-emerald-500 bg-emerald-100 dark:bg-emerald-950 text-emerald-900 dark:text-emerald-200"&gt;
&lt;span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-600 dark:bg-emerald-400"&gt;&lt;/span&gt; 100% Client-Side
&lt;/span&gt;
&lt;/div&gt;
&lt;p class="text-base sm:text-lg text-[#2c221a] dark:text-[#ede6db] leading-relaxed font-semibold mb-4"&gt;
A TOTP code is HOTP (RFC 4226) applied to a time counter. The authenticator computes T = floor((Unix time - T0) / 30), runs HMAC-SHA1 over T as an 8-byte big-endian integer using the Base32-decoded shared secret, applies dynamic truncation to pull a 31-bit integer out of the digest, and returns that value modulo 10^6 as the 6-digit code. Both the client and the server derive the same code offline because they share the secret and read the same clock.
&lt;/p&gt;
&lt;div class="flex flex-wrap gap-2 text-xs font-mono font-bold mt-4 pt-3 border-t border-[#ded4c3] dark:border-[#3b332c]"&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Difficulty: Intermediate&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Time: 7 Mins&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Privacy: 100% Client-Side RAM&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Standard: RFC 6238 / RFC 4226&lt;/span&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;div class="tool-embed-container comic-panel not-prose my-10 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d] overflow-hidden" id="embed-totp-authenticator-1788851897566377000-box" role="region" aria-label="Interactive Tool Workbench"&gt;
&lt;div class="comic-halftone p-4 sm:p-5 border-b-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#f5efe4] dark:bg-[#211a14] flex flex-wrap items-center justify-between gap-3"&gt;
&lt;div class="flex items-center gap-2.5 flex-wrap min-w-0"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[10px] font-black tracking-wider uppercase"&gt;
Interactive Workbench
&lt;/span&gt;
&lt;span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800 shrink-0"&gt;
&lt;span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"&gt;&lt;/span&gt;
&lt;span&gt;LIVE&lt;/span&gt;
&lt;/span&gt;
&lt;h3 class="text-base sm:text-lg font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight truncate m-0"&gt;
How TOTP 2FA Codes Work: RFC 6238 Math, Base32 Secrets, and Clock Drift
&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="flex items-center gap-2 shrink-0"&gt;
&lt;a href="https://toolbox.vishnudigital.com/totp-authenticator" target="_blank" rel="noopener noreferrer" class="comic-btn-secondary !py-1 !px-3 !text-xs font-mono font-bold flex items-center gap-1.5 shadow-[2px_2px_0px_#2c221a] dark:shadow-[2px_2px_0px_#14100d] hover:translate-x-[-1px] hover:translate-y-[-1px] transition-all" title="Open full utility in new window"&gt;
&lt;span&gt;Open Fullscreen ↗&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="relative w-full overflow-hidden bg-[#fdfbf7] dark:bg-[#1a1410]" style="min-height: 420px;"&gt;
&lt;div id="embed-totp-authenticator-1788851897566377000-skeleton" class="absolute inset-0 flex flex-col items-center justify-center p-8 bg-[#fdfbf7] dark:bg-[#1e1814] z-10 transition-opacity duration-300"&gt;
&lt;div class="flex items-center gap-3 p-4 rounded-xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#f5efe4] dark:bg-[#28201a] shadow-[2px_2px_0px_#2c221a] dark:shadow-[2px_2px_0px_#14100d] max-w-sm w-full"&gt;
&lt;div class="w-8 h-8 rounded-lg bg-amber-500/20 flex items-center justify-center shrink-0"&gt;
&lt;svg class="w-5 h-5 text-amber-600 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"&gt;
&lt;circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"&gt;&lt;/circle&gt;
&lt;path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div class="min-w-0 flex-1"&gt;
&lt;div class="text-xs font-bold text-[#2c221a] dark:text-[#ede6db] truncate"&gt;Initializing Workbench...&lt;/div&gt;
&lt;div class="text-[10px] font-mono text-[#2c221a]/60 dark:text-[#ede6db]/60"&gt;100% Client-Side RAM Sandbox&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;iframe
id="embed-totp-authenticator-1788851897566377000-frame"
data-base-src="https://toolbox.vishnudigital.com/totp-authenticator?embed=true"
src="about:blank"
title="How TOTP 2FA Codes Work: RFC 6238 Math, Base32 Secrets, and Clock Drift – Free Online Utility"
class="w-full border-0 block opacity-0 transition-opacity duration-300"
style="height: 580px; min-height: 420px;"
loading="lazy"
allow="clipboard-read; clipboard-write; web-share"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads"&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;div class="p-3 sm:px-5 border-t border-[#ded4c3] dark:border-[#3b332c] bg-[#f5efe4]/80 dark:bg-[#211a14]/80 flex flex-wrap items-center justify-between gap-3 text-xs font-mono"&gt;
&lt;div class="flex items-center gap-2 text-[#2c221a]/70 dark:text-[#ede6db]/70"&gt;
&lt;span class="text-emerald-600 dark:text-emerald-400 font-bold"&gt;🔒 Private Execution:&lt;/span&gt;
&lt;span class="hidden sm:inline"&gt;Computations run in local browser memory. Zero server uploads.&lt;/span&gt;
&lt;span class="sm:hidden"&gt;Zero server uploads.&lt;/span&gt;
&lt;/div&gt;
&lt;div class="flex items-center gap-1.5 shrink-0"&gt;
&lt;a href="https://toolbox.vishnudigital.com/totp-authenticator" target="_blank" rel="noopener noreferrer" class="font-bold text-amber-700 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-300 hover:underline"&gt;
Launch Fullscreen →
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function() {
const containerId = "embed-totp-authenticator-1788851897566377000";
const frame = document.getElementById(containerId + "-frame");
const skeleton = document.getElementById(containerId + "-skeleton");
if (!frame) return;
function getCurrentTheme() {
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}
function resolveSrc() {
const baseSrc = frame.getAttribute("data-base-src");
if (!baseSrc) return "";
const theme = getCurrentTheme();
return baseSrc + (baseSrc.includes("?") ? "&amp;" : "?") + "theme=" + theme;
}
function loadIframe() {
if (frame.getAttribute("src") !== "about:blank") return;
const fullSrc = resolveSrc();
frame.src = fullSrc;
frame.onload = function() {
if (skeleton) {
skeleton.classList.add("opacity-0");
setTimeout(function() { skeleton.style.display = "none"; }, 300);
}
frame.classList.remove("opacity-0");
frame.classList.add("opacity-100");
syncThemeToIframe();
};
}
function syncThemeToIframe() {
if (!frame || !frame.contentWindow) return;
try {
frame.contentWindow.postMessage({
type: "TOOLBOX_THEME_CHANGE",
theme: getCurrentTheme()
}, "*");
} catch(e) {}
}
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
loadIframe();
observer.disconnect();
}
});
}, { rootMargin: "250px" });
observer.observe(frame);
} else {
loadIframe();
}
window.addEventListener("message", function(event) {
if (
event.data &amp;&amp;
typeof event.data === "object" &amp;&amp;
event.data.type === "TOOLBOX_RESIZE" &amp;&amp;
event.data.height
) {
if (event.source === frame.contentWindow) {
const measured = Math.max(event.data.height + 20, 420);
frame.style.height = measured + "px";
}
}
});
const themeObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.attributeName === "class") {
syncThemeToIframe();
}
});
});
themeObserver.observe(document.documentElement, { attributes: true });
document.querySelectorAll(".theme-toggle").forEach(function(btn) {
btn.addEventListener("click", function() {
setTimeout(syncThemeToIframe, 50);
});
});
})();
&lt;/script&gt;
&lt;div class="tool-hero-cta not-prose my-10 p-6 sm:p-7 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d] flex flex-col md:flex-row items-start md:items-center justify-between gap-6 transition-all" role="complementary" aria-label="Interactive Companion Tool"&gt;
&lt;div class="space-y-2 max-w-xl"&gt;
&lt;div class="flex items-center gap-2"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[10px] font-black tracking-wider uppercase"&gt;
Free In-Browser Tool
&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold text-stone-500 dark:text-stone-400"&gt;TOOLBOX UTILITY&lt;/span&gt;
&lt;/div&gt;
&lt;h3 class="text-xl font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight m-0"&gt;
TOTP 2FA Authenticator Simulator
&lt;/h3&gt;
&lt;p class="text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium m-0"&gt;
Paste a Base32 secret and watch live 6-digit codes rotate on a 30-second timer. Secrets are decoded in browser memory and never transmitted.
&lt;/p&gt;
&lt;/div&gt;
&lt;div class="shrink-0 w-full md:w-auto"&gt;
&lt;a href="https://toolbox.vishnudigital.com/totp-authenticator" class="comic-btn-primary w-full md:w-auto text-center" &gt;
Open TOTP Code Simulator →
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-totp-actually-is"&gt;What TOTP Actually Is&lt;/h2&gt;
&lt;p&gt;Time-based One-Time Password (TOTP) is defined in &lt;strong&gt;RFC 6238&lt;/strong&gt;. It is not a new algorithm — it is a thin wrapper around HMAC-based One-Time Password (HOTP), specified in &lt;strong&gt;RFC 4226&lt;/strong&gt;. HOTP derives a code from a shared secret and a moving &lt;em&gt;counter&lt;/em&gt;. TOTP simply says: use the clock as the counter.&lt;/p&gt;
&lt;p&gt;That one substitution is what makes authenticator apps work offline. There is no request to Google, Authy, or your identity provider when a code appears. The phone holds the secret, reads its own clock, and computes the answer. The server does the identical computation and compares.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What you get from this guide:&lt;/strong&gt; the exact formula, the truncation step almost every explanation skips, the enrolment URI format, and the three failure modes that generate support tickets.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-rfc-6238-formula-step-by-step"&gt;The RFC 6238 Formula, Step by Step&lt;/h2&gt;
&lt;h3 id="step-1-derive-the-time-counter"&gt;Step 1: Derive the time counter&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;T = floor((Current Unix Time - T0) / X)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Current Unix Time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds since 1970-01-01T00:00:00Z, UTC&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Epoch offset from which counting starts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time step in seconds&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At Unix time &lt;code&gt;1789000000&lt;/code&gt;, with defaults, &lt;code&gt;T = floor(1789000000 / 30) = 59633333&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="step-2-compute-the-hmac"&gt;Step 2: Compute the HMAC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt; is encoded as an &lt;strong&gt;8-byte big-endian unsigned integer&lt;/strong&gt; and passed as the message to HMAC, keyed with the raw secret:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HS = HMAC-SHA1(K, T) // 20-byte digest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The default hash is SHA-1. RFC 6238 explicitly permits HMAC-SHA256 and HMAC-SHA512. Using SHA-1 here is not the weakness people assume — HMAC&amp;rsquo;s security relies on the pseudorandomness of the compression function, not on collision resistance, so the known SHA-1 collision attacks do not translate into TOTP forgery. Read the underlying construction in the
.&lt;/p&gt;
&lt;h3 id="step-3-dynamic-truncation"&gt;Step 3: Dynamic truncation&lt;/h3&gt;
&lt;p&gt;This is the step most tutorials omit, and the reason a naive implementation produces codes the server rejects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;offset = HS[19] &amp;amp; 0x0F // low 4 bits of the last byte
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;P = HS[offset..offset+3] // 4 consecutive bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;binCode = P &amp;amp; 0x7FFFFFFF // clear the most significant bit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Masking the high bit removes any signed-integer ambiguity across language implementations. The result is a 31-bit integer.&lt;/p&gt;
&lt;h3 id="step-4-reduce-to-digits"&gt;Step 4: Reduce to digits&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Code = binCode mod 10^Digits
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With &lt;code&gt;Digits = 6&lt;/code&gt;, &lt;code&gt;binCode = 1284755224&lt;/code&gt; yields &lt;code&gt;755224&lt;/code&gt; — the canonical RFC 4226 test vector. Codes are &lt;strong&gt;zero-padded&lt;/strong&gt; to the full width: &lt;code&gt;000042&lt;/code&gt; is a valid code, &lt;code&gt;42&lt;/code&gt; is not.&lt;/p&gt;
&lt;h3 id="reference-implementation"&gt;Reference implementation&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Browser-native TOTP with Web Crypto. rawKey = Base32-decoded secret bytes.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;totp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;algo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SHA-1&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 8-byte big-endian counter
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DataView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;raw&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;HMAC&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;algo&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;sign&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;HMAC&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;div class="in-article-ad-wrapper not-prose my-8 p-3 sm:p-4 rounded-xl border-2 border-stone-900/40 dark:border-stone-400/40 bg-[#fdfbf7] dark:bg-[#211c18] shadow-[3px_3px_0px_rgba(41,30,20,0.3)] dark:shadow-[3px_3px_0px_rgba(242,237,228,0.2)] overflow-hidden"&gt;
&lt;div class="flex items-center justify-between mb-2 border-b border-stone-200 dark:border-stone-800 pb-1"&gt;
&lt;span class="text-[10px] font-mono font-bold uppercase tracking-widest text-stone-500 dark:text-stone-400"&gt;Advertisement&lt;/span&gt;
&lt;span class="text-[9px] font-mono uppercase px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300"&gt;Sponsored&lt;/span&gt;
&lt;/div&gt;
&lt;div class="w-full flex justify-center items-center overflow-hidden min-h-[250px]"&gt;
&lt;ins class="adsbygoogle"
style="display:block; text-align:center; min-height:250px; width:100%;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-5474548764059850"
&gt;&lt;/ins&gt;
&lt;script&gt;
(adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id="base32-secrets-why-not-base64"&gt;Base32 Secrets: Why Not Base64?&lt;/h2&gt;
&lt;p&gt;The shared secret is transported as &lt;strong&gt;Base32&lt;/strong&gt; (RFC 4648, the same document that defines Base64 — see the
for the shared 5-bit and 6-bit chunking mechanics).&lt;/p&gt;
&lt;p&gt;Base32 is the deliberate choice for three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Case-insensitive alphabet&lt;/strong&gt; (&lt;code&gt;A-Z&lt;/code&gt; and &lt;code&gt;2-7&lt;/code&gt;) — a user can retype it from a printed backup without shift-key errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No visually ambiguous characters&lt;/strong&gt; — &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, and &lt;code&gt;8&lt;/code&gt; are excluded, so &lt;code&gt;O&lt;/code&gt;/&lt;code&gt;0&lt;/code&gt; and &lt;code&gt;l&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt; confusion disappears.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe in URIs and QR codes&lt;/strong&gt; without percent-encoding.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Practical detail that breaks implementations: Base32 padding (&lt;code&gt;=&lt;/code&gt;) is optional in authenticator secrets, and most apps strip whitespace and lowercase before decoding. A 160-bit secret encodes to 32 Base32 characters. RFC 4226 requires a minimum of 128 bits and &lt;strong&gt;recommends 160 bits&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-enrolment-qr-code-otpauth-uri"&gt;The Enrolment QR Code: &lt;code&gt;otpauth://&lt;/code&gt; URI&lt;/h2&gt;
&lt;p&gt;The QR code you scan is a plain text URI in the Key Uri Format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;otpauth://totp/Acme%20Inc:alice@example.com?secret=JBSWY3DPEHPK3PXP&amp;amp;issuer=Acme%20Inc&amp;amp;algorithm=SHA1&amp;amp;digits=6&amp;amp;period=30
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Practical note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base32 shared key&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Required.&lt;/strong&gt; Everything else has a default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issuer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service name shown in the app&lt;/td&gt;
&lt;td&gt;Set it in both the label prefix and the query parameter — older apps read only one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;algorithm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SHA1&lt;/code&gt;, &lt;code&gt;SHA256&lt;/code&gt;, &lt;code&gt;SHA512&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Widely &lt;strong&gt;ignored&lt;/strong&gt; by popular apps, which assume SHA-1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;digits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt; or &lt;code&gt;8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Also frequently ignored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;period&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time step in seconds&lt;/td&gt;
&lt;td&gt;Also frequently ignored.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Design consequence:&lt;/strong&gt; if your server issues &lt;code&gt;algorithm=SHA256&amp;amp;digits=8&lt;/code&gt; and your users authenticate with an app that silently assumes SHA-1 and 6 digits, every code fails and the failure is indistinguishable from a wrong password. Unless you control the client, stay on the SHA-1 / 6-digit / 30-second defaults.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="clock-drift-validation-windows-and-replay"&gt;Clock Drift, Validation Windows, and Replay&lt;/h2&gt;
&lt;p&gt;TOTP has no synchronisation channel. The protocol&amp;rsquo;s entire tolerance mechanism is the server checking neighbouring time steps.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server window&lt;/th&gt;
&lt;th&gt;Effective validity&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;0-30 s&lt;/td&gt;
&lt;td&gt;Rejects codes typed slowly; poor UX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T-1, T, T+1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up to 90 s&lt;/td&gt;
&lt;td&gt;Common default; absorbs drift and typing delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T-2 … T+2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up to 150 s&lt;/td&gt;
&lt;td&gt;Use only with strict rate limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three rules that separate a correct deployment from a fragile one:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run NTP on the server.&lt;/strong&gt; Client drift is the user&amp;rsquo;s problem; server drift breaks every user at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Burn the code after use.&lt;/strong&gt; RFC 6238 § 5.2 is explicit: accept each code once per user per time step. Without this, a code intercepted at second 1 stays replayable for the rest of the window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate-limit attempts.&lt;/strong&gt; A 6-digit code is 1,000,000 possibilities. At an unlimited request rate, an attacker inside a 90-second window has real odds. Cap at roughly 5 failures, then lock or back off exponentially.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="where-totp-fits-in-2026"&gt;Where TOTP Fits in 2026&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;SMS OTP&lt;/th&gt;
&lt;th&gt;TOTP (RFC 6238)&lt;/th&gt;
&lt;th&gt;FIDO2 / Passkey&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Works offline&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resists SIM swap&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resists real-time phishing&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (origin-bound)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server holds a reusable secret&lt;/td&gt;
&lt;td&gt;✅ (risk)&lt;/td&gt;
&lt;td&gt;✅ (risk)&lt;/td&gt;
&lt;td&gt;❌ (public key only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery burden&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium (backup codes)&lt;/td&gt;
&lt;td&gt;Medium (multi-device sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;TOTP&amp;rsquo;s structural weakness is the &lt;strong&gt;shared secret at rest&lt;/strong&gt;. The server must store the key in a form it can compute with, so a database breach exposes every enrolled seed unless the seeds are encrypted with a key held outside the database. Store them encrypted, and treat the enrolment QR code as a credential — a screenshot in a chat thread is a bypassed second factor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="step-by-step-testing-totp-codes-with-toolbox"&gt;Step-by-Step: Testing TOTP Codes with Toolbox&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open the tool&lt;/strong&gt;: visit the
.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paste the Base32 secret&lt;/strong&gt; produced by your enrolment endpoint (spacing and case are normalised for you).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set algorithm, digits, and period&lt;/strong&gt; to match your server configuration — this is where SHA-256 or 8-digit mismatches surface immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare against your backend&lt;/strong&gt;: call your verification endpoint with the displayed code while the countdown ring is above roughly 5 seconds, so a step boundary does not confuse the result.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproduce drift deliberately&lt;/strong&gt;: verify a code just as the ring empties to confirm your server actually accepts &lt;code&gt;T-1&lt;/code&gt; and does not silently reject late submissions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; you can prove whether a failing 2FA flow is a secret-encoding bug, an algorithm mismatch, or a clock/window problem — without installing a mobile app or exposing the seed to a third-party service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related guides:&lt;/strong&gt;
·
·
&lt;/p&gt;
&lt;section class="faqs-container not-prose my-12 border-t-2 border-stone-200 dark:border-stone-800 pt-8" aria-label="Frequently Asked Questions"&gt;
&lt;div class="flex items-center gap-2 mb-6"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2 py-0.5 rounded text-[10px] font-black tracking-wider"&gt;FAQ&lt;/span&gt;
&lt;h2 class="text-2xl font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight m-0"&gt;
Frequently Asked Questions
&lt;/h2&gt;
&lt;/div&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;How does an authenticator app generate a 6-digit code without internet access?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
The app stores a shared secret exchanged once during enrolment (usually via a QR code). Every 30 seconds it computes HMAC-SHA1 over the current time step (Unix time divided by 30), truncates the 20-byte digest to a 31-bit integer, and takes that value modulo 1,000,000 for 6 digits. Both phone and server hold the same secret and read the same clock, so they derive the same code with no network round trip.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;Why do TOTP codes change every 30 seconds?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
RFC 6238 defines a time step X (default 30 seconds) and computes T = floor((current Unix time - T0) / X), with T0 normally 0. Each time T increments, the HMAC input changes and a new code appears. Thirty seconds balances usability against how long a stolen code stays valid.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;What happens if my phone&amp;#39;s clock is wrong?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
TOTP has no synchronisation protocol, so a device off by more than one time step produces codes the server rejects. Most servers accept the previous and next step (roughly a 90-second window) to absorb drift and typing delay. Persistent failures are fixed by enabling automatic network time on the device, not by retyping codes.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;Is TOTP still secure in 2026, or should I move to passkeys?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
TOTP is far stronger than SMS one-time passwords and satisfies most compliance baselines, but it is phishable: a real-time phishing proxy can relay the digits inside the validity window, and the shared seed can leak from a breached database or a screenshotted QR code. Where phishing is in scope, FIDO2 or WebAuthn passkeys are stronger because the signed challenge is bound to the origin.
&lt;/p&gt;
&lt;/details&gt;
&lt;/section&gt;</description></item></channel></rss>