为什么 OWASP 不建议在客户端和服务器上都对密码进行 bcrypt?

Why doesn't OWASP recommend to bcrypt the password both on the client and the server?

由于最近 GitHub 和 Twitter 出现问题:

我想知道,为什么最好的做法不是同时在客户端和服务器上加密密码?由于我不会改变任何已经是服务器端最佳实践的东西(盐、强哈希、HTTPS),它只会更安全。服务器会将已经散列的密码视为密码,并在存储之前再次对其进行散列。

任何散列(包括 bcrypt)都需要秘密盐 - 阅读 here 了解更多详情。如果盐丢失了,客户端将无法创建相同的散列——这与丢失密码是一样的。因此,您需要创建一种机制,让您的所有客户都能安全地获取盐。并且您需要确保黑客无法获取这种盐。这实现起来相当复杂。

另一件需要考虑的事情是最终用户设备的限制——例如,Android 设备的 CPU 相当弱,远不及普通服务器强大。由于 bcrypt 的主要优势是计算哈希所花费的时间,因此您需要选择参数,以便好的服务器(甚至可能使用 GPU)能够在较慢的时间内计算它(比方说,> 1s 20 个字符的密码)。这就是制作那些彩虹表如此困难的原因。

所以,除非你能保证你所有的用户都运行在足够强大的设备上,否则不建议在客户端做bcrypt

这个方案的问题在于它需要服务端信任客户端。特别是,它假设客户端总是实际哈希用户输入的内容。如果我们打破这个假设,就像入侵者可能的那样,问题就会开始出现。

Bob 从您的服务器日志中获得了一个(单哈希)密码列表。这些不是明文密码,但也不是密码文件中的双哈希密码。但是假设他对您的客户端做了一个小改动:他删除了 bcrypt() 行,因此在发送之前它不再散列他粘贴到密码字段中的任何内容:相反,它只发送原始文本。

然后他开始发送登录信息。现在您的服务器看到用户名和单哈希密码(因为那是 Bob 键入的,因为那是 Bob 知道的)。它假设这是普通的客户端,所以它继续再次对密码进行哈希处理并检查其文件中的双重哈希密码……并且它被恰好哈希了两次,所以它匹配。 Bob 不知道明文密码,但通过修改客户端,他不需要知道它。

客户端散列可以做到,但我们应该考虑我们真正实现的是什么。

您可能想要实现的是,当通过(希望加密的 SSL)连接发送密码时,攻击者无法读取密码。如果攻击者可以拦截流量,他 (s) 很可能也可以更改它,因此可以剥离任何 JavaScript 进行客户端散列。然后整个保护来自服务器端散列。

可以实现的是,您可以减少服务器负载,因为您让客户端进行繁重的计算。如果您可以保证客户端的完整性,那么您可以在客户端上进行密钥扩展并在服务器上使用快速散列。如果是已安装的应用程序,这可以是一个选项,但不推荐用于网站,因为不能保证客户端的完整性,而且因为 JavaScript 通常较慢,所以你可以少做几轮。

如果攻击者只能侦听流量而不能更改流量,您将获得一点好处。您愿意花在散列上的时间必须分为客户端部分和服务器部分(不能让用户永远等待)。服务器时间必须足够长才能保证安全,留给客户端的时间很少。如果您在客户端使用太快的散列,那么截获的密码散列仍然在暴力破解的范围内(尽管它 攻击者必须克服的障碍)。

所以总之,通常是不值得的,优势太小了,时间还是投入到服务端的hashing-time上比较好。

我一直在寻求解决可以在服务器上登录纯文本密码的类似问题。结论是 如果可以的话,您应该总是另外散列客户端密码

这里有一些关于 client-plus-server hashing 的文章:

Client-Plus-Server Password Hashing as a Potential Way to Improve Security Against Brute Force Attacks without Overloading the Server

Salted Password Hashing - Doing it Right

具体见:

In a Web Application, always hash on the server

If you are writing a web application, you might wonder where to hash. Should the password be hashed in the user's browser with JavaScript, or should it be sent to the server "in the clear" and hashed there?

Even if you are hashing the user's passwords in JavaScript, you still have to hash the hashes on the server. Consider a website that hashes users' passwords in the user's browser without hashing the hashes on the server. To authenticate a user, this website will accept a hash from the browser and check if that hash exactly matches the one in the database. This seems more secure than just hashing on the server, since the users' passwords are never sent to the server, but it's not.

The problem is that the client-side hash logically becomes the user's password. All the user needs to do to authenticate is tell the server the hash of their password. If a bad guy got a user's hash they could use it to authenticate to the server, without knowing the user's password! So, if the bad guy somehow steals the database of hashes from this hypothetical website, they'll have immediate access to everyone's accounts without having to guess any passwords.

This isn't to say that you shouldn't hash in the browser, but if you do, you absolutely have to hash on the server too. Hashing in the browser is certainly a good idea, but consider the following points for your implementation:

Client-side password hashing is not a substitute for HTTPS (SSL/TLS). If the connection between the browser and the server is insecure, a man-in-the-middle can modify the JavaScript code as it is downloaded to remove the hashing functionality and get the user's password.

Some web browsers don't support JavaScript, and some users disable JavaScript in their browser. So for maximum compatibility, your app should detect whether or not the browser supports JavaScript and emulate the client-side hash on the server if it doesn't.

You need to salt the client-side hashes too. The obvious solution is to make the client-side script ask the server for the user's salt. Don't do that, because it lets the bad guys check if a username is valid without knowing the password. Since you're hashing and salting (with a good salt) on the server too, it's OK to use the username (or email) concatenated with a site-specific string (e.g. domain name) as the client-side salt.

经过研究,散列客户端似乎也有明显的安全优势。如果通过 HTTPS 的密码被泄露,或者如果密码被记录在服务器上,那么纯文本密码就不能轻易地在用户的其他帐户上重复使用(许多用户重复使用他们的密码)。

唯一可能的缺点是客户端性能和服务器端密码验证。用户可以操纵您的客户端 JS 并提交 "weak" 密码。服务器不会知道任何更好的。但我认为这是一个小问题,它依赖于人们故意修改他们的客户端代码以削弱他们自己的安全性。