otp_offline 0.3.0

Library for offline verification of YubiKey OTPs.
Documentation
# `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

````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