客户端BCrypt,将盐和哈希分别存储在MySQL数据库中

信息安全 密码 密码管理 爪哇 数据库 bcrypt
2021-08-13 07:05:40

我正在开发一个带有 MySQL 数据库的 Android 应用程序,用于存储用户登录凭据。在将密码存储在数据库中之前,我使用jBCrypt对密码进行哈希处理。注册时,密码在客户端进行哈希处理,如下所示:

String salt = BCrypt.gensalt();
String hash = BCrypt.hashpw("password", salt).split("\\$")[3];
salt = salt.split("\\$")[3];
hash = hash.substring(salt.length(), hash.length());

在这种情况下,BCrypt.hashpw()会给我哈希

$2a$10$rrll.6qqZFLPe8.usJj.je0MayttjWiUuw/x3ubsHCivFsPIKsPgq

然后我删除参数 ( $2a$10$) 并将前 22 个字符作为盐存储,最后 31 个作为哈希存储在数据库中:

------------------------------------------------------------------------
|  uid  |          salt            |               hash                |
------------------------------------------------------------------------
|   1   |  rrll.6qqZFLPe8.usJj.je  |  0MayttjWiUuw/x3ubsHCivFsPIKsPgq  |
------------------------------------------------------------------------

现在,每当客户想要登录时,他们都会输入他们的用户名和密码,并且只会从数据库中返回salt 。客户端通过调用BCrypt.hashpw()他们的盐来计算他们的哈希:

String salt = "$2a$10$" + returnedSalt;
String hash = BCrypt.hashpw(“password”, salt).split("\\$")[3];
hash = hash.substring(salt.length(), hash.length());

给我:

hash = "0MayttjWiUuw/x3ubsHCivFsPIKsPgq"

这等于存储在数据库中的哈希值。然后客户端将用户名和计算的哈希发送回服务器。如果它们匹配,则用户将登录。

我知道我可以通过直接获取整个 BCrypt 哈希并将其与给定密码进行比较来简化此过程

if (BCrypt.checkpw(“password”, bCryptHash))
  // match

但是将整个散列密码发送给用户进行检查感觉不对。

我知道最好在服务器端散列密码,但是这个解决方案有什么问题吗?我错过了什么吗?

假设我在电话和服务器之间有一个未加密的 HTTP 连接。这会是一个安全的解决方案吗?

3个回答

客户是攻击者。在你的办公室里走来走去,同时念出这句话 144 遍;一定要用小鼓来标点你的措辞。这样,你就会记住它。

在您的服务器中,您正在发送 Java 代码以在客户端上运行。诚实的客户会运行你的代码。没有人强迫攻击者也这样做;并且您使用客户端身份验证正是因为您担心客户端可能是试图冒充普通用户的其他人。从服务器的角度来看,您只能看到发送给客户端的字节以及返回的字节。您无法确保这些字节是用您的代码计算的。攻击者是一个邪恶的恶棍,完全能够设想运行您的代码,而是发送您的服务器期望的答案。

现在考虑一个简单的密码验证方案,您只需将用户的密码存储在数据库中。要进行身份验证,请让用户发送密码,然后将其与您存储的密码进行比较。这很简单。这也很糟糕:它被称为明文密码问题是对数据库的任何简单的只读一瞥(无论是 SQL 注入攻击、被盗的备份磁带、从垃圾箱中检索到的旧硬盘......)都会给攻击者所有的密码。简而言之,这里的错误是在数据库中准确存储从客户端发送时授予访问权限的值。

你提出的方案呢?完全相同的事情。您将哈希值“按原样”存储在数据库中。当客户端发送该确切值时,将授予访问权限。当然,诚实的客户会通过散列密码来发送值。但是,让我们面对现实:许多攻击者不是诚实的人。


现在在客户端做部分散列是有价值的。事实上,良好的密码散列是一场军备竞赛,其中散列是故意进行的昂贵。将一些工作卸载到客户端可能是一件好事。当客户端很弱时,它就不能很好地工作,例如带有 Java 的智能手机,或者更糟糕的是 Javascript(这是完全不同的东西,尽管名称相似)。

在这种情况下,您需要在客户端上运行 bcrypt,并在服务器上存储的不是 bcrypt 输出,而是 bcrypt 输出的散列和一些合理的散列函数(像 SHA-256 这样的快速函数就可以了)。密码P的处理将是客户端上的 bcrypt,然后是结果的 SHA-256,在服务器上计算。这将把大部分 CPU 开销推到客户端上,并且对于它的用途将与原始 bcrypt 一样安全(见下文)。


我知道您想“加密”密码(散列不是加密!),因为您想使用纯 HTTP。你不喜欢 HTTPS 的原因和其他人一样,那就是可怕的 SSL 证书。每年为 SSL 证书支付 20 美元就相当于用洒上柠檬汁的土豆削皮器去除皮肤。

不幸的是,没有逃脱削皮器。正如其他人所说,即使您拥有坚如磐石的身份验证机制,原始 HTTP 仍然不受保护,主动攻击者可以简单地等待用户进行身份验证,然后从该点劫持连接。“主动攻击者”的一个非常常见的例子是那些简单地运行一个假的 WiFi 接入点的人——即一个完全真实的 WiFi 接入点,但他们也保留了在任何时候劫持连接的选项。这是一种攻击模式不能没有延伸过全面的加密协议来对抗所有数据,而不仅仅是初始身份验证方法。此类协议中最简单的一种是 SSL/TLS。任何提供您绝对需要的相同保证的协议也将与 SSL/TLS 一样复杂,并且更难实现,因为与 SSL/TLS 相反,它尚未在客户端软件中实现。

SSL 在那里,只需使用它。至于柠檬,把它吸起来。

(如果财务成本是一个障碍,还有免费的替代品,比如Let's Encryptpki.io。它们是否符合你的要求还有待观察,但如果你真的缺钱,它们值得考虑。)

如果您没有使用安全连接 (https),​​那么您的架构在几个方面存在缺陷:

  • 哈希密码是登录所需的全部:您容易受到重放攻击;
  • 由于连接未加密,因此您发送给客户端的 javascript 可能会被攻击者操纵(攻击者可以让客户端将密码发回给他们)。

如果您想要安全登录,您需要能够保证客户端代码是正确的,并且在网络上没有任何有价值的东西可以被嗅探。

在几乎所有情况下,最好的解决方案是使用 SSL/TLS 安全连接。您应该通过此安全连接发送非散列密码并在服务器端进行散列。

此外,通过删除该$2a$10$部分,您可以防止自己在将来的任何地方增加迭代计数:因为您不存储迭代计数,所以在您决定增加迭代计数后,您无法从新哈希中识别旧哈希。

如果您可以保证客户端代码,理论上您可以使用SRP,但要正确使用它是一件复杂的事情,您很可能会失败)。

如果您通过未加密的 HTTP 连接发送任何内容,请假设它正在被监视并且可能会被 MITM 破坏。

如果有人要 MITM 你,他们可以提供自己的盐并用自己的哈希值进行响应。因此,任何攻击者都可以完全欺骗您的身份验证过程。

您应该对密码服务器端进行哈希处理并使用安全连接。