基于 HMAC 的请求签名 - 存储盐

信息安全 密码学 密码 验证 哈希 hmac
2021-09-08 22:41:49

我正在开发一个(非高安全性)项目,该项目目前尚未上线,但可能会在某个时候上线。

我们有一个在服务器和一个 Android 客户端应用程序上运行的 REST API(使用 Restlet 和 Neo4j 实现)。

为了保持无状态,我们需要将身份验证信息与每个请求一起发送。我们决定使用基于 MAC 的方法:我们使用 HMAC-SHA256 算法对一组标头值以及每个 HTTP 请求的实体主体进行签名。然后,我们将该哈希与用户名放在Authorization请求的标头中。

对于 HMAC 机密,我们使用用户选择的密码。为了使事情更安全,我们在使用密码之前对密码进行哈希处理并将其存储在客户端中。

当然,为了让服务器能够验证 HMAC-SHA256 标头,我们需要传输一次秘密。但由于这种情况只发生一次,因此安全风险并不大。

此设置的问题是用户密码的安全存储。我知道在不使用盐来防止彩虹表攻击的情况下,永远不应该对密码进行哈希处理。但是当我生成一个安全/随机盐时,第一次登录用户的客户端无法访问该盐以生成正确的哈希值。

在这种情况下,最好的决定是什么?我是否应该使用“常规”散列算法(如 SHA256)而不使用盐来简单地存储密码?我想那将是非常疏忽的。我还可以使用更安全的算法,如 bcrypt 或 PBKDF2 和“虚拟盐”,如用户名用户名的 MD5 哈希值和更高的迭代次数。即使知道盐,生成彩虹表也会非常缓慢和痛苦。

使用“隐蔽安全”盐的 bcrypt 是否足够安全?还是整个设置不安全,应该用另一种身份验证方法进行交换?

3个回答

您应该做的最重要的事情是:使用 SSL使用 HTTPS,而不是 HTTP。

理由:

  • 如果您使用 SSL,您的方法是合理的。

    • 战术细节:我建议使用具有较大迭代次数的 PBKDF2 从密码生成 HMAC 密钥。您可以使用固定值(或用户名的哈希)作为盐。

    • 这肯定有一些弱点:由于您在客户端设备上存储了密码的哈希值,因此获得移动设备访问权限的人可以使用字典攻击来尝试恢复密码,他们可能成功也可能不成功,具体取决于密码有多强。此外,任何获得移动设备访问权限的人都可以学习加密密钥,这是提交经过身份验证的 HTTP 请求所需的全部内容。在许多情况下,这种弱点可能是可以接受的风险。

  • 如果您不使用 SSL,您的方案可能会出现严重的安全问题。任何可以嗅探到网络的人都可以学习密码的哈希值,如果密码不强,他们可以使用字典搜索来恢复用户的密码。


如果您想要更好的安全性,那么这里有一个更强大的方法:

  • 始终使用 SSL。 不要乱用未加密的 HTTP;一切都使用 HTTPS。

  • 使用全强度加密密钥。让服务器生成用于 HMAC 的全强度 128 位加密随机密钥。服务器可以存储此密钥。

    • 客户端第一次连接时,它需要学习这个加密密钥。为此,有一个特殊请求,客户端提供其用户名和密码,并在密码正确时接收加密密钥作为响应。(服务器可以对这些请求进行速率限制,以阻止字典攻击。)当客户端收到加密密钥时,它会将其存储在本地。

    • 对于每个后续请求,客户端可以查找其本地存储的加密密钥副本并使用它来计算 HMAC。

    • 如果用户从新设备登录网站,没问题,该设备只需通过上述初始化步骤来检索加密密钥,然后一切都会继续工作。

这避免了在终端设备上存储用户密码或用户密码哈希的需要。它还可以防止针对加密密钥的字典搜索攻击;由于加密密钥是一个完全随机的值,它是独立于密码随机选择的,因此针对密码的离线字典搜索攻击在检索密钥时不会成功。但是,它确实保留了一个弱点,即如果坏人获得客户端设备,他们将能够学习加密密钥并因此发出伪造请求。考虑到你的目标,我怀疑这和你能做的一样好。

我不确定我是否完全理解你的问题。

salt 通常以 bcrypt 字符串表示形式编码。

但是,使用用户名作为 salt 可能无法正常工作,因为 bcrypt 期望 16 个字节作为 salt。因此,您需要先转换用户名。我不明白的是,为什么双方都不能有盐?正如你所说,盐只是为了防止彩虹表攻击,所以你甚至可以在没有安全问题的情况下传输它。

如果您使用散列密码作为 HMAC 密钥,那么您的散列将成为共享密钥。散列(加盐或不加盐)将保护密码,以防它在其他地方使用,但它不会保护您的服务,因为散列可以直接使用。

我认为最好将 hmac 密钥视为凭据,令牌。你不能存储那些真正安全的。当然,无论如何你都应该把它们放在安全的地方。

而不是“一次发送密码”,我建议您使用类似于 OAUTH 令牌的东西来派生临时秘密,或者您可以简化并在服务器上生成“访问密钥”并允许用户复制它们(这是例如亚马逊 AWS 所做的)。如果这是一个随机字符串,则没有泄露用户密码的危险(当然,只是凭据)。

另一种方法是使用 SRP 之类的东西,它会产生一个共享的预主密钥,可用于您的请求签名密钥。