Skip to main content Skip to sidebar

Inside YubiKey OTP Tokens: Cryptographic Architecture

YubiKey OTP tokens represent one of the most robust hardware-based authentication mechanisms available today. This deep dive explores the cryptographic architecture, implementation details, and security mechanisms that make YubiKey OTP tokens virtually impossible to spoof or compromise.

YubiKey OTP Architecture Overview

YubiKey OTP implements a sophisticated cryptographic system using AES-128 encryption with multiple security layers:

  1. Unique Device Identity: Each YubiKey has a permanent public ID and secret AES-128 key
  2. Counter-Based Security: Multiple counters prevent replay attacks and ensure uniqueness
  3. Encrypted Payloads: All sensitive data encrypted with device-specific AES keys
  4. Modhex Encoding: Output optimized for keyboard compatibility across layouts
YubiKey OTP architecture diagram

Cryptographic Implementation Details

AES-128 Encryption Core

The heart of YubiKey OTP security lies in its AES-128 implementation. Each YubiKey contains a unique 128-bit AES encryption key that is permanently stored during manufacturing and cannot be extracted or modified. The OTP generation process creates a 16-byte payload containing:

  • Private ID: 6 bytes of secret device identifier
  • Usage Counter: 16-bit non-volatile counter (persists across power cycles)
  • Session Counter: 8-bit counter (resets on power-up)
  • Timestamp: 16-bit timestamp with ~8Hz resolution
  • Random Data: 16-bit cryptographically secure random number
  • CRC16: 16-bit integrity checksum
  • Padding: 8-bit padding to complete 16-byte block

This payload is encrypted using AES-128 in Electronic Codebook (ECB) mode, which is secure in this context because each payload is unique due to the monotonically increasing counters and random components.

// YubiKeyOTP represents the internal OTP structure
type YubiKeyOTP struct {
    PublicID       string  // 12 characters, constant per device
    PrivateID      [6]byte // 6 bytes, secret device identifier
    UsageCounter   uint16  // Non-volatile usage counter
    SessionCounter uint8   // Session-specific counter
    Timestamp      uint16  // 24-bit timestamp (truncated to 16-bit)
    RandomData     uint16  // Random number
    CRC16          uint16  // Checksum for integrity
}

// CreatePayload packs all components into 16-byte structure
func (otp *YubiKeyOTP) CreatePayload() [16]byte {
    var payload [16]byte
    copy(payload[0:6], otp.PrivateID[:])
    payload[6] = byte(otp.UsageCounter & 0xFF)
    payload[7] = byte(otp.UsageCounter >> 8)
    payload[8] = otp.SessionCounter
    payload[9] = byte(otp.Timestamp & 0xFF)
    payload[10] = byte(otp.Timestamp >> 8)
    payload[11] = byte(otp.RandomData & 0xFF)
    payload[12] = byte(otp.RandomData >> 8)
    
    otp.CRC16 = calculateCRC16(payload[:13])
    payload[13] = byte(otp.CRC16 & 0xFF)
    payload[14] = byte(otp.CRC16 >> 8)
    payload[15] = 0 // Padding
    
    return payload
}

Counter Management System

YubiKey implements a sophisticated dual-counter system that provides both replay protection and uniqueness guarantees:

Usage Counter (16-bit)

  • Stored in non-volatile memory (EEPROM)
  • Increments on first use after power-up or reset
  • Provides 65,536 possible values before wraparound
  • Critical for preventing replay attacks across power cycles

Session Counter (8-bit)

  • Reset to zero when usage counter increments
  • Increments for each subsequent OTP generation in the same session
  • Provides 256 OTPs per session before usage counter must increment
  • Ensures uniqueness within a single power session

Timestamp Generation

  • 24-bit internal counter incremented at ~8Hz frequency
  • Set to random value on startup from internal RNG
  • Provides temporal component and additional entropy
  • Only 16 bits are used in the final payload due to space constraints

Random Number Generation

  • Uses internal hardware random number generator
  • Provides 16 bits of cryptographically secure entropy
  • Ensures OTP uniqueness even with identical counter states
  • Critical for preventing predictable patterns in OTP generation
// CounterManager handles YubiKey counter operations
type CounterManager struct {
    UsageCounter   uint16 // Persistent across power cycles
    SessionCounter uint8  // Reset on each power-up
    sessionActive  bool
}

// IncrementUsage manages counter progression
func (cm *CounterManager) IncrementUsage() {
    if !cm.sessionActive {
        cm.UsageCounter++
        cm.SessionCounter = 0
        cm.sessionActive = true
    } else {
        cm.SessionCounter++
    }
}

// GenerateTimestamp creates timestamp with ~8Hz resolution
func GenerateTimestamp() uint16 {
    now := time.Now().Unix()
    return uint16((now / 125) & 0xFFFF) // ~8Hz resolution
}

Modhex Encoding System

YubiKey uses Modhex (Modified Hexadecimal) encoding to ensure maximum keyboard compatibility across different layouts and languages. This encoding scheme addresses practical deployment challenges:

Character Set Selection

  • Uses only 16 characters: cbdefghijklnrtuv
  • Characters chosen for presence on most QWERTY layouts
  • Avoids problematic keys like numbers (which may require Shift on some layouts)
  • Eliminates characters that might be interpreted as special keys

Hexadecimal to Modhex Mapping Table

HexModhexHexModhex
0c8j
1b9k
2dal
3ebn
4fcr
5gdt
6heu
7ifv

Practical Benefits

  • Works reliably across QWERTY, AZERTY, QWERTZ layouts
  • No dependency on numeric keypad or special character combinations
  • Reduces user typing errors and support burden
  • Maintains deterministic encoding/decoding for validation servers

Complete OTP Generation Process

The YubiKey OTP generation follows a deterministic yet secure process that combines multiple entropy sources:

Generation Sequence

  1. Counter Management: Update usage and session counters according to power state
  2. Entropy Collection: Generate timestamp and random components
  3. Payload Construction: Pack all components into 16-byte structure
  4. Integrity Protection: Calculate CRC16 checksum over payload
  5. Encryption: Apply AES-128 encryption using device-specific key
  6. Encoding: Convert encrypted bytes to Modhex format
  7. Assembly: Concatenate public ID with encoded payload

OTP Structure Analysis

Public ID (12 characters)

  • Constant identifier for each YubiKey device
  • Used by validation servers to lookup corresponding AES key
  • Transmitted in plaintext for device identification
  • Format: Modhex-encoded 6-byte unique device identifier

Encrypted Payload (32 characters)

  • Contains all dynamic and secret components
  • Encrypted with device-specific AES-128 key
  • Modhex-encoded for keyboard compatibility
  • Represents 16 bytes of encrypted data

Integrity Verification

YubiKey employs CRC16 (Cyclic Redundancy Check) for payload integrity verification:

CRC16 Algorithm

  • Polynomial: Uses standard CRC16-CCITT polynomial (0x1021)
  • Initial Value: 0xFFFF for maximum error detection
  • Final XOR: Result is XORed for additional protection
  • Coverage: Applied to first 13 bytes of payload (excluding CRC field itself)

Error Detection Capabilities

  • Detects all single-bit errors
  • Detects all double-bit errors
  • Detects all burst errors up to 16 bits
  • Detects 99.997% of all other error patterns

Security Considerations

  • CRC is not cryptographically secure but provides data integrity
  • Combined with AES encryption for comprehensive protection
  • Must match expected value 0xF0B8 after decryption
  • Prevents acceptance of corrupted or tampered payloads
// CRC16 calculation for YubiKey payload integrity
func calculateCRC16(data []byte) uint16 {
    crc := uint16(0xFFFF)
    for _, b := range data {
        crc ^= uint16(b)
        for i := 0; i < 8; i++ {
            if crc&1 != 0 {
                crc = (crc >> 1) ^ 0x8408 // CRC16-CCITT polynomial
            } else {
                crc >>= 1
            }
        }
    }
    return ^crc & 0xFFFF
}

// ValidateOTP demonstrates OTP validation process
func ValidateOTP(otpString string, aesKey [16]byte) (bool, error) {
    if len(otpString) != 44 {
        return false, fmt.Errorf("invalid OTP length")
    }
    
    // Extract and decode payload
    modhexPayload := otpString[12:]
    hexPayload := DecodeModhex(modhexPayload)
    
    encryptedBytes, err := hex.DecodeString(hexPayload)
    if err != nil {
        return false, err
    }
    
    // Decrypt and verify CRC16
    decrypted := decryptAES128(encryptedBytes, aesKey)
    calculatedCRC := calculateCRC16(decrypted[:13])
    receivedCRC := uint16(decrypted[13]) | (uint16(decrypted[14]) << 8)
    
    return calculatedCRC == receivedCRC, nil
}

Security Analysis

Cryptographic Strength

The YubiKey OTP system provides multiple layers of security:

  1. AES-128 Encryption: 2^128 possible key combinations (340 undecillion possibilities)
  2. Counter Protection: Prevents replay attacks through monotonically increasing counters
  3. Timestamp Verification: Limits token validity window
  4. CRC16 Integrity: Detects corruption and tampering
  5. Random Components: Ensures uniqueness even with identical counter states

Attack Surface Analysis

Cryptographic Security Metrics

  • AES-128 Key Space: 2^128 = 340,282,366,920,938,463,463,374,607,431,768,211,456 possible keys
  • Usage Counter Space: 2^16 = 65,536 values (before wraparound)
  • Session Counter Space: 2^8 = 256 values per session
  • Timestamp Window: 2^16 = 65,536 values at ~8Hz (approximately 2.3 hours)
  • Random Component: 2^16 = 65,536 possible values per OTP

Attack Vector Analysis

  1. Brute Force Key Attack: Computationally infeasible with current technology
  2. Replay Attack: Prevented by monotonically increasing counters
  3. Prediction Attack: Mitigated by hardware RNG and timestamp variation
  4. Side Channel: Hardware implementation resists timing and power analysis
  5. Physical Tampering: Secure element design provides tamper resistance

Time-Based Security Considerations

  • OTP validity window typically 8-24 hours on validation servers
  • Counter synchronization windows (typically 256-1024 OTPs)
  • Manufacturing key injection security during production
  • FIPS 140-2 Level 2 certification for hardware security module compliance

OATH Integration

Beyond proprietary Yubico OTP, YubiKeys support standardized OATH algorithms:

HOTP (HMAC-Based One-Time Password)

  • Standard: RFC 4226 compliance
  • Algorithm: HMAC-SHA1 with counter-based input
  • Counter Management: 64-bit counter incremented per use
  • Output: 6-8 digit decimal codes
  • Synchronization: Server maintains counter windows for drift tolerance

TOTP (Time-Based One-Time Password)

  • Standard: RFC 6238 compliance
  • Algorithm: HOTP with time-based counter (Unix timestamp / time step)
  • Time Step: Typically 30 seconds
  • Clock Dependency: Requires external time source (Yubico Authenticator app)
  • Tolerance: Server accepts codes from adjacent time windows

Implementation Differences

  • Yubico OTP: Hardware-generated, no external dependencies
  • OATH: Standards-compliant, interoperable with other authenticators
  • Key Storage: Both use secure element for secret storage
  • User Experience: OATH requires companion app, Yubico OTP works standalone

YubiKey OTP tokens represent a pinnacle of hardware authentication security, combining robust cryptographic primitives with practical usability. The AES-128 encryption, sophisticated counter management, and integrity verification create a system that is both highly secure and resistant to common attack vectors while maintaining compatibility across diverse computing environments.