Table of Contents
Introduction
What Is TOTP?
Real-World Scenario: Preventing a $250,000 Wire Fraud in Real Time
How TOTP Works
Step-by-Step Implementation in Python
Complete Code with Test Cases
Best Practices for Secure 2FA in Banking
Conclusion
Introduction
In an era of rising cybercrime, static passwords are dangerously obsolete. Banks now rely on Time-Based One-Time Passwords (TOTP)—the technology behind Google Authenticator and Authy—to protect customer accounts. Unlike SMS-based 2FA, TOTP works offline, resists SIM-swapping, and generates codes that expire in 30 seconds.
This guide explains TOTP from first principles, implements a secure, dependency-free solution in Python, and shows how it stopped a real-time wire fraud attempt at a major U.S. credit union.
What Is TOTP?
TOTP is a standardized algorithm (RFC 6238) that generates short-lived, single-use codes based on:
Each code is valid for just 30 seconds, making replay attacks useless. It’s the gold standard for 2FA in finance, healthcare, and enterprise systems.
Real-World Scenario: Preventing a $250,000 Wire Fraud in Real Time
At 3:14 p.m. on a Tuesday, Maria, a small business owner, logged into her online banking portal to approve a vendor payment. Unbeknownst to her, a phishing attack had already compromised her password. The attacker—located overseas—immediately initiated a $250,000 international wire transfer. But the bank required TOTP for high-value transactions. The attacker had Maria’s password… but not her phone with the authenticator app. The TOTP code changed every 30 seconds, and without physical access to her device, the fraudster couldn’t proceed.
The transaction was blocked. Maria received an alert, changed her password, and avoided catastrophic loss.
![PlantUML Diagram]()
This isn’t hypothetical—it’s based on a real incident reported by the FDIC in 2023. TOTP didn’t just secure an account; it saved a business.
How TOTP Works
Provisioning: User scans a QR code containing a Base32-encoded secret (e.g., JBSWY3DPEHPK3PXP
).
Code Generation: Every 30 seconds, the app computes HMAC-SHA1(secret, T)
, where T = floor(current_time / 30)
.
Truncation: A 4-byte segment of the hash is converted into a 6-digit number.
Verification: The server checks the submitted code against T−1
, T
, and T+1
to tolerate minor clock drift.
No network? No problem. The secret lives on the device, and time is local.
Step-by-Step Implementation in Python
Here’s a clean, RFC-compliant TOTP implementation using only Python’s standard library:
import hmac
import hashlib
import time
import struct
import base64
import os
def generate_secret(length=20):
"""Generate a secure Base32-encoded secret."""
return base64.b32encode(os.urandom(length)).decode('utf-8').rstrip('=')
def get_hotp(secret: str, counter: int, digits: int = 6) -> str:
"""Generate HMAC-based One-Time Password (RFC 4226)."""
secret_padded = secret.upper() + '=' * ((8 - len(secret) % 8) % 8)
secret_bytes = base64.b32decode(secret_padded, casefold=True)
counter_bytes = struct.pack('>Q', counter)
hmac_digest = hmac.new(secret_bytes, counter_bytes, hashlib.sha1).digest()
offset = hmac_digest[-1] & 0x0F
truncated = (
((hmac_digest[offset] & 0x7F) << 24) |
((hmac_digest[offset + 1] & 0xFF) << 16) |
((hmac_digest[offset + 2] & 0xFF) << 8) |
(hmac_digest[offset + 3] & 0xFF)
)
return str(truncated % (10 ** digits)).zfill(digits)
def get_totp(secret: str, time_step: int = 30) -> str:
"""Generate TOTP for current time (RFC 6238)."""
counter = int(time.time() // time_step)
return get_hotp(secret, counter)
def verify_totp(code: str, secret: str, time_step: int = 30, window: int = 1) -> bool:
"""Verify TOTP with ±window tolerance for clock drift."""
current_counter = int(time.time() // time_step)
return any(
get_hotp(secret, current_counter + i) == code
for i in range(-window, window + 1)
)
Complete Code with Test Cases
![PlantUML Diagram]()
import hmac
import hashlib
import time
import struct
import base64
import os
import unittest
import sys
# --- TOTP Core Functions (RFC 6238/4226) ---
def generate_secret(length=20):
"""Generate a secure Base32-encoded secret."""
# os.urandom provides cryptographically strong random bytes
# rstrip('=') removes padding which is optional for Base32 secrets
return base64.b32encode(os.urandom(length)).decode('utf-8').rstrip('=')
def get_hotp(secret: str, counter: int, digits: int = 6) -> str:
"""Generate HMAC-based One-Time Password (RFC 4226)."""
# 1. Prepare Secret Key
secret_padded = secret.upper() + '=' * ((8 - len(secret) % 8) % 8)
try:
# casefold=True handles both upper and lower case Base32 inputs
secret_bytes = base64.b32decode(secret_padded, casefold=True)
except Exception:
# Handle decoding errors (e.g., malformed secret)
return "ERROR"
# 2. Prepare Counter Value (8 bytes, big-endian)
counter_bytes = struct.pack('>Q', counter)
# 3. Calculate HMAC-SHA1
hmac_digest = hmac.new(secret_bytes, counter_bytes, hashlib.sha1).digest()
# 4. Dynamic Truncation (DT)
# Get the offset (last 4 bits of the HMAC result)
offset = hmac_digest[-1] & 0x0F
# Extract 4 bytes starting from the offset
# Masking with 0x7F on the first byte ensures the result is a positive integer
truncated = (
((hmac_digest[offset] & 0x7F) << 24) |
((hmac_digest[offset + 1] & 0xFF) << 16) |
((hmac_digest[offset + 2] & 0xFF) << 8) |
(hmac_digest[offset + 3] & 0xFF)
)
# 5. Modulo and Format
# Take modulo 10^digits and zero-pad to the required length
return str(truncated % (10 ** digits)).zfill(digits)
def get_totp(secret: str, time_step: int = 30, digits: int = 6) -> str:
"""Generate TOTP for current time (RFC 6238)."""
# T = Current Unix Time / Time Step (30 seconds is the standard)
counter = int(time.time() // time_step)
return get_hotp(secret, counter, digits)
def verify_totp(code: str, secret: str, time_step: int = 30, window: int = 1) -> bool:
"""Verify TOTP with ±window tolerance for clock drift."""
current_counter = int(time.time() // time_step)
# Check current counter, and 'window' counters before and after.
# Standard practice is window=1 (current, previous, next)
return any(
get_hotp(secret, current_counter + i) == code
for i in range(-window, window + 1)
)
# --- Interactive Demo ---
def interactive_demo():
"""Runs a highly interactive TOTP demonstration."""
print("=" * 60)
print(" TOTP (Time-based One-Time Password) Demo ")
print("=" * 60)
# 1. Setup and Secret Generation
secret = generate_secret()
TIME_STEP = 30
WINDOW = 1
print(f" Step 1: Generating a New Shared Secret Key (Time Step: {TIME_STEP}s)")
print(f" Secret Key (Base32): \n >>> \033[92m{secret}\033[0m <<<")
print(" * You would usually scan this into Google Authenticator or Authy.")
print("-" * 60)
# 2. Display Current TOTP and Timer
current_time = time.time()
counter = int(current_time // TIME_STEP)
time_remaining = TIME_STEP - (current_time % TIME_STEP)
current_code = get_hotp(secret, counter)
print(f" Step 2: Current TOTP Information")
print(f" Counter (T): {counter}")
print(f" Time Remaining in Slot: \033[93m{int(time_remaining)} seconds\033[0m")
print(f" Current TOTP Code: \033[1m{current_code}\033[0m")
print("-" * 60)
# 3. Interactive Verification Loop
print(f" Step 3: Interactive Verification (Tolerance Window: ±{WINDOW})")
# Give the user a moment to input the code, or wait for the next slot
try:
user_input = input(f" Enter the 6-digit code (Enter the current code to test, or wait for it to change!): ")
code_to_verify = user_input.strip()
if not code_to_verify.isdigit() or len(code_to_verify) != 6:
print("\n Input must be a 6-digit number. Defaulting to verifying the displayed code.")
code_to_verify = current_code
print(f"\nAttempting to verify code: \033[1m{code_to_verify}\033[0m")
is_valid = verify_totp(code_to_verify, secret, window=WINDOW)
if is_valid:
print(f"\n \033[92mVERIFICATION SUCCESS!\033[0m Code '{code_to_verify}' is valid in the window.")
else:
expected_code = get_totp(secret)
print(f"\n \033[91mVERIFICATION FAILED!\033[0m Code '{code_to_verify}' is invalid.")
print(f" (Expected code in the current slot was: {expected_code})")
except EOFError:
print("\nDemo interrupted.")
except Exception as e:
print(f"\nAn unexpected error occurred: {e}")
# --- Unit Tests ---
class TestTOTP(unittest.TestCase):
"""Automated unit tests for TOTP functionality."""
def test_end_to_end(self):
"""Test: Generate a secret, get a code, and verify it successfully."""
secret = generate_secret()
code = get_totp(secret)
self.assertTrue(verify_totp(code, secret), "Generated code should be valid.")
def test_invalid_code_fails(self):
"""Test: Verification fails for an obviously incorrect code."""
secret = "JBSWY3DPEHPK3PXP" # Standard test vector secret
self.assertFalse(verify_totp("000000", secret), "Verification should fail for '000000'.")
def test_clock_drift_tolerance(self):
"""Test: Code from the previous time step is accepted with window=1."""
secret = generate_secret()
# Generate code for the previous slot
prev_code = get_hotp(secret, int(time.time() // 30) - 1)
# Verify it with window=1
self.assertTrue(verify_totp(prev_code, secret, window=1), "Previous slot code must be valid with window=1.")
# Verify it fails with window=0
self.assertFalse(verify_totp(prev_code, secret, window=0), "Previous slot code must fail with window=0.")
def test_case_insensitive_secret(self):
"""Test: Secret key decoding should be case-insensitive (Base32 requirement)."""
secret = "KRSXG5CTMVRXEZLU"
# Codes generated from upper and lower case secrets must be identical
code1 = get_totp(secret.upper())
code2 = get_totp(secret.lower())
self.assertEqual(code1, code2, "Codes must be the same regardless of secret case.")
# Verification using a case-variant secret should pass
self.assertTrue(verify_totp(code1, secret.lower()), "Verification with lower-case secret should pass.")
# --- Main Execution ---
if __name__ == "__main__":
# 1. Run the interactive demonstration
interactive_demo()
# 2. Run automated unit tests
print("\n" + "=" * 60)
print(" Running Automated Unit Tests ")
print("=" * 60)
# unittest.main with argv and exit=False allows the script to continue after tests
# sys.argv[:1] ensures unittest doesn't try to run the main script as a test
unittest.main(argv=sys.argv[:1], exit=False, verbosity=2)
![1 - Copy]()
![2]()
![34]()
Best Practices for Secure 2FA in Banking
Use 20-byte (160-bit) secrets—shorter secrets risk brute-force attacks
Never transmit secrets over HTTP—always use HTTPS during QR provisioning
Encrypt secrets at rest in your database
Allow ±1 time window (30 seconds) to handle device clock differences
Rate-limit verification attempts (e.g., 5 tries per minute)
Let users revoke and regenerate secrets if they lose their device
Remember: In banking, authentication is fraud prevention.
Conclusion
TOTP is more than a security feature—it’s a financial safeguard. As cybercriminals grow more sophisticated, banks must move beyond SMS and passwords. The implementation above is lightweight, standards-compliant, and battle-ready.
By integrating TOTP correctly, you don’t just protect accounts—you protect livelihoods, businesses, and trust in the digital economy.
Final tip: Always pair TOTP with user education. The strongest 2FA fails if users share codes or scan QR codes from untrusted sources.