65.9K
CodeProject 正在变化。 阅读更多。
Home

一次性密码 (OTP) 揭秘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (35投票s)

2013年5月13日

CPOL

8分钟阅读

viewsIcon

248292

downloadIcon

8930

本文介绍了 OTP 生成器的工作原理。

引言

2004 年初,我与 Gemplus 的一个小团队合作进行 EAP-SIM 身份验证协议。由于我们比市场领先一步,我们的团队被重新分配与 Verisign 的团队合作开发一种新的身份验证方法:OTP 或一次性密码。

当时,现有的 OTP 是 RSA 的一种令牌,它使用时钟来同步密码。

Versign 的实验室提出了一个非常简单但我认为非常聪明的概念。您现在可能在银行或 Google 中使用的 OTP 就此诞生了。

在接下来的两篇文章中,我将介绍这个算法和身份验证方法。在这篇文章中,我将提供一个完整的 OTP 生成器代码。它与我 2004 年开始在 Versign 实验室研究这个概念时编写的 Javacard Applet 非常相似。

一次性密码生成器

这个 OTP 基于非常流行的 HMAC SHA 算法。HMAC SHA 是一种通常用于通过质询响应进行身份验证的算法。它不是加密算法,而是将一组字节转换为另一组字节的哈希算法。此算法是不可逆的,这意味着您无法使用结果回溯到源。

HMAC SHA 使用密钥来转换输入的字节数组。密钥是绝不能被黑客访问的秘密,而输入是质询。这意味着 OTP 是一种质询响应身份验证。

密钥至少必须为 20 字节;质询通常是一个 8 字节的计数器,这意味着在值用完之前还有相当长的时间。

该算法将 20 字节的密钥和 8 字节的计数器生成一个 8 位数字。这意味着在 OTP 生成器的生命周期中必然会有重复,但这无关紧要,因为不会连续出现重复,而且 OTP 只在几分钟内有效。

为什么 OTP 是一种非常强大的身份验证方法?

这种方法非常强大有几个原因。

  • 密钥是 20 位
  • 密码是计数器/密码的组合,仅一次有效且有效期很短
  • 生成每个密码的算法是不可逆的
  • 使用 OTP 令牌时,密钥是硬件保护的
  • 如果 OTP 在您的手机上接收,密钥始终保留在服务器上

这些特性使 OTP 成为一种强大的身份验证协议。身份验证中的弱点通常是人为因素。很难记住许多复杂的密码,因此用户经常在整个互联网上使用相同的密码,而且并不是真正强大的密码。使用 OTP,您无需记住密码,最多只需记住 PIN 码(4 到 8 位数字),前提是 OTP 令牌受到 PIN 码保护。如果是通过手机发送的 OTP,它会受到手机安全性的保护。PIN 码很短,但通常在令牌被锁定之前,您最多只能尝试 3 次。

OTP 的弱点(如果存在)是用于生成或接收 OTP 的介质。如果用户丢失了它,身份验证可能会受到损害。可能的解决方案是使用生物识别凭证来保护该设备,使其 virtually totally safe。

OTP 生成器的代码如下

public class OTP
{
    public const int SECRET_LENGTH = 20;
    private const string
	MSG_SECRETLENGTH = "Secret must be at least 20 bytes",
	MSG_COUNTER_MINVALUE = "Counter min value is 1";

    public OTP()
    {
    }

    private static int[] dd = new int[10] { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }; 

    private byte[] secretKey = new byte[SECRET_LENGTH] 
    {
	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
	0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43
    };

    private ulong counter = 0x0000000000000001;

    private static int checksum(int Code_Digits) 
    {
	int d1 = (Code_Digits/1000000) % 10;
	int d2 = (Code_Digits/100000) % 10;
	int d3 = (Code_Digits/10000) % 10;
	int d4 = (Code_Digits/1000) % 10;
	int d5 = (Code_Digits/100) % 10;
	int d6 = (Code_Digits/10) % 10;
	int d7 = Code_Digits % 10;
	return (10 - ((dd[d1]+d2+dd[d3]+d4+dd[d5]+d6+dd[d7]) % 10) ) % 10;
    }

    /// <summary>
    /// Formats the OTP. This is the OTP algorithm.
    /// </summary>
    /// <param name="hmac">HMAC value</param>
    /// <returns>8 digits OTP</returns>
    private static string FormatOTP(byte[] hmac)
    {
	int offset =  hmac[19] & 0xf ;
	int bin_code = (hmac[offset]   & 0x7f) << 24
		| (hmac[offset+1] & 0xff) << 16
		| (hmac[offset+2] & 0xff) <<  8
		| (hmac[offset+3] & 0xff) ;
	int Code_Digits = bin_code % 10000000;
	int csum = checksum(Code_Digits);
	int OTP = Code_Digits * 10 + csum;

	return string.Format("{0:d08}", OTP);
    }

    public byte[] CounterArray
    {
	get
	{
	    return BitConverter.GetBytes(counter);
	}

	set
	{
	    counter = BitConverter.ToUInt64(value, 0);
	}
    }

    /// <summary>
    /// Sets the OTP secret
    /// </summary>
    public byte[] Secret
    {
	set
	{
	    if (value.Length < SECRET_LENGTH)
	    {
		throw new Exception(MSG_SECRETLENGTH);
	    }

	    secretKey = value;
	}
    }

    /// <summary>
    /// Gets the current OTP value
    /// </summary>
    /// <returns>8 digits OTP</returns>
    public string GetCurrentOTP()
    {
	HmacSha1 hmacSha1 = new HmacSha1();

	hmacSha1.Init(secretKey);
	hmacSha1.Update(CounterArray);
		
	byte[] hmac_result = hmacSha1.Final();

	return FormatOTP(hmac_result);
    }

    /// <summary>
    /// Gets the next OTP value
    /// </summary>
    /// <returns>8 digits OTP</returns>
    public string GetNextOTP()	
    {
	// increment the counter
	++counter;

	return GetCurrentOTP();
    }

    /// <summary>
    /// Gets/sets the counter value
    /// </summary>
    public ulong Counter
    {
	get
	{
	    return counter;
	}

	set
	{
	    counter = value;
	}
    }
}

FormatOTP()checksum() 方法是 OTP 算法的核心。这些方法将 hmacsha 的结果转换为 8 位 OTP。

附件代码还包含 HMAC SHA 算法的实现。当然可以使用 .NET Framework 的标准 hmacsha,但我提供的代码实际上使用的是一个运行 .NET CLR 的原型智能卡中的演示。在我编写这段代码时,卡上尚未实现 cryptography 命名空间。

这样,您还可以了解 hmacsha 算法的实现方式。

OTP 服务器和身份验证协议

  通常有两种方法可以使用 OTP 进行身份验证。我将描述在线银行网站身份验证的实际情况。我只想明确一点。您不能使用我将在本文中描述的内容来破解银行网站!相反,阅读本文后,您应该会理解为什么使用 OTP 作为第二种身份验证因素是极其安全的。

OTP 本身已经非常安全,至少有以下两个原因:

  • 您无法重复使用
  • 您无法回溯源头。

第二个特性在安全性方面非常重要。OTP 取决于 2 个参数:

  • 一个秘密密钥
  • 一个计数器

即使黑客截获了数百万个 OTP,该算法也是不可逆的,这意味着即使您知道密钥,也无法回溯用于生成 OTP 的计数器。因此,没有密钥和计数器,即使拥有数百万个 OTP,也几乎不可能找到一个模式来猜测密钥和当前的计数器值。

与许多安全协议一样,OTP 的强度取决于所使用的加密算法的质量,在本例中为 HMACSHA1,它是一种经过验证的质询响应算法。可以使用其他 HMAC 算法代替 HMACSHA,因为随着 CPU 功耗的增加,加密算法也必须变得更强。这可以通过增加密钥大小或重新设计算法本身来完成。

OTP 通常用于执行身份验证或验证信用卡交易。对于交易,OTP 会发送到用户的手机;对于身份验证,可以使用安全令牌,或请求将 OTP 发送到用户的手机。

使用发送到手机的 OTP

这通常是在使用 OTP 验证交易时使用的身份验证方法。银行系统会向您发送一个 OTP,您然后在几分钟内输入该 OTP。此机制不需要任何同步过程,因为 OTP 最初是由服务器生成并发送到第三方设备的。服务器期望您在通常 2 分钟内输入正确的 OTP。如果您未能做到,只需请求一个新的 OTP,然后在指定时间内输入。

当一个系统同时支持这两种身份验证方法时,这意味着后端有两个不同的密钥和计数器:一对用于 OTP 令牌,另一对用于通过 SMS 传输的 OTP。

使用 OTP 令牌

我最初从事的项目之一是在 Javacard 中实现了 OTP 的早期版本,使用的是带屏幕的 OTP 令牌或带有卡片 applet 的手机来生成 OTP。在此模型中,服务器和身份验证令牌都必须生成一个必须同步的 OTP。

过程如下:用户用其令牌生成 OTP,输入并按“确定”。服务器接收令牌生成的 OTP,然后递增计数器并生成新的 OTP。

这就是可能出现同步问题的地方。
 
同步问题

如果用户输入了正确的 OTP,那么服务器在递增计数器并计算 OTP 时,身份验证将成功。

现在可能出现一些导致服务器计数器失步的情况,导致身份验证机制无法工作。在某些情况下,可以自动重新同步计数器,但在某些情况下,用户需要使用特定过程手动重新同步服务器计数器。

可能出现几次失步的情况: 

  1. 用户意外按下令牌的生成按钮,但未执行身份验证。
    在这种情况下,令牌的计数器将比服务器计数器领先几步。
  2. 用户在未从令牌生成 OTP 的情况下输入 OTP。在这种情况下,服务器的计数器将比令牌计数器领先。
  3. 用户使用令牌生成 OTP,但输入了错误的 OTP。 

如果用户提供的 OTP 与服务器的不匹配,服务器可以尝试通过尝试预期计数器周围的几个计数器值来自动重新同步。在我们的服务器中,我们会使用名义计数器值周围的 10 个值。如果无法同步,服务器将保留当前的计数器值,以避免进一步使服务器失步。

但是,服务器必须实施一种策略来告知服务器和令牌已完全失步,并且必须执行手动同步。

手动同步过程

服务器可以向用户提供手动同步过程。OTP 数字只有 8 位,由 8 字节计数器的哈希值生成,并格式化为 20 字节的结果。这意味着对于 2 个不同的计数器值,有可能获得相同的 OTP。因此,仅使用一个 OTP 值尝试同步并不可靠。手动重新同步过程需要用户输入 2 个连续的 OTP,然后服务器可以尝试找到所需的序列,因为对于不同的计数器值获得相同的 2 个 OTP 序列的概率极低,如果不是零的话。

获取源代码

您可以从文章附带的 ZIP 文件中获取项目源代码,也可以在 GitHub 上关注它,因为它会定期更新,因为这是一个公共存储库。

关注点

OTP 是一种流行且相当简单的身份验证方法;我希望这些文章能帮助您了解它的幕后工作原理。

© . All rights reserved.