otp_offline 0.2.0

Library for offline verification of YubiKey OTPs.
Documentation
  • Coverage
  • 82.35%
    42 out of 51 items documented2 out of 23 items with examples
  • Size
  • Source code size: 58.43 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 4.92 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 23s Average build duration of successful builds.
  • all releases: 33s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • tripplet

YubiKey OTP Verification Library

A Rust library for working with YubiKey One-Time Passwords (OTP), including modhex encoding/decoding functionality.

Overview

The YubiKey OTP output is provided in the Modhex character set. The Modhex character set uses characters common across the majority of latin alphabet QWERTY keyboard layouts, allowing for functionality regardless of the language set.

Modhex Character Set

The modhex character set provides a direct substitution for hexadecimal characters:

Hex a b c d e f 0 1 2 3 4 5 6 7 8 9
Modhex l n r t u v c b d e f g h i j k

Features

  • Hex to Modhex conversion: Convert hexadecimal strings to modhex format
  • Modhex to Hex conversion: Convert modhex strings back to hexadecimal
  • Byte array support: Direct conversion between byte arrays and modhex
  • OTP decryption: Full YubiKey OTP decryption with AES-128-ECB
  • Data extraction: Parse all embedded OTP data (counters, timestamp, etc.)
  • CRC validation: Built-in CRC16 checksum verification
  • Validation functions: Check if strings contain valid hex or modhex characters
  • Error handling: Comprehensive error types for invalid characters
  • Minimal dependencies: Uses only AES crate for cryptographic operations

Installation

Add this to your Cargo.toml:

[dependencies]
yubikey-otp-verify = "0.1.0"

Usage

Basic Modhex Operations

use otp_offline::modhex::{hex_to_modhex, modhex_to_hex, is_valid_modhex};

// Convert hex to modhex
let hex = "0123456789abcdef";
let modhex = hex_to_modhex(hex)?;
assert_eq!(modhex, "cbdefghijklnrtuv");

// Convert modhex back to hex
let hex_result = modhex_to_hex(&modhex)?;
assert_eq!(hex_result, "0123456789abcdef");

// Validate modhex strings
let yubikey_otp = "cccccdcfgvjubvnccblhhktldthvurhdrbvbtnvjtttr";
assert!(is_valid_modhex(yubikey_otp));

Working with Byte Arrays

use otp_offline::modhex::{bytes_to_modhex, modhex_to_bytes};

// Convert bytes to modhex
let bytes = [0x01, 0x23, 0xab, 0xcd];
let modhex = bytes_to_modhex(&bytes);
assert_eq!(modhex, "cbdelnrt");

// Convert modhex back to bytes
let result_bytes = modhex_to_bytes(&modhex)?;
assert_eq!(result_bytes, vec![0x01, 0x23, 0xab, 0xcd]);

OTP Decryption

use otp_offline::otp::decrypt_otp;

// Your YubiKey configuration
let aes_key = [0x00, 0x01, 0x02, /* ... your 16-byte AES key ... */];
let expected_public_id = Some("cccccdcfgvju");

// Decrypt a YubiKey OTP
let otp = "cccccdcfgvjubvnccblhhktldthvurhdrbvbtnvjtttr";
match decrypt_otp(otp, &aes_key, expected_public_id, None) {
    Ok(decrypted) => {
        println!("Public ID: {}", decrypted.public_id);
        println!("Private ID: {}", decrypted.private_id);
        println!("Usage Counter: {}", decrypted.usage_counter);
        println!("Timestamp: {}", decrypted.timestamp);
        println!("Session Counter: {}", decrypted.session_counter);
        println!("Valid CRC: {}", decrypted.valid);
    },
    Err(e) => println!("Decryption failed: {}", e),
}

Error Handling

use otp_offline::modhex::{hex_to_modhex, ModhexError};

match hex_to_modhex("xyz123") {
    Ok(modhex) => println!("Converted: {}", modhex),
    Err(ModhexError::InvalidHexCharacter(c)) => {
        println!("Invalid hex character: '{}'", c);
    }
    Err(e) => println!("Error: {}", e),
}

API Reference

Functions

hex_to_modhex(hex_str: &str) -> Result<String, ModhexError>

Convert a hexadecimal string to modhex format. Accepts both uppercase and lowercase hex characters.

modhex_to_hex(modhex_str: &str) -> Result<String, ModhexError>

Convert a modhex string to hexadecimal format (lowercase output).

bytes_to_modhex(bytes: &[u8]) -> String

Convert a byte array to modhex string representation.

modhex_to_bytes(modhex_str: &str) -> Result<Vec<u8>, ModhexError>

Convert a modhex string to a byte vector. The modhex string must have even length.

is_valid_modhex(s: &str) -> bool

Check if a string contains only valid modhex characters.

is_valid_hex(s: &str) -> bool

Check if a string contains only valid hexadecimal characters (0-9, a-f, A-F).

decrypt_otp(otp: &str, aes_key: &[u8; 16], expected_public_id: Option<&str>, expected_private_id: Option<&[u8; 6]>) -> Result<DecryptedOtp, OtpError>

Decrypt and validate a YubiKey OTP using AES-128-ECB. Returns a DecryptedOtp struct containing all embedded data.

Error Types

ModhexError

  • InvalidHexCharacter(char): Invalid character found in hex string
  • InvalidModhexCharacter(char): Invalid character found in modhex string

OtpError

  • InvalidOtpFormat: Invalid OTP format or length
  • ModhexError(ModhexError): Modhex conversion error
  • InvalidPrivatePartLength: Invalid private part length (must be 32 characters)
  • DecryptionError: AES decryption failed
  • InvalidHex: Invalid hex string conversion

DecryptedOtp

Structure containing decrypted OTP data:

  • public_id: Public ID (first 12 characters)
  • private_id: Private ID as hex string with colons
  • usage_counter: Usage counter (increments with each use)
  • timestamp: Internal timestamp (~8Hz counter)
  • session_counter: Session counter (increments each power-on)
  • random: Random value as hex string with colons
  • checksum: CRC16 checksum as hex string with colons
  • valid: Whether the CRC checksum is valid
  • public_id_valid: Whether the public ID matches expected value

Examples

YubiKey OTP Processing

use otp_offline::modhex::{modhex_to_hex, is_valid_modhex};

fn process_yubikey_otp(otp: &str) -> Result<String, Box<dyn std::error::Error>> {
    // First, validate that the OTP is valid modhex
    if !is_valid_modhex(otp) {
        return Err("Invalid OTP: contains non-modhex characters".into());
    }

    // Convert to hex for further processing
    let hex_otp = modhex_to_hex(otp)?;

    // The hex string can now be processed for OTP verification
    Ok(hex_otp)
}

// Example YubiKey OTP
let otp = "cccccdcfgvjubvnccblhhktldthvurhdrbvbtnvjtttr";
let hex_otp = process_yubikey_otp(otp)?;
println!("OTP in hex: {}", hex_otp);

Complete OTP Verification

use otp_offline::otp::{decrypt_otp, DecryptedOtp, OtpError};
use otp_offline::modhex::is_valid_modhex;

fn verify_yubikey_otp(
    otp: &str,
    aes_key: &[u8; 16],
    expected_public_id: &str,
    expected_private_id: &[u8; 6]
) -> Result<DecryptedOtp, OtpError> {
    // Validate modhex format
    if !is_valid_modhex(otp) {
        return Err(OtpError::InvalidOtpFormat);
    }

    // Decrypt and validate
    let decrypted = decrypt_otp(
        otp,
        aes_key,
        Some(expected_public_id),
        Some(expected_private_id)
    )?;

    // Check validation results
    if !decrypted.valid {
        println!("Warning: CRC checksum validation failed");
    }

    if !decrypted.public_id_valid {
        println!("Warning: Public ID doesn't match expected value");
    }

    Ok(decrypted)
}

// Usage
let aes_key = [/* your 16-byte AES key */];
let public_id = "cccccdcfgvju";
let private_id = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];

let otp = "cccccdcfgvjubvnccblhhktldthvurhdrbvbtnvjtttr";
match verify_yubikey_otp(otp, &aes_key, public_id, &private_id) {
    Ok(decrypted) => println!("OTP verified successfully!"),
    Err(e) => println!("OTP verification failed: {}", e),
}

Key Configuration

To use the OTP decryption functionality, you'll need:

  1. Public ID: The first 12 characters of your YubiKey OTP
  2. AES Key: The 16-byte AES key programmed into your YubiKey (for validation)
  3. Private ID: The 6-byte private identifier (for validation)

These values are set during YubiKey personalization and are unique to each device.

Testing

Run the test suite:

cargo test

License

This project is licensed under the MIT License - see the LICENSE file for details.