otp_offline 0.3.0

Library for offline verification of YubiKey OTPs.
Documentation
  • Coverage
  • 82.35%
    42 out of 51 items documented3 out of 24 items with examples
  • Size
  • Source code size: 51.24 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 4.94 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 43s 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

Offline verification primitives for YubiKey OTPs.

This crate provides:

  • modhex parsing and formatting
  • OTP parsing and AES-128 decryption
  • replay-detection validation helpers
  • a small file-backed example store behind an optional feature

Feature flags

Default features:

  • decryption
  • modhex

Optional features:

  • simple_store — enables a small example file-backed store implementation
  • decryption implies the optional aes dependency
  • simple_store depends on both decryption and modhex

Modhex basics

YubiKey OTPs are encoded with modhex, a keyboard-layout-friendly alphabet.

The mapping is:

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

Examples

Encode and decode modhex

use otp_offline::modhex::ModHex;

let raw = [0x01, 0x23, 0xab, 0xcd];
let encoded = ModHex::from(&raw[..]).to_string();

assert_eq!(encoded, "cbdelnrt");

let decoded = ModHex::try_from(encoded.as_str()).unwrap();
assert_eq!(decoded.raw_bytes(), &raw);

assert!(ModHex::is_valid("cbdelnrt").is_ok());
assert!(ModHex::is_valid("not-modhex").is_err());

Parse and decrypt a YubiKey OTP

use otp_offline::otp::Otp;

let otp = Otp::from_modhex("cbcdcecfcgchkgnhckifdncgiflkcediddgrldhuubth").unwrap();
let decrypted = otp.decrypt(&[0; 16]).unwrap();

assert_eq!(otp.id.to_string(), "cbcdcecfcgch");
assert_eq!(decrypted.private.id.raw_bytes, [7, 8, 9, 10, 11, 12]);
assert_eq!(decrypted.private.usage_counter, 1);
assert_eq!(decrypted.private.session_counter, 1);

Validate a decrypted OTP against a previous one

The crate exposes validation helpers for replay detection and monotonic counter checks.

use otp_offline::otp::{DecryptedOtp, DecryptedPrivateData, Otp};

let otp = Otp::from_modhex("cbcdcecfcgchkgnhckifdncgiflkcediddgrldhuubth").unwrap();
let decrypted = otp.decrypt(&[0; 16]).unwrap();

let previous = DecryptedOtp {
    id: decrypted.id,
    private: DecryptedPrivateData {
        id: decrypted.private.id,
        usage_counter: 0,
        session_counter: 0,
        timestamp: 1000,
        random: [0; 2],
    },
};

assert!(decrypted.validate(&previous).is_ok());

Notes on validation behavior

otp_offline::otp validates decrypted OTPs using the embedded counters and identifiers:

  • public IDs must match
  • private IDs must match
  • usage counters must not decrease
  • if the usage counter is unchanged, the session counter must increase
  • usage counter 0x7fff is treated as exhausted and rejected

CRC validation happens during decryption of the private 16-byte OTP payload.

License

MIT