4
\$\begingroup\$

Following Wikipedia: Time-based one-time password and Wikipedia: HMAC-based one-time password, is this Rust implementation of the TOTP/HOTP algorithm correct?

As far as I can see, this matches the Implementation from the Node OTPAuth library, however when testing using an Online Tool I am getting a different OTP. Leading me to wonder if there is an issue with my implementation, or if this is just a difference in the underlying SHA1 implementaiton between Rust and Node.

use std::time::{SystemTime, UNIX_EPOCH}; use hmac::{Hmac, Mac}; use sha1::Sha1; type HmacSha1 = Hmac<Sha1>; pub fn gen_hotp(secret_key: &str) -> u32 { let mut mac = HmacSha1::new_from_slice(secret_key.as_bytes()).unwrap(); mac.update(&get_counter().to_be_bytes()); let to_truncate = mac.finalize().into_bytes(); truncate(to_truncate.as_slice()) } /// Calculate `C` using seconds since UNIX Epoch, with a period of `30s` fn get_counter() -> u64 { let secs = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("We should not be behind the Epoch.") .as_secs() as f64; f64::floor(secs / 30.0) as u64 } /// Truncate the HMAC fn truncate(mac: &[u8]) -> u32 { let byte_offset: usize = (mac[19] & 0x0F) as usize; let result = (((mac[byte_offset + 0]) as u32) << 24) | (((mac[byte_offset + 1]) as u32) << 16) | (((mac[byte_offset + 2]) as u32) << 8) | (((mac[byte_offset + 3]) as u32) << 0); (result & 0x7FFF_FFFF) % 1_000_000 } 

Crates used:

\$\endgroup\$
0

    1 Answer 1

    1
    \$\begingroup\$

    Do not deploy this code in production, as secure code requires greater care and attention to detail than we see evidenced here. Writing and understanding correct code is certainly part of deploying a secure system. But writing and understanding English language technical descriptions of the system is also enormously important.

    We desire a secure System, which is comprised both of complex elements that are plugged in, and complex interacting humans.

    what spec?

    is this Rust implementation of the TOTP/HOTP algorithm correct?

    No, it is not.

    It is, rather, a correct implementation of the HOTP algorithm. gen_hotp is a well-chosen identifier, which explains the OP code has nothing to do with time-based OTP.

    Oh, wait, we're not accepting a counter parameter -- we're consulting the clock to derive that value. So this is a mis-named TOTP implementation?!?

    standard algorithm

    if this is just a difference in the underlying SHA1 implementation between Rust and Node?

    There is no difference. One obtains identical SHA1 results from any implementation in any language, if it's worth calling "an implementation" at all. That's the whole point of having a standard hash algorithm.

    There are standard test vectors for standard algorithms. For example FIPS-180 specifies that a 3-byte input of "abc" shall hash to a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d.

    test vector

    Appendix D of the HOTP spec explains that for a shared secret of "12345678901234567890", the first three truncated HOTP results shall be 755224, 287082, and 359152.

    unit tests

    The OP code contains no automated tests, and it desperately needs some. Rust offers excellent crates for unit testing. Verify, e.g., that count=2 yields a 359152 result.

    Section 4.2 wants you to verify that in 2039 the implementation will still let folks log in:

    The implementation of this algorithm MUST support a time value T larger than a 32-bit integer when it is beyond the year 2038.

    Section 5.2 talks about validating against the window of past timestamps with language like "should" and "recommend", but the OP code has no support for any of that, and no comments explaining the absence.

    This code implements, at best, "core" TOTP functions. It includes none of the machinery to support requirements like "The verifier MUST NOT accept the second attempt of the OTP ...", nor for throttling brute force guesses.

    magic numbers

    There are several parameters hard coded here. That's fine. But they deserve names.

    The 30s period is seldom changed in practice, but it's worth at least defining a constant for it. Section E. 5. suggests a 64s period, for example.

    The very nicely spelled 1_000_000 corresponds to truncation to six digits, the bare minimum mandated by the spec. Consider that a caller of this library might alternately wish to verify seven or eight digits.

    \$\endgroup\$

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.