# `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:
| Modhex | `c` | `b` | `d` | `e` | `f` | `g` | `h` | `i` | `j` | `k` | `l` | `n` | `r` | `t` | `u` | `v` |
## Examples
### Encode and decode modhex
````rust
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
````rust
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.
````rust
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