1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Implementation of [IETF RFC 4226][4226], "HOTP: An HMAC-Based One-Time Password
//! Algorithm."
//!
//! # Examples
//!
//! [The workhorse `hotp` function][hotp] returns a [`Token`][Token] of the specified length:
//!
//! ```rust
//!# use rfc_4226::{hotp, Token};
//! let key = b"ferris23!@#$%^&*()";
//! let counter = 9001_u64;
//! let token: Token<6> = hotp(key, counter).unwrap();
//! assert_eq!(token, Token(852888));
//! ```
//!
//! The crate makes extensive use of ["const generics"][const-generics] to encode token lengths
//! in all operations, forcing consumers to specify exactly what, for instance, "is the token
//! equal to this number?" means. This explicitness also enables some nice features, such as
//! automatic zero-padding of tokens to the correct length for display to a user:
//!
//! ```rust
//!# use rfc_4226::{hotp, Token};
//! let key = b"ferris23!@#$%^&*()";
//! let counter = 292167_u64;
//! let token: Token<6> = hotp(key, counter).unwrap();
//! // Equivalent:
//! let token = hotp::<_, _, 6>(key, counter).unwrap();
//! assert_eq!(token.to_string(), "000000");
//! ```
//!
//! This type-level encoding is also used to ensure that the HOTP spec is followed closely
//! at compile time.
//!
//! ```rust,compile_fail
//!# use rfc_4226::{hotp, Token};
//! let key = b"ferris23!@#$%^&*()";
//! let counter = 9001_u64;
//! // The HOTP spec only allows tokens of length 6–9
//! let pin: Token<4> = hotp(key, counter).unwrap();
//! ```
//!
//! [4226]: https://datatracker.ietf.org/doc/html/rfc4226
//! [const-generics]: https://rust-lang.github.io/rfcs/2000-const-generics.html
#![no_std]

pub mod digest;
pub mod length;
use digest::{hmac_sha1, Digest as _};
use length::{Length, TokenLength};

/// HOTP token type.
///
/// Note that comparing tokens of different lengths will fail:
///
/// ```rust,compile_fail
///# use rfc_4226::Token;
/// assert_eq!(Token::<6>(0), Token::<7>(0));
/// ```
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Token<const DIGITS: u8>(pub u32);

impl<const DIGITS: u8> core::fmt::Display for Token<DIGITS> {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        write!(f, "{:01$}", self.0, DIGITS as usize)
    }
}

/// HOTP error type.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HotpError {
    ShortSecret(usize),
}

impl core::fmt::Display for HotpError {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        let Self::ShortSecret(len) = self;
        write!(
            f,
            "Shared secret length {} bits is too short (minimum: 128 bits)",
            len * 8
        )
    }
}

/// Main HOTP function.
///
/// This function takes an 8-byte `counter` element and a key (specified as a sequence of bytes)
/// and uses the HMAC-SHA1 digest method followed by truncation to produce an HOTP
/// [token][`Token`] of the desired number of `DIGITS`.
///
/// # Errors
///
/// Currently (and subject to change), the only possible error from this method results from
/// providing an invalid shared secret (`key`). According to [RFC 4226][4226], algorithm requirement 6:
///
/// > The length of the shared secret MUST be at least 128 bits. This document RECOMMENDS a
/// shared secret length of 160 bits.
///
/// As such, this method will return an error if the key is shorter than 16 bytes (128 bits).
///
/// # Extension to other digest protocols
///
/// While [RFC 4226][4226] technically only allows for the HMAC-SHA1 protocol, extensions such as
/// [RFC 6238][6238] (which describes TOTP) allow the use of other protocols. As such, this
/// crate pragmatically exposes a method for using other digest protocols in [the `Digest`
/// trait][`digest::Digest`]. To use other digest functions:
///
/// 1. implement [`Digest`][`digest::Digest`] for your type, and
/// 2. invoke [`Digest::truncate`][`digest::Digest::truncate`] on an instance of the type to
///    generate a [`Token`][`Token`].
///
/// So long as the digest is at least 16 bytes long, this should work without issue.
///
/// For those specifically interested in TOTP, see also [the companion `rfc-6238`
/// crate][rfc-6238].
///
/// [4226]: https://datatracker.ietf.org/doc/html/rfc4226
/// [6238]: https://datatracker.ietf.org/doc/html/rfc6238
/// [rfc-6238]: https://lib.rs/crates/rfc-6238
pub fn hotp<K, C, const DIGITS: u8>(key: K, counter: C) -> Result<Token<DIGITS>, HotpError>
where
    C: Into<u64>,
    K: AsRef<[u8]>,
    Length<DIGITS>: TokenLength,
{
    let key = key.as_ref();
    // R6: "The length of the shared secret MUST be at least 128 bits."
    if key.len() < 16 {
        return Err(HotpError::ShortSecret(key.len()));
    }
    Ok(hmac_sha1(key, counter.into()).truncate::<DIGITS>())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_appendix_d() {
        fn generate(count: u64) -> Token<6> {
            hotp(b"12345678901234567890", count).unwrap()
        }
        assert_eq!(generate(0), Token(755224));
        assert_eq!(generate(1), Token(287082));
        assert_eq!(generate(2), Token(359152));
        assert_eq!(generate(3), Token(969429));
        assert_eq!(generate(4), Token(338314));
        assert_eq!(generate(5), Token(254676));
        assert_eq!(generate(6), Token(287922));
        assert_eq!(generate(7), Token(162583));
        assert_eq!(generate(8), Token(399871));
        assert_eq!(generate(9), Token(520489));
    }
}