【问题标题】:HMAC-based one time password in C# (RFC 4226 - HOTP)C# 中基于 HMAC 的一次性密码 (RFC 4226 - HOTP)
【发布时间】:2011-05-17 12:27:03
【问题描述】:

我正在尝试生成一个 6 位/字符不区分大小写的一次性到期密码。

我的来源是https://www.rfc-editor.org/rfc/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 位数字。

【问题讨论】:

  • 我相信 C 是一个日期时间戳,而 K 是我已经分配给每个用户帐户的密钥。至于我如何正确地对它们进行哈希处理,然后将其截断为 6 位,这让我感到困惑。
  • 附录 C 提供了一个 Java 参考实现,应该很容易翻译成 C#。
  • 是的,但它只生成一个数字 HOTP。我真的很想要一个字母数字的 HOTP。

标签: c# hmac one-time-password


【解决方案1】:

这里有两个问题:

  1. 如果您正在生成字母数字,则不符合 RFC - 此时,您可以简单地获取任意 N 个字节并将它们转换为十六进制字符串并获取字母数字。或者,convert them to base 36 如果您想要 a-z 和 0-9。 RFC 的第 5.4 节为您提供了一组 Digit 参数的标准 HOTP 计算(请注意,Digit 是与 CKT 一起的参数)。如果您选择忽略此部分,则无需转换代码 - 只需使用您想要的。

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

【讨论】:

  • 在深入研究之后,我不得不放弃将过期 OTP 合二为一的想法。该算法使用一个计数器——也就是说,它被设计为在硬件设备(如加密狗/手机)和验证服务器上独立工作。这样,如果计数器相同,则始终会生成相同的 OTP - 因为无法将数据拉回,就像我即将到期的 HMAC 一样。但你在这两个方面都是对的。为了让它过期,我必须做的不仅仅是这个算法的设置,如果我想要一个字母数字 OTP,那么我需要一个不同的算法。
【解决方案2】:

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

otpLifespan 是我的 HOTP 生命周期(以分钟计)。

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 扩展而来的,它有一个静态验证方法来检查长度,确保它是数字的,如果使用了校验和,则验证校验和,最后将传入的 hotp 与生成的 hotp 进行比较。

唯一的缺点是每次验证过期的 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);
}

我希望这对尝试生成一次性密码的其他人有所帮助。

【讨论】:

    【解决方案3】:

    这个 sn-p 应该可以满足您的要求:

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

    【讨论】:

    • 看起来不错。但是char[] chars = new char[a.Length]; 给出了一个错误,因为 chars 已经在作用域中定义了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-23
    • 2020-05-04
    • 1970-01-01
    • 2012-01-24
    • 1970-01-01
    • 1970-01-01
    • 2022-01-20
    相关资源
    最近更新 更多