HMAC-based one time password in C# (RFC 4226 - HOTP)
Asked Answered
M

3

6

I am attempting to wrap my brain around generating a 6 digit/character non case sensitive expiring one-time password.

My source is https://www.rfc-editor.org/rfc/rfc4226#section-5

First the definition of the parameters

C       8-byte counter value, the moving factor.  This counter
       MUST be synchronized between the HOTP generator (client)
       and the HOTP validator (server).

K       shared secret between client and server; each HOTP
       generator has a different and unique secret K.

T       throttling parameter: the server will refuse connections
       from a user after T unsuccessful authentication attempts.

Then we have the algorithm to generate the HOTP

As the output of the HMAC-SHA-1 calculation is 160 bits, we must
   truncate this value to something that can be easily entered by a
   user.

                   HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

Then, we have Truncate defined as

String = String[0]...String[19]
 Let OffsetBits be the low-order 4 bits of String[19]
 Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
 Let P = String[OffSet]...String[OffSet+3]
 Return the Last 31 bits of P

And then an example is offered for a 6 digit HOTP

The following code example describes the extraction of a dynamic
binary code given that hmac_result is a byte array with the HMAC-
SHA-1 result:

    int offset   =  hmac_result[19] & 0xf ;
    int bin_code = (hmac_result[offset]  & 0x7f) << 24
       | (hmac_result[offset+1] & 0xff) << 16
       | (hmac_result[offset+2] & 0xff) <<  8
       | (hmac_result[offset+3] & 0xff) ;

I am rather at a loss in attempting to convert this into useful C# code for generating one time passwords. I already have code for creating an expiring HMAC as follows:

byte[] hashBytes = alg.ComputeHash(Encoding.UTF8.GetBytes(input));
byte[] result = new byte[8 + hashBytes.Length];

hashBytes.CopyTo(result, 8);
BitConverter.GetBytes(expireDate.Ticks).CopyTo(result, 0);

I'm just not sure how to go from that, to 6 digits as proposed in the above algorithms.

Mcmillian answered 29/11, 2010 at 20:53 Comment(3)
I believe C is a DateTime stamp and K is the secret Key I have already assigned to each user's account. As to how I correctly hash them and then truncate down to 6 digits is where I'm confused.Mcmillian
Appendix C provides a reference implementation in Java that should be easily translatable to C#.Pellitory
It is, but it only generates a numeric HOTP. I'd really like an alpha-numeric HOTP.Mcmillian
T
3

You have two issues here:

  1. If you are generating alpha-numeric, you are not conforming to the RFC - at this point, you can simply take any N bytes and turn them to a hex string and get alpha-numeric. Or, convert them to base 36 if you want a-z and 0-9. Section 5.4 of the RFC is giving you the standard HOTP calc for a set Digit parameter (notice that Digit is a parameter along with C, K, and T). If you are choosing to ignore this section, then you don't need to convert the code - just use what you want.

  2. Your "result" byte array has the expiration time simply stuffed in the first 8 bytes after hashing. If your truncation to 6-digit alphanumeric does not collect these along with parts of the hash, it may as well not be calculated at all. It is also very easy to "fake" or replay - hash the secret once, then put whatever ticks you want in front of it - not really a one time password. Note that parameter C in the RFC is meant to fulfill the expiring window and should be added to the input prior to computing the hash code.

Toolmaker answered 29/11, 2010 at 23:3 Comment(1)
After digging into it more I had to drop the idea of an expiring OTP all in one. The algorithm uses a counter - that is, it was designed to work independantly on a hardware device (like a dongle/cell phone) and on a validation server. This way, if the counter is the same, the same OTP is always generated - since there is no way to pull the data back out, as there was in my expiring HMAC. But you are right on both counts. To make it expire I have to do more than this algorithm is set up for, and if I want to have an alphanumeric OTP then I need a different algorithm.Mcmillian
M
2

For anyone interested, I did figure out a way to build expiration into my one time password. The approach is to use the created time down to the minute (ignoring seconds, milliseconds, etc). Once you have that value, use the ticks of the DateTime as your counter, or variable C.

otpLifespan is my HOTP lifespan in minutes.

DateTime current = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 
    DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, 0);

for (int x = 0; x <= otpLifespan; x++)
{
    var result = NumericHOTP.Validate(hotp, key, 
        current.AddMinutes(-1 * x).Ticks);

    //return valid state if validation succeeded

    //return invalid state if the passed in value is invalid 
    //  (length, non-numeric, checksum invalid)
}

//return expired state

My expiring HOTP extends from my numeric HOTP which has a static validation method that checks the length, ensures it is numeric, validates the checksum if it is used, and finally compares the hotp passed in with a generated one.

The only downside to this is that each time you validate an expiring hotp, your worse case scenario is to check n + 1 HOTP values where n is the lifespan in minutes.

The java code example in the document outlining RFC 4226 was a very straightforward move into C#. The only piece I really had to put any effort into rewriting was the hashing method.

private static byte[] HashHMACSHA1(byte[] keyBytes, byte[] text)
{
    HMAC alg = new HMACSHA1(keyBytes);

    return alg.ComputeHash(text);
}

I hope this helps anyone else attempting to generate one time passwords.

Mcmillian answered 30/11, 2010 at 22:33 Comment(0)
E
2

This snippet should do what you are asking for:

  public class UniqueId
{
    public static string GetUniqueKey()
    {
        int maxSize = 6; // whatever length you want
        char[] chars = new char[62];
        string a;
        a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
           char[] chars = new char[a.Length];
        chars = a.ToCharArray();
        int size = maxSize;
        byte[] data = new byte[1];
        RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();
        crypto.GetNonZeroBytes(data);
        size = maxSize;
        data = new byte[size];
        crypto.GetNonZeroBytes(data);
        StringBuilder result = new StringBuilder(size);
        foreach (byte b in data)
        { result.Append(chars[b % (chars.Length - 1)]); }
        return result.ToString();
    }
}
Eh answered 1/1, 2012 at 18:7 Comment(1)
Looks good. But char[] chars = new char[a.Length]; gives an error as chars has been defined in the scope already.Forgetmenot

© 2022 - 2024 — McMap. All rights reserved.