如何使用全球盐确保安全?

信息安全 密码 哈希 php
2021-09-04 16:37:10

如果我了解密码哈希和存储的基础知识,我们需要的是:

  1. “强”盐
  2. “真正的”随机盐
  3. 每个密码唯一的盐
  4. 具有高 CPU 成本的密码散列函数

我们使用 PHP以“中等”成本使用 1、2、3 和 4 对密码进行哈希处理crypt()CRYPT_BLOWFISH

我们有一个新的需求:我们需要能够比较哈希值,因为我们注意到低级别的欺诈/垃圾邮件/诈骗者使用相同的密码。我们不需要知道滥用者的密码,而只需要知道个人资料使用与先前识别的滥用者相同的密码。

所以我们正在考虑在我们的数据库中有 2 个字段:

  • 密码A:用于登录程序(此字段将使用 1、2、3 和 4)

crypt($password, '$2y$06$' . $uniqueSalt);

  • 密码B:用于“比较”(此字段将我们使用 1、2 和 4)

crypt(md5(md5($password) . $globalSaltA) . $globalSaltB, '$2y$10$' . $globalSaltC);

全局盐 A 和 B 将仅存储在应用程序代码中。

为了弥补密码 B 的缺失点 3,我们增加了高得多的“成本”。

在评论之后,我明白如果攻击者在服务器上获得完全访问权限(数据库 + 代码),这种策略是多么不安全。但是,如果攻击者只获取数据库,我们就可以了。

检测信用卡欺诈的公司对信用卡号码也有同样的问题。他们无法存储它们,但需要比较它们。

那么你会建议什么策略/解决方案?

4个回答

核心问题:如果您的服务器可以有效地将散列密码与(可能)所有用户的所有散列密码进行比较,那么攻击者也可以。获取您的服务器文件/数据库副本的攻击者将处于运行离线字典攻击的位置,即散列潜在密码并寻找匹配项。

普通密码散列使用每个密码的盐来防止并行攻击:我们希望攻击者为每个密码和每个用户帐户支付散列函数的全部计算代价(通过多次迭代变得昂贵) 。但是,如果多个密码散列使用相同的盐,则攻击者可以尝试针对所有这些散列的潜在密码,而代价是调用一次散列函数。因此,您的“全局盐”大大削弱了该方案:它允许攻击者攻击 1000 个帐户,而攻击一个帐户的成本。

重要的一点:你的问题是暂时性的。实际上,您实际上并不想将新用户密码与所有其他用户的所有密码进行比较;您想要的是将每个用户在注册时选择的密码与有限的“已知罪犯密码”列表进行比较。不幸的是,当一个注册用户陷入“罪犯”状态时,他已经注册了,他的密码并没有被保留,只有哈希值。因此,实际上,您希望能够在注册完成后访问用于注册的密码。

一个可能的解决方案:使用托管,也就是非对称加密创建 RSA 密钥对。将公钥存储在服务器上。不要在服务器上存储对应的私钥;相反,将其存储在其他地方,例如在离线的笔记本电脑上(或者可能只是在几个 USB 闪存驱动器上)。

当用户注册时,像往常一样散列他的密码(使用 PBKDF2、多次迭代、新的随机盐等)。但是,还要使用 RSA 密钥加密密码(而不是哈希),并将加密结果与哈希一起存储在您的数据库中。加密只需要公钥,并且是随机的,所以这个加密版本不会给获得数据库内容副本的攻击者额外的利用。当用户登录时,像往常一样使用密码哈希。

当用户被证明是垃圾邮件发送者时,获取私钥并解密托管密码。这样,您将获得“错误密码”并将其添加到密码列表中以在注册时拒绝。该列表可以保留为明文:由于相应的帐户已关闭,因此没有问题。注意在“安全”的机器上进行解密,最好是离线:你真的不想看到私钥被盗。


提醒一句:垃圾邮件发送者就像细菌一样,它们往往会随着外部约束而进化。如果您根据垃圾邮件发送者重复使用密码进行注册的习惯过滤掉他们,那么您很快就会训练他们生成随机密码。因此,我预测,如果您安装了这样的系统,那么在相对较短的时间后,它将不再有效地驱逐垃圾邮件发送者;在那之后,它只是你数据库中的重量(不是很多,因为带有 2048 位 RSA 密钥的 RSA 加密的短消息只有 256 个字节,但仍然是重量)。

你不能。它完全取消了拥有唯一盐(或任何盐)的安全性。拥有唯一盐的意义在于它可以防止使用彩虹表来识别密码。使用全局盐存储允许该表受到彩虹表的攻击,并且在许多情况下最有可能识别原始密码。

对此的替代方法是生成盐列表以尝试新密码(与以前的垃圾邮件发送者相关的密码)。由于您必须多次运行密码哈希,因此它使创建新用户的操作稍微困难一些,但这是完成您所说的事情的唯一安全方法。新用户创建也应该是您唯一需要执行此步骤的时间,所以它不应该太、太大。如果您想进一步减少影响,我还建议仅在其他指标似乎支持它是垃圾邮件用户时才这样做。

您还可以在识别出错误密码后存储它们以保持列表简短。例如,以前使用 salt 1234 的用户有一个哈希 abcd。当一个新用户提交密码“ImSpam”并且您尝试使用 salt 1234 时,您会看到它是 abcd 并且现在知道该 salt 的实际密码,因此您可以停止检查它并且只完全禁止密码。这应该使列表相对较短。您也可能让旧的垃圾邮件用户老化,因为如果您有一段时间没有受到该垃圾邮件发送者的攻击,他们放弃的机会是不错的。

让我们称唯一加盐的散列“更好”,而全局/单加盐的散列“更糟”。出于性能原因,您可能希望在用户数据库上保留更好的哈希值,以便快速验证用户身份。我建议将“更糟糕”的哈希移动到另一个更安全的服务器,该服务器具有一个精简、简单、经过身份验证的接口,由两种方法组成,AddPassword(userId, password) 和 Blacklist(spammerUserId)。

AddPassword 只会报告成功或失败。黑名单会将垃圾邮件发送者的 ID 添加到黑名单中,并返回与垃圾邮件发送者密码匹配的所有用户 ID 的列表。但请注意 - 任何有权访问的人都可以针对您的黑名单方法运行字典,从而毒害合法用户并获得匹配密码的列表。

在内部,您可以随心所欲地存储它们:散列、加盐、加密或明文。你只需要把那台机器安全地锁起来。

正如@AJHenderson 已经说过的那样,简单的答案是您不能。您了解为什么需要独特的盐,并且您不会错过任何数学技巧,与独特的盐结合起来确实是不可能的。也就是说,这就是我解决问题的方法:

1. 我将添加一个名为 UnsaltedPassword 之类的新数据库字段,并用所有 NULL 值填充它。每次有人注册或更改他/她的密码时,用密码的散列填充它,只有不加盐并且比其他散列方法强十倍。

2. 修改登录过程,使其检查用户在该字段中是否有 NULL 值,如果用户有,则填写该字段。如果这会导致过多的负载,请仅对某些帐户执行散列功能,例如帖子计数低的人或只是随机机会(例如,以十分之一的机会执行散列)。

3. 最后,我会在填写该字段时添加检查重复密码,并检查黑名单。重复的人可能会被标记(可能还会被告知他们使用了弱密码),并且黑名单会立即被禁止(或者如果可能的话,被禁止)。

与登录相比,密码更改不会经常发生(始终需要运行其他盐的散列函数),所以我认为在数据库中有两个不同的散列字段用于不同的目的是有意义的。十倍的强度应该是可行的,或者你可以做更多(1到2秒的散列时间是我的目标,因为它几乎是一次性交易)。攻击者破解这些可能比仅仅使用独特的盐更昂贵。