Security  

Generate and Verify TOTP (Time-Based One-Time Passwords): Securing Banking Transactions Against Real-Time Fraud

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:

  • A shared secret (known only to user and server)

  • The current Unix time (synchronized in 30-second intervals)

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

  1. Provisioning: User scans a QR code containing a Base32-encoded secret (e.g., JBSWY3DPEHPK3PXP).

  2. Code Generation: Every 30 seconds, the app computes HMAC-SHA1(secret, T), where T = floor(current_time / 30).

  3. Truncation: A 4-byte segment of the hash is converted into a 6-digit number.

  4. 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)
    )
  • Handles Base32 padding and case insensitivity

  • Resilient to clock skew (±30 seconds by default)

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 - Copy234

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.