在使用密码散列的通常网络应用程序中,用户密码是在将其发送到服务器之前在客户端进行散列,还是在没有散列的情况下作为纯文本密码的加密发送到服务器?
我的主要问题是,如果攻击者发现了散列密码,那么为什么他们不能将散列发送到服务器以在客户端散列的情况下进行身份验证?
在使用密码散列的通常网络应用程序中,用户密码是在将其发送到服务器之前在客户端进行散列,还是在没有散列的情况下作为纯文本密码的加密发送到服务器?
我的主要问题是,如果攻击者发现了散列密码,那么为什么他们不能将散列发送到服务器以在客户端散列的情况下进行身份验证?
至少部分散列必须发生在服务器端。实际上,无论客户端发送给服务器的任何内容都授予访问权限,因此,如果该相同的值只是按原样存储在服务器上,那么您将遇到与纯文本密码存储相同的问题,即在您的服务器上进行一次只读瞥见数据库提供所有访问权限。
在大多数情况下,所有哈希都在服务器上完成,因为客户端是一个 Web 浏览器,充其量只能执行一些 Javascript,而 Javascript 不太擅长加密任务。客户端自己发送密码(当然是通过 HTTPS),服务器计算哈希值,并将其与存储的哈希值进行比较。攻击者不能简单地“传递哈希”;他必须发送一个值,该值在由服务器进行散列时产生存储的散列值。
如果客户端擅长加密(例如游戏的重型本地客户端),那么大部分散列过程可以在客户端完成,但仍然需要在服务器上完成最终的散列计算。
请注意,在所有片段中,我有意排除安全功能,因为我想保持紧凑。不要依赖这些代码片段做任何事情。
在使用密码散列的通常网络应用程序中,用户密码是在将其发送到服务器之前在客户端进行散列还是在没有散列的情况下作为纯文本密码的加密发送到服务器?
不,它是散列服务器端。您通过纯文本或 HTTPS 将密码发送到服务器。然后,服务器将为密码生成一个哈希,如下所示:
public void RegisterAccount(string username, string password, string email)
{
if (!Database.EmailExists(email) && !Database.UsernameExists(username))
{
Database.AddNewUser(username, Hash.GenerateHash(password), email);
}
}
假设这Database.AddNewUser(string, string, string);
会将用户名、散列密码和电子邮件插入数据库。哈希是根据密码生成的。
假设您输入“请不要破解我”作为密码。bcrypt 会将其转换为$2a$08$BOHyNdXAVkWOUgPG/hvsjew7ZpngqJIgueY6m76xd4y3UllXUZLBy
,或基于盐的其他一些变体。数据库现在看起来像这样:
+--------------+--------------------------------------------------------------+--------------------+
| user | password | email |
+--------------+--------------------------------------------------------------+--------------------+
| markbuffalo | $2a$08$BOHyNdXAVkWOUgPG/hvsjew7ZpngqJIgueY6m76xd4y3UllXUZLBy | mark@buffalo.gov |
+--------------+--------------------------------------------------------------+--------------------+
通常,您将调用数据库来读取该用户名的密码。调用数据库后,您将使用一个函数,例如Hash.ValidateHash()
,来查看它是否匹配。如果匹配,您可以使用该非散列密码登录。
你可以在一些非常不幸的情况下。
我的主要问题是,如果攻击者发现了散列密码,那么为什么他不能将散列发送到服务器以在客户端散列的情况下进行身份验证?
这是可能的,但它只能在代表开发人员发生大规模史诗故障的严重情况下发生。事实上,我以前见过这种情况,我将通过代码向您展示这是如何发生的:
public void Login(string username, string password)
{
if (Database.LoginMatches(username, password) ||
Database.LoginMatches(username, Hash.ValidateHash(password)))
{
logMeIn();
}
}
在上面的代码片段中,登录想要检查用户是否拥有哈希值,或者验证存储在数据库中的哈希值的密码。这不是正确的方法。事实上,这是我一生中见过的最糟糕的史诗级失败案例之一。
使用此代码,您可以简单地输入哈希或密码,然后登录。回忆上表。以下任一登录都将正常工作:
Login("markbuffalo", "$2a$08$BOHyNdXAVkWOUgPG/hvsjew7ZpngqJIgueY6m76xd4y3UllXUZLBy");
Login("markbuffalo", @"don't hack me please");
这完全是错误的做法。这不应该发生。在。全部。尽管如此,一些开发人员还是出于我不知道的原因采用了这种方法。
就像Rápli András所说,h(x)
不匹配h(h(x))
。正确的方法是根据哈希检查传入的密码以查看它们是否匹配。如果它们不匹配,请不要使用任何OR
魔法 ( ||
);根本不允许他们登录。
对于你的第一个问题:
这取决于应用程序。在大多数情况下,密码以明文形式发送到服务器(如果应用程序使用未加密的协议,例如 HTTP,这是不安全的)。但是,服务器可能会在数据库中存储散列值。如果是,应用程序计算从用户接收到的明文密码的哈希值,并将其与存储在 db 中的值进行匹配。
对于你的第二个问题:
服务器需要密码输入:X
。攻击者不知何故发现了h(X)
。然后,如果他输入这个散列值进行身份验证,散列函数将再次针对 运行h(x)
,因此他将无法进入,因为h(x)
不匹配h(h(x))
。
正如其他人指出的那样,通常的工作方式是客户端发送明文密码,但通过安全通道(使用可逆加密,而不是散列),然后使用相同的盐在服务器端对密码进行散列作为存储的哈希。
Client <----- Secure link -----> Server
Cleartext password Encrypted password Hashed password
保护:
现在的问题是当您必须通过不提供加密的通道发送密码时,低级协议(例如 PPP 或 802.1X)可能就是这种情况。传统的选择(对于 PPP)是:
所以前者可以防止密码数据库被盗,但不能防止窃听,而后者的情况正好相反。
撇开实际上设置 TLS 隧道(PEAP、EAP-TTLS、EAP-TLS ...)以保护密码交换的 EAP 协议不谈,还有一种替代协议可以将密码的不可逆“散列”发送到一个服务器,它还存储密码的“散列”:SRP,以及更普遍的“增强PAKE ”协议。