我见过的密码散列示例是:H(用户名 + 盐 + 密码)。添加用户名的目的是什么?有什么目的吗?
为什么在散列密码之前将用户名添加到盐中?
不,没有目的。这是安全剧院*。盐的目的是使对所有散列密码的并行攻击不可行,并破坏彩虹表。在其中添加用户名不会改善该行为或增加安全性的任何其他方面。它实际上有一点缺点,因为如果您更改用户名,您现在会遇到一些麻烦,并且您需要维护一个更复杂和非标准的系统。在纯粹的密码学意义上,没有缺点。但实际上,更多的复杂性意味着更多的错误。
盐和用户名的属性
您可能认为用户名本身可能不公开,因此将其用作额外的秘密并没有什么坏处,但事实是数据库可能已经包含明文形式的用户名,从而使这个已经值得怀疑的好处无效。人们应该坚持使用预先存在的身份验证技术。但是让我们看一下每个对象的属性:
盐是:
不是秘密 - 盐以明文形式存储。
安全 - 它们是随机生成的并且很长。
独特 - 每个用户的盐都是故意不同的。
密码是:
现在将其与用户名进行比较。用户名是:
不是秘密 - 它们是公开的或至少以明文形式存储。
不安全 - 没有人会想到选择一个长而复杂的用户名。
不唯一 - 用户名可以安全地在站点之间共享。
好盐的特点
现在,盐到底有什么作用?一般来说,一种好的盐有三个好处:
它可以防止攻击者一次攻击每个用户的哈希。攻击者不再能够对候选密码进行哈希处理并同时针对每个条目进行测试。他们被迫重新计算任何给定的密码,以测试每个用户的哈希值。盐提供的这种好处随着不同目标哈希条目的数量的增长而线性增长。当然,即使只有一个哈希需要保护,盐仍然很重要,正如我在下面解释的那样。
它使彩虹表不可行。彩虹表是高度优化的预计算表,可将密码与哈希匹配。它们比巨大的查找表占用更少的空间,但需要大量时间来生成(时空权衡)。为了使彩虹表工作,给定的密码必须始终解析为相同的哈希值。盐打破了这一假设,使彩虹表变得不切实际,因为它们必须为每种可能的盐都有一个新条目。
它可以防止通过彩虹表进行有针对性的预计算,至少在散列是真正随机的时候是这样。攻击者只有在获得哈希(以及盐)后才能开始攻击。如果盐已经公开但散列不是,那么可以通过为该特定盐生成彩虹表来优化攻击。这就是 WPA2 协议如此丑陋的原因之一。salt 是 ESSID(网络名称),因此有人甚至可以在进行 4 次握手之前就开始攻击目标路由器。
那么当这个值是公开的、不安全的和重用时,在散列之前连接一个值有什么可能的好处呢?它最终不需要攻击者挖掘更多信息。它不会增加盐的安全性。它不会增加密码的复杂性。没有任何好处。
正确的密码散列
那么他们应该怎么做才能增加安全性呢?他们可以使用KDF,例如 PBKDF2、bcrypt、scrypt 或 argon2,而不是单个散列。他们可以添加一个辣椒,这是一个存储在数据库外部的随机全局值,并添加到密码和盐中,因此有必要窃取辣椒来尝试攻击哈希,而不是简单地使用 SQLi 转储数据库。
编辑:正如一些评论指出的那样,有一种人为的场景,用户名将有利于添加到组合中。这种情况将是实现被严重破坏以至于盐实际上不是盐的情况,并且用户名是数据库中唯一或半唯一的每个用户值,在这种情况下混合用户名会更好比没有。但实际上,如果你没有盐,你应该开始使用盐而不是尝试使用用户名。使用真正的安全性,当用户的安全受到威胁时不要半信半疑。
* 在这种情况下,我将安全剧院定义为实施安全措施的实践,这些措施实际上并没有以任何有意义的方式提高安全性,只是为了提供更好的安全性的假象。
如果盐是足够随机的,那么添加用户名不会增加额外的保护。但它也不会造成任何伤害,除了使代码更复杂,这增加了错误的可能性。在进行代码审查时,它可能被视为开发人员不完全理解他在做什么的迹象。
相反,如果盐只是大部分是静态的或从密码派生的,那么用户名可能会有所帮助,因为在这种情况下,用户名本质上是用于盐无法服务的目的。但是,这不是推荐的方式,因为盐应该是随机的,而用户名不是。
请参阅在散列哈希(username_str + password_str)之类的密码时使用用户的用户名作为盐是个好主意吗?什么应该用作盐?更深入地讨论什么是好盐以及为什么用户名不是好盐。另请参阅为什么加盐哈希对密码存储更安全?首先要了解盐的用途。
正如@Damien_The_Unbeliever 在评论中指出的那样,它可以防止在系统被部分入侵的情况下冒充用户。
想象以下场景。不知何故,攻击者获得了对用于登录的 db 表的读/写访问权限,其中包含用户名、密码哈希和盐——可能是通过 SQL 注入攻击。该攻击者对系统没有其他提升的访问权限,但希望以无法追踪(或难以追踪)的方式冒充系统中的用户。
- 首先,攻击者记录受害者账户的原始密码哈希和盐。
- 接下来,攻击者注册自己的帐户,并将密码哈希和盐从他们的帐户复制到受害者的帐户中。
- 现在,攻击者可以使用攻击者自己的密码登录受害者的帐户。
- 当攻击者完成后,他们恢复受害者的密码哈希和盐,使受害者很难意识到发生了什么。
虽然在某些系统中,具有这种访问级别的攻击者可以简单地使用受害者的用户名、salt 和任意密码(而不是复制他们自己的密码)生成密码哈希,但如果系统除了使用胡椒之外,这是不可能的salt(一个全局定义的常量,添加到所有密码散列的输入中,它存储在与密码散列和盐不同的地方)。在不损害辣椒的情况下,在这种情况下,攻击者可以为受害者设置具有已知密码的密码哈希的唯一方法是复制系统生成的密码,如上所述。
请注意,两因素身份验证可能甚至无法阻止这种情况。如果攻击者还可以访问用户的 2FA 初始化代码(可能存储在同一个 db 表中),则攻击者的代码也可以临时写入受害者的帐户。
相反,如果用户名用于计算密码哈希,则这种特殊的模拟攻击将不起作用。即使攻击者将他们的密码哈希、盐和 2FA 代码复制到受害者的帐户中,在受害者帐户上使用攻击者的密码也不会导致与攻击者帐户相同的密码哈希,因此登录将失败。
当然,这些好处是否值得额外的复杂性和错误的可能性值得商榷,因为这种情况要求攻击者已经严重破坏了系统,并且在这种情况下,攻击者不会恢复用户的实际密码。但是,可以说这是一种额外的保护。
对于初学者,密码散列应该使用专用的密码散列函数来完成,例如 bcrypt、Argon2、scrypt 或 PBKDF2。在这种情况下,您不必像原语一样处理连接盐和密码;该函数将它们作为单独的参数。请参阅“如何安全地散列密码?” ,本网站上有关该主题的热门问答之一。
密码散列中盐的目的是随机化应用于每个密码条目的散列函数。因此,通常适用于现代专用密码哈希的三个规则是:
- 每次您注册一个新密码时,您都应该生成一个新的盐。(请注意,这意味着当用户更改密码时,您应该为该密码生成一个新的盐,而不是为旧的盐重用盐。盐绑定到密码数据库条目的状态,而不是用户。)
- salt的值应该独立于密码本身;知道密码应该对猜测盐没有帮助,否则攻击者可以使用这些知识来预先计算攻击表。一个明显的例子是你不应该使用密码本身作为盐。(这并不像听起来那么愚蠢——在这种情况下,你不会将盐存储在密码中——但它很愚蠢,因为使用相同密码的两个用户将具有相同的哈希值。)
- 两种盐彼此相等的机会应该非常低。最好不仅在您的应用程序中,而且在全球范围内。
现在我们可以通过以下观察来回答您的问题:
- 由足够数量的随机字节组成的盐已经满足这些标准。16 个随机字节(来自加密的强随机数生成器)听起来很合理。
- 将用户名连接到这样一个随机的盐并没有帮助。
- 如果盐是以某种可预测但有些不重复的方式生成的——例如,作为每次注册新密码时递增的计数器,或作为时间戳——连接用户名或站点域名等附加值有助于它更独特。但是使用随机盐会更简单(不需要跟踪持久的计数器状态)。