SMS 身份验证:随机 OTP 或加密的

信息安全 密码 随机的 多因素 短信 一次性密码
2021-08-20 18:14:53

我正在使用 SMS 添加双重身份验证来增强现有的登录过程。由于我不使用物理令牌,我想知道什么被认为是最安全的:

发送使用某种基于时间的一次性密码 (TOTP) 算法生成的随机 8 个字符串或 8 位哈希。

我自己的考虑如下: 不必存储使用 TOTP 生成的哈希以检查用户是否正确输入。随机生成的令牌确实需要存储并链接到用户帐户和时间戳。

然而,随机生成字符串的过程似乎更容易,并且似乎有更少的易受攻击点。

4个回答

如果处理得当,基于时间的一次性密码将相当安全。这是一个比通常假设更大的“如果”。

什么会起作用:

  1. 确定您的时间粒度,例如 5 分钟。此处考虑的所有日期都将是该粒度的倍数(即 8:05:00、17:25:00... 但不是 16:34:00)。

  2. 生成适当大小的秘密对称密钥K/dev/urandom (直接来自、或的 16 个字节CryptGenRandom()具体取决于您的宗教信仰)。java.security.SecureRandomSystem.Security.Cryptography.RNGCryptoServiceProvider

  3. 在时间T为用户U生成一次性密码P时,计算:

    P = 编码(HMAC/SHA-256( U || T , K ))

    这意味着您将用户名和当前日期“连接”成一个自定义结构(XML、ASN.1、带分隔符的文本......只要符合您的要求,只要它是确定性的且不含糊的),结束你计算一个MAC,特别使用HMAC和 SHA-256 作为底层散列函数,K作为密钥。encode()函数会截断 HMAC 的输出并将其编码为可以在手机上显示并由用户输入的内容。

  4. 您将P发送给用户。在时间T'(大概在T之后不久),用户回来并输入密码P'然后,您针对同一用户U重新计算时间T'和时间T'-5minP如果任一值匹配P',则验证成功,否则失败。

棘手的点:

  • 你不能截断“太多”。攻击者可能想试试运气并发送一个随机密码;可能的密码空间必须足够宽,以使这种成功非常不可能。

  • 你可以使用任何你想要的“编码”,只要它是确定性的并且大部分是统一的。例如,将 HMAC 值(一个 256 位字符串)解释为一个大整数(在0 .. 2 256 -1范围内),然后将该整数除以 100000000 的余数;然后,您可以用十进制表示该值(必要时用零填充),以获得一个不错的 8 位密码。余数运算是“截断”,在这种情况下它将是足够无偏的。如果您更喜欢 8 个字母,请除以 26 8并使用基数 26 来表示余数。

  • 粒度与安全​​性相关联。请注意,在验证时,我们只尝试了两个密码(我们需要这样做,因为即使用户在 11:34 收到他的 SMS 并在 11:36 输入它,密码也必须是有效的)。这将攻击者的难度除以 2:如果密码是 8 位序列,则有 100000000 个可能的密码,但攻击者成功的概率为 1/50000000。5 分钟的粒度意味着一次性密码的有效期为 5 到 10 分钟。如果我们想让这个生命周期更精确(例如 5 到 6 分钟),那么我们必须降低粒度(例如降低到 1 分钟)并接受更多密码(对于T'T'-1T'-2 ...到T'-5) 提高了攻击者的成功率(此处为 1/16666667)。这里有一个权衡:更高的精度意味着更少的安全性。在我看来,最好保持高安全性并允许密码寿命在 5 到 10 分钟范围内自由变化。

    请注意,这个问题粒度正是基于时间的密码的安全性与随机选择的密码的安全性不同的点。如上所述,对于某些N(至少 2 个) ,您需要接受N个连续密码,攻击者成功的机会是相同大小的随机密码的N倍。

  • 对于多前端系统,密钥K必须共享,因为收到第一个请求并生成密码的服务器不一定与收到第二个请求并必须验证密码的服务器相同;但两者必须使用相同的密钥。此外,所有前端必须具有相同的时间概念(使用NTP)。

  • 密钥K不需要永久存储;它在重新启动后无法幸存是可以接受的,因为生成的密码无论如何都是短暂的。你可以在服务器启动时生成K(这意味着重启前短信发送的密码在重启后不会被接受,这可能没什么大不了的)。但是请注意,瞬态密钥对于多前端系统可能会很麻烦(前端必须以某种方式使用相同的密钥)。

  • 您想添加一些保护机制来避免破坏攻击:要求对给定用户U进行一百万次身份验证的人。用户U不希望收到一百万条虚假短信;而且当然不想为发送一百万条短信付费(这些东西不是免费的)。一种可能的对策是在验证用户的主要(非移动)密码(或双因素系统中的第一个身份验证因素)之后,仅将基于 SMS 的密码用作第二个身份验证步骤。

我建议您使用随机值。这是最简单的方法。在安全方面,简单就是好的。

您可以使用 TOTP,但何必呢?(您提到它们不必存储在数据库中,但那又怎样?您有数据库;为什么不使用它?)如果您正确实施 TOTP 解决方案,它也可以是安全的,但它更复杂,并且有犯错的机会更多。只需见证@Thomas Pornin 答案的长度。

底线:亲吻。使用随机值。

如果您使用高熵随机生成的服务器端秘密、客户端已知的时间戳以及用户名串联的加密哈希,我认为该方法没有缺陷。当然,我不会使用 8 位数字(例如,1 亿分之一的随机绕过机会)或 8 位十六进制数字(约 40 亿分之一的机会),但会采用加密哈希并将其转换为 base-36 (小写字母加数字)(2 万亿分之一)或 base-64(100 万亿分之一)并取前 8 个字符(尽管最好是更多字符)。注意 base64 对 UX 目的来说,清理一些经常混淆的字符可能是有意义的;例如,不要让用户区分 a Iorl1, Oor0(在看似随机的字符串中)和可能 strip +/来自 base64。您还必须确保在用户输入 OTP 的屏幕上将 time_str 和 user_name 传回。

还要确保以恒定时间方式比较 time_str(否则通过计时攻击可以通过反复试验找出令牌),如果 OTP 密码不在有效时间段内,则过期密码。请注意,如果您担心 100 万亿分之一的随机入侵机会不够安全,您还可以跟踪来自 IP 地址的错误登录尝试,并在大约 5 到 10 次错误尝试后阻止它们/要求验证码/速率限制它们。

这是一些示例python代码:

import hashlib
import time
import base64

secret_str = 'pcA2Sh1e2ovxzjcih4OUiGKHBzytB8FaVScTo0iQ'
time_str = str(time.time())
user_name = 'drjimbob'

def get_hash_from_time_username(time_str, user_name):
    hash = hashlib.sha512(secret_str+time_str+user_name).digest()
    b64_hash = base64.b64_encode(hash) 
    # starts as 86 characters long (excluding `=` at end)
    for ch in ['0','O','1','I','l','+','/', '=']:
        b64_hash = b64_hash.replace(ch,'')
    b64_hash = b64_hash[:8] 
    # overwhelming odds roughly ~1 in 10^60 it will be shorter than 8 chars
    if len(b64_hash) < 8: # in very rare exception repeat the hash.
        b64_hash = (b64_hash + b64_hash)[:8]
    return b64_hash

def check_from_time_username(input_one_time_pass, time_str, user_name):
    cur_time = time.time()
    if cur_time < int(time_str): # time_str in future; user changed time_str
        return False
    if cur_time - 3600 > int(time_str): # time_str is more than an hour old
        return False
    if len(input_one_time_pass) < 8:
        return False
    b64_hash = get_hash_from_time_username(time_str, user_name)
    is_ok = 0 
    for i in range(8):
        is_ok += ord(b64_hash[i]) ^ ord(input_one_time_pass[i])
        # bitwise compare to have constant time string comparison
    return is_ok == 0

如果您通过 SMS 等未加密、不安全的渠道发送 OTP,那么生成 OTP 的方法将无关紧要。SMS 就像没有 PGP 可能性的外包电子邮件。有关这方面的一些信息,请参阅此博客文章:http: //www.wikidsystems.com/WiKIDBlog/another-nail-for-sms-authentication/why-using-sms-for-authentication-is-a-bad-主意