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

前端 未结 3 1607
猫巷女王i
猫巷女王i 2021-02-01 10:59

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

My source is http://tools.ietf.org/html/rfc4226#sec

相关标签:
3条回答
  • 2021-02-01 11:37

    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();
        }
    }
    
    0 讨论(0)
  • 2021-02-01 11:43

    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.

    0 讨论(0)
  • 2021-02-01 11:50

    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.

    0 讨论(0)
提交回复
热议问题