你把盐字符串存储在哪里?

301

在将密码散列存储到数据库中时,我一直使用适当的每个条目(entry)的盐字符串。对于我的需求,将盐存储在数据库中与散列密码一起工作得很好。

然而,有些人建议将盐从数据库中分开存储。他们的论点是,如果数据库被攻击者攻破,攻击者仍然可以建立彩虹表,考虑特定的盐字符串,以便逐个破解一个帐户。如果此帐户具有管理权限,则他甚至可能不需要破解其他任何帐户。

从安全的角度来看,将盐存储在不同位置是否值得呢?考虑一下服务器代码和数据库位于同一台机器上的Web应用程序。如果盐存储在该机器上的一个平面文件中,那么如果数据库被攻破,盐文件也很可能会被攻破。

有没有推荐的解决方案?


12
如果有一个攻击者无法获取盐的存储位置,那么你应该将密码也存储在那里。但是为什么不为每个密码使用不同的盐呢? - jrockway
14
他为每个密码使用不同的盐,jrockway。 - Amber
9
你的盐有多长?你的盐应该足够长(32位?),以至于事实上几乎没有彩虹表会预先计算出它。 - sourcenouveau
我还想把这个[dba.se]的问题扔进来 http://dba.stackexchange.com/questions/7492/password-hashes-fixed-length-binary-fields-or-single-string-field - jcolebrand
13
我是PWDTK的作者,http://sourceforge.net/projects/pwdtknet/。老实说,我不会太担心,我会将盐与密码存储在同一个数据库中。你应该始终假定盐已经为攻击者所知,因此你的重点应该是使用大的加密随机盐并进行足够的密钥拉伸(在PBKDF2中进行迭代),以使得对于一个已知盐来制作出一张彩虹表变得不可行。老实说,你试图通过将盐存储在其他地方来实现的目标是“安全性取决于混淆”,通常情况下不会产生任何好处,特别是当你考虑到可能会出现另一个服务器故障的情况。 - thashiznets
显示剩余4条评论
7个回答

296
彩虹表的作用在于它们是事先创建并大规模分发的,以节省他人的计算时间——生成彩虹表时所需的时间和直接破解密码+盐值组合的时间相同(因为实际上在生成彩虹表时所做的就是预运行暴力破解散列的计算),因此认为知道盐值后就可以“生成彩虹表”的说法是站不住脚的。
只要盐值是基于每个用户独立的,将其存储在单独的文件中就没有真正意义——盐值的作用只是使得一张彩虹表无法破解数据库中的所有密码。

20
同意。通过将盐分单独存储,您要保护的威胁模型是用户可以以某种恶意手段访问DB中的盐分,但无法访问哈希值(在DB中)。而且这个人将提前计算彩虹表,假设他以后能够找到哈希值。这并非不可能,但对于防御这种单一攻击途径,投入工程力量也不值得。 - Tom Ritter
13
如果你没有为每个用户使用独立的盐值,那么加盐就毫无意义。盐值的作用是让破解哈希密码变成每个用户密码的单独任务;如果所有用户都使用相同的盐值,那么这一点也无法实现。 - Amber
3
@TomRitter 这并不是唯一的情况。你假设所有密码都很复杂,但是有些攻击者可能会使用盐和哈希值只检查最常见的10000个密码。这样他们就可以获得相当数量的用户密码。然而,如果他们无法访问盐,则类似于用户使用更长、更安全的密码。现在,盐数据库能否在密码数据库被窃取时保持安全的可能性还有待商榷,但这是一个分开讨论的问题。 - chacham15
@Amber,我相信TomRitter是正确的。将盐分开存储意味着强制攻击者使用暴力攻击而不是更容易的字典攻击。如果你知道盐的值,你可以在一般的字典攻击中直接附加它。如果你能100%保护你的盐,你可以简单地使用相同的盐并强制攻击者对所有内容进行暴力破解(即使用户使用“password”作为密码)。但你能保护你的盐吗?可能不能。因此最好将其与哈希值存储在一起,并强制执行更强的密码规则,以减少故障点。 - Ultratrunks
@doliver 既然这两种方法在任何合理的时间内都同样不可能成功,我认为这个争论纯粹是学术性的,在实践中隐藏盐并没有真正的现实好处。 - Nick Coad
显示剩余6条评论

44

我这里提供一个略有不同的看法。

我总是将盐和经过盐加密的密码哈希值混合存储在一起。

例如,我会将盐的前半部分放在密码盐哈希之前,将盐的后半部分放在密码盐哈希之后。应用程序知道这个设计,因此可以获取这些数据,并获取盐和经过盐加密的密码哈希值。

我的做法理由:

如果密码/哈希数据被攻击者窃取,攻击者无法仅从数据中查看盐。这样,攻击者不能实际执行暴力破解攻击以获取与哈希匹配的密码,因为他不知道哈希值,也无法知道哪些部分是盐的组成部分或经过盐加密的密码哈希值的组成部分(除非他知道您的应用程序身份验证逻辑)。

如果将经过盐加密的密码哈希值存储为原样,那么可以执行暴力破解攻击以获取一个密码,该密码通过盐加密和哈希后产生与经过盐加密的密码哈希值相同的数据。

但是,例如,即使将经过盐加密的密码哈希值存储为原样,但是在前面添加了一个随机字节,只要攻击者不知道这个字节应该被丢弃,这也会增加攻击的难度。您的应用程序会知道在用于身份验证用户时丢弃数据的第一个字节。

得出结论..

1)永远不要以精确形式存储身份验证应用程序使用的数据。

2)如果可能,请保持您的身份验证逻辑保密以提高安全性。

再深入一步..

如果无法保密您的应用程序身份验证逻辑 - 许多人知道数据库中存储数据的方式。并且假设您已经决定将经过盐加密的密码哈希值与盐混合存储在一起,并且部分盐将前缀添加到经过盐加密的密码哈希之前,其余部分将后缀添加到其中。

生成随机盐时,您还可以随机决定存储盐的哈希值之前/之后的比例。
例如,您生成了一个512字节的随机盐。将盐附加到密码上,并获取盐值密码的SHA-512哈希。您还生成了一个200的随机整数。然后,您存储前200个字节的盐,紧接着是盐值密码哈希和剩余的盐。
在验证用户的密码输入时,您的应用程序将遍历字符串,并假定数据的第1个字节是盐的第1个字节,后跟盐的哈希值。此次遍历将失败。应用程序将继续使用数据的前2个字节作为盐的前2个字节,并重复直到在使用前200个字节作为盐的前200个字节后找到正面结果。如果密码错误,则应用程序将继续尝试所有排列,直到没有找到为止。
此方法的优点:
增加安全性-即使您的身份验证逻辑已知,编译时也不知道确切的逻辑。即使具有确切逻辑的知识,进行暴力攻击也几乎是不可能的。更长的盐长度将进一步增加安全性。
此方法的缺点:
由于在运行时推断出确切的逻辑,因此这种方法非常消耗CPU。盐的长度越长,这种方法就变得越耗费CPU。
验证不正确的密码将涉及最高的CPU成本。这可能会对合法请求产生反作用,但可以增加对攻击者的安全性。
此方法可以以各种方式实现,并且可以通过使用可变宽度的盐和/或盐值密码哈希来使其更加安全。

14
你的方法只是在哈希过程中添加了一个密钥(应用盐的算法)。如果你另外加入一个“胡椒粉”来增加这个密钥,那么这将更容易实现。我在我的教程中也试图指出这一点。像 BCrypt 这样的现代哈希函数会自行应用盐,并在每次迭代中使用原始盐,所以你不会对此有任何控制权。 - martinstoeckli
@martinstoeckli 您说得没错,BCrypt会自行应用盐,但是盐+哈希的存储取决于您作为开发人员的选择。因此,您可以轻松地将一个pepper添加到盐+哈希中,并将其持久化到数据库中。然后,在后续检索时,您从数据库中读取该值,去除pepper值,并将剩余值传递给BCrypt。 - PeterToTheThird
2
@PeterToTheThird - 这将抵消辣椒的优势。辣椒添加了一个服务器端的秘密,只要它保持秘密(与盐相反),它才能起作用。典型的攻击是SQL注入,当有人获得对数据库但不是代码的访问权限时,存储在数据库中的辣椒就会变得无用。大多数BCrypt实现将自动将盐添加到生成的哈希值中,因此该值已经包含了盐、成本因素、算法和哈希。这个字符串可以存储在一个长度为60个字符的单个字段中。 - martinstoeckli
2
@martinstoeckli - 是的,实际的实现取决于您使用的哈希函数。显然,在实现身份验证逻辑时,您需要考虑哈希函数的参数和输出。最终,pepper只是引入到哈希函数中的另一个变量,而它与盐和哈希值不存储在同一位置。 - Ibraheem
这并不增强安全性。如果我可以下载您的数据库,我也能够下载/反向工程您的应用程序代码。靠模糊不清来保障安全性?并不是真正的安全。 - Cypher
显示剩余4条评论

29
通常情况下,它们会被预先添加到哈希中并存储在同一字段中。没有必要单独存储它们-关键是针对每个密码使用随机盐,以便不能使用单个彩虹表攻击整个密码哈希集合。使用随机盐,攻击者必须逐个暴力破解每个哈希值(或计算所有可能盐的彩虹表-这需要更多的工作)。如果您有更安全的存储位置,那么仅在那里存储哈希值就是有意义的。

5
如果所有哈希密码及其匹配的盐值都被泄露,会发生什么?这不也是不安全的吗? - mghaoui
12
如果您想知道“密码”,那么除非一些盐值相同,否则您仍需要为每个盐构建一个彩虹表。 - Franklin

5
根据William Penberthy的《Developing ASP.NET MVC 4 Web Applications》书籍:
1. 获取存储在单独数据库中的盐需要黑客入侵两个不同的数据库才能访问盐和被盐加密的密码。将它们存储在与密码相同的表中,甚至是同一数据库的另一个表中,这意味着当黑客获得对数据库的访问权时,他们将可以访问到盐和密码哈希值。因为安全性包括使黑客进入系统变得不值得付出太多代价或时间,所以增加黑客获取访问权限的次数应该可以使系统更加安全。
2. 简易使用是将盐保存在与散列密码相同的数据库中的主要原因。您不需要确保两个数据库始终可同时使用且始终同步。如果每个用户都有随机盐,则具有盐的优点很小,因为虽然它可能使发现单个用户的密码更容易,但破解整个系统的密码所需的工作量会非常大。在此级别的讨论中,这真的是期望的内容:保护密码。如果黑客已经获取了数据库的副本,则您的应用程序数据已经存在危险。此时,问题是减轻用户由于共享密码可能带来的风险。
3. 维护两个单独链接的数据库的要求非常广泛。诚然,它增加了安全感,但唯一的优点是保护密码,即单个数据元素。如果数据库中的每个字段都被分别加密,并且使用相同的盐进行加密,则将其与数据分开存储会更有意义,因为这可以提高系统的基本安全性。

如果应用程序可以同时验证两个数据库,那么如果攻击者已经入侵了应用程序代码,它不就本质上相当于一个数据库吗? - John Ernest

1
如果您使用一个库(或自己制作的库)来使用固定大小的字符串作为盐值,那么您可以将盐值和哈希密码存储在同一字段中。然后,您需要拆分存储的值以检索盐和哈希密码以验证输入。
对于一个10个字符长度的盐值和40个字符长度的固定哈希大小,应该是这样的:
salt = "california"
passwd = "wp8tJ4Pr"

stored_passwd = salt + hash(passwd + salt)

salt = substr(stored_passwd, 0, 10)
hashed_passwd = substr(stored_passwd, 10, 40)

if hash(user_input + salt) == hashed_passwd:
    print "password is verified"

盐的整个目的是防止使用预先计算的表(例如彩虹表)进行密码攻击,因此将盐与哈希密码一起存储实际上是无害的。


1
盐的作用是使所有彩虹表无效,并需要制作一个新的彩虹表。猜测字符串和制作彩虹表需要同样长的时间。例如,"password" 的 SHA-256 哈希值为 "5e88 4898 da28 0471 51d0 e56f 8dc6 2927 7360 3d0d 6aab bdd6 2a11 ef72 1d15 42d8"。添加盐后,如 "badpassword",要进行哈希的新字符串是 "passwordbadpassword",由于雪崩效应,它会显着改变输出,变为 "457b f8b5 37f1 802e f9c8 2e46 b8d3 f8b5 721b 7cbb d485 f0bb e523 bfbe 73e6 58d6"。通常,盐只存储在与密码相同的数据库中,也因为如果一个数据库被黑客攻击,另一个数据库也可能会被攻击。

0

为什么要使用盐来防止彩虹表攻击?恶意用户以某种方式访问数据库并查看散列密码,获取最常见密码的表格,找到其哈希值并在表格中查找密码。

因此,当用户发送密码时,我们会将随机生成的盐添加到密码中。

 userPassword + salt

然后我们将其传递给我们的哈希算法。

 hash(userPassword+salt)

由于盐值是随机生成的,userPassword+salt 成为一个随机值,绝对不是最常用的密码之一。因此,恶意用户无法通过检查彩虹表来找出使用的密码。

现在,盐值被添加到哈希值前面,因为当用户登录时,它会再次使用,以比较传递的凭据和保存的凭据。

 hash(userPassword+salt)=ashdjdaskhfjdkhfjdashadslkhfdsdh

这是密码在数据库中的存储方式:ashdjdaskhfjdkhfjdashadslkhfdsdh.salt

现在,如果恶意用户看到了这个,他可以破解密码,但需要花费巨大的时间。因为每个密码都会得到一个不同的盐值。假设恶意用户有一个包含5000个常见密码及其哈希值的表。

重要的一点是,恶意用户不只有一个表。因为有太多不同的算法,所以恶意用户将为每种算法拥有5000个密码的哈希值。

现在对于每个密码,假设他从第一个用户的密码开始,他将把那个盐值添加到5000个常见密码中,并为每个不同的算法创建一个新的彩虹表来查找仅1个密码。然后对于第二个用户的密码,他将看到一个不同的盐值,他将计算新的彩虹表。甚至不能保证用户的密码会在这些常见密码列表中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接