Expand description
§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:
decryptionmodhex
Optional features:
simple_store— enables a small example file-backed store implementationdecryptionimplies the optionalaesdependencysimple_storedepends on bothdecryptionandmodhex
§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
0x7fffis treated as exhausted and rejected
CRC validation happens during decryption of the private 16-byte OTP payload.
§License
MIT