C#中基于HMAC的一次性密码(RFC 4226 – HOTP)

我试图围绕生成一个6位/字符非大小写敏感的一次性密码到期。

我的来源是http://tools.ietf.org/html/rfc4226#section-5

首先是参数的定义

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. 

然后我们有算法来生成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)) 

然后,我们将Truncate定义为

 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 

然后为6位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) ; 

我宁愿在尝试将其转换为有用的C#代码以生成一次性密码时感到茫然。 我已经有了创建过期HMAC的代码,如下所示:

 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); 

我只是不确定如何从那里,到上述算法中提出的6位数。

你有两个问题:

  1. 如果您正在生成字母数字,那么您就不符合RFC – 此时,您可以简单地取任何N个字节并将它们转换为hex字符串并获得字母数字。 或者,如果你想要az和0-9, 将它们转换为36 。 RFC的5.4节为您提供了设置Digit参数的标准HOTP计算(请注意, DigitCKT )。 如果您选择忽略此部分,则无需转换代码 – 只需使用您想要的内容即可。

  2. 您的“结果”字节数组在散列后的前8个字节中只有填充的到期时间。 如果您截断为6位字母数字不会将这些与哈希的部分一起收集,那么根本不能计算它。 它也很容易“伪造”或重放 – 将秘密哈希一次,然后在你面前放置你想要的任何刻度 – 而不是真正的一次性密码。 请注意,RFC中的参数C旨在满足到期窗口,并应计算哈希码之前添加到输入中。

对于任何有兴趣的人,我确实想出了一种方法来将过期构建到我的一次性密码中。 方法是使用创建的时间到分钟(忽略秒,毫秒等)。 获得该值后,使用DateTime的刻度作为计数器或变量C.

otpLifespan是我的HOTP寿命, otpLifespan几分钟。

 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 

我的过期HOTP扩展自我的数字HOTP,它具有一个静态validation方法,用于检查长度,确保它是数字,validation校验和是否使用,最后将传入的hotp与生成的hotp进行比较。

唯一的缺点是,每次validation即将到期的hotp时,最糟糕的情况是检查n + 1个HOTP值,其中n是以分钟为单位的寿命。

文档中描述RFC 4226的Java代码示例是一个非常直接的C#进程。 我真正需要努力重写的唯一部分是散列方法。

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

我希望这可以帮助其他人尝试生成一次性密码。

这段代码应该满足您的要求:

  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(); } }