修改用户名哈希

信息安全 哈希
2021-08-17 22:36:35

为了验证最终用户对某些信息资源的访问权限,有几个问题要求输入关于存储(可能加盐)用户名哈希的智慧。我看到的大多数答案都非常反对这个想法,但没有一个解决了这种方法可能会缓解的漏洞。

我指的漏洞是用户名/密码重用。许多最近的数据库泄露导致发布明文用户名和相关密码(如果开发人员没有先散列)或密码散列(可能已经或可能没有加盐)。

所以我的问题是,存储散列用户名是否会使用特定于站点的秘密值加盐(除了正确存储加盐和散列密码),是否会减轻用户名/密码重用的漏洞?当然,这不能防止从以前被入侵的站点重复使用的用户名/密码对。这个问题主要是指通过不成为这种妥协的来源来保护用户信息的想法。

更新:似乎有很多关于如何实施的问题。这是我为构建的 MySQL 数据库所采用的方法的简要概述。它缺少所有细节,并且可能有一些我没有准确重建的细节(为了简单或无意的疏忽):

具有以下字段的用户表:

表名:USERS

USER_IDENTIFIER:GUID(在本例中兼作密码哈希的盐值)
  USERNAME_HASH: HASH(用户名 + site_salt)
  PASSWORD_HASH:哈希(密码 + USER_IDENTIFIER)
       USER_KEY: ENCRYPT(value: <系统生成的随机数>,
                            键:HASH(USER_IDENTIFIER+用户名+密码)) 

也可以有一个未加密的公共名称(只要它与用户名不同就不是问题)。

一个组表,由于缺少更好的名称,具有以下字段:

表名:组

 GROUP_ID:GUID
 OWNER_ID:拥有该组的用户的 USER_IDENTIFIER
GROUP_KEY: ENCRYPT(value: <系统生成的随机数>,
                      密钥:<拥有用户的未加密 USER_KEY>)

允许组所有者与其他用户共享组密钥的查找表,如下所示:

表名:SHARED_GROUP_KEYS

  GROUP_ID:拥有该组的用户正在共享的组的 GUID
   USER_ID:使用上述 GROUP_ID 访问组的用户的 GUID
SHARED_KEY:ENCRYPT(值:组的未加密 GROUP_KEY,
                       密钥:获取用户的未加密 USER_KEY)

然后,数据库中的信息将在组内共享。如果您已被授予访问某个组的权限(通过在该组的 SHARED_GROUP_KEYS 表中拥有一个有效条目),您就拥有查看与该组关联的内容的密钥。否则你不会。

为了进行身份验证,用户只提供用户名和密码。

所以回到最初的问题,它只关注 USER 表的 USERNAME_HASH 字段,为了防止用户名在发生妥协时溢出,对用户名进行散列而不是将其存储为明文是否有意义?换句话说,正如我在下面的评论中提到的,改变将用户名视为公共信息的范式是否有任何优点,而是将其视为另一个秘密(仅用于身份验证)?它是否有助于防止我的站点成为用户凭据的来源,从而允许未经授权访问该用户在另一个站点上的信息?

2个回答

在一个典型的系统中,有几个“用户名”。有用户输入的名称以开始登录操作。然后可以有一个“显示名称”(添加到论坛帖子、自动电子邮件...)、联系人电子邮件地址、帐单名称、持卡人姓名...保护这些名称中的一个相对没有什么意义无需与其他人打交道,因为它们都包含平均相似的信息(这听起来可能令人惊讶,但如果可以选择,许多人会更愿意使用自己的真实姓名来实现所有这些目的)。

“登录名”,即用户输入开始登录操作的用户名,服务于以下角色:它允许服务器有效地定位用户特定的数据。服务器可能有成千上万的用户,在一个大型数据库中,每次登录尝试都扫描所有用户很不方便;您想快速找到“用户标识符”,您可以从中获取该用户的哈希密码

登录名通常不被认为是机密数据(如果它们是机密数据,我们将称它们为“密码”),这只是因为登录名与用户的管理身份相似并且是正常的。然而,如果你想以某种方式隐藏登录名列表,你必须使用确定性注入:这是一种转换,这样给定两次相同的登录名,你会得到相同的输出(这是确定性,它是“查找特定于用户的数据”部分才能工作,并且必须在服务器范围内理解),并且两个不同的登录名将产生不同的输出(转换必须是单射的,或者至少不应允许以不可忽略的方式出现冲突可能性)。有几种解决方案:

  • 您可以使用SHA-256 等哈希函数您将获得全球确定性(每个人都使用相同的 SHA-256)和高概率几乎注入。
  • 如果您想消除攻击者可能从预计算中获得的任何优势,您可以添加服务器范围的盐。由于确定性,盐对于您服务器上的所有登录名仍然必须相同。salt 相当于选择您自己的、特定于服务器的哈希函数。请注意,获得散列登录名列表的攻击者仍然能够对所有名称进行并行攻击(对于每个潜在的用户名,攻击者对其进行散列 - 使用服务器范围的盐 - 并比较结果到所有散列的登录名);因此,这种特定的盐只完成通常由盐执行的部分工作(但您无法避免它,因为您需要确定性)。
  • 你可以把盐保密;那么,它就不再是一个盐,而是一个密钥,哈希函数也不再是一个哈希函数,而是一个消息认证码(例如HMAC)。仅当攻击者可以访问数据库但不能访问密钥时,这才会对您有任何好处(因此这是一种限制性攻击模型)。密钥需要密钥管理,这绝不是简单的事情,尤其是在存在多个前端的情况下(它们共享数据库,因此它们必须共享密钥——保密与共享并不完全兼容)。

请记住,所有这些都是关于二级防御的:该模型假设攻击者实现了非法读取访问(这已经是一个大问题了!)。虽然在实践中确实会发生这种读取访问(这就是我们散列密码的原因),但我们不能忘记这不应该是主要的防线;另一方面,每个散列或加密都会增加复杂性,这是众所周知的安全克星。所以有一个权衡,通常的智慧是避免复杂性,如果它只用于对已经半公开的数据(即登录名)的二级保护。

至于细节,您建议的方案的粗略轮廓将起作用(如果您愿意,您可以自由地将登录名和用户输入的密码的串联视为“密码” - 它不会使方案较弱)。但是,请帮自己一个忙:不要发明自己的散列。对于登录名和密码的哈希,依赖bcryptPBKDF2使用用户密码加密的用户特定密钥实际上是密钥派生函数的结果;再说一次,你最好使用像 PBKDF2 这样的合适的 KDF;您只需要存储用户特定的盐。“散列密码”和“加密的用户特定密钥”都可以用作字典攻击的基础 (又名“尝试潜在密码”),因此它们必须包含相同级别的保护(许多迭代以使处理变慢,一种阻止并行性的盐)。

我仍然认为这是一个糟糕的权衡:增加了很多复杂性,但几乎没有额外的安全性。

(重写的答案,请参阅历史原文。)

如果您只是使用标准哈希函数而不加盐对用户名进行哈希处理,那么相同的用户名显然会产生相同的哈希值,因此对于访问两个站点的用户数据库的攻击者来说比较哈希值是微不足道的。但是,使用特定于站点的盐确实可以避免这个问题。

但是,对于很多类型的网站来说,收集一个比较完整的用户列表并不难。(例如,在论坛网站上,用户名可能会显示在每个帖子旁边。)一旦攻击者拥有这样的列表(并访问您的用户数据库),他们就可以轻松地对列表中的每个用户名进行散列,然后匹配散列到您的数据库。因此,要使这种技术完全有用,您必须设计您的网站,以使实际的用户名永远不会在任何地方显示。即使这样,攻击者也可以从其他站点收集用户名并对其进行哈希处理,希望他们能在您的站点上找到一些匹配的用户。

(如果你能设法对攻击者保密你的站点特定的盐,情况会好一些,但假设你能做到这一点通常是不明智的。大多数情况下,如果攻击者可以获得你的数据库的副本, 他们还可以获取他们可能需要的 salt 和有关您的实现的任何其他信息。也就是说,在某些情况下可能并非如此,例如通过简单的 SQL 注入攻击导致的数据库泄漏。)