如何安全地散列密码?

信息安全 应用安全 密码学 密码 哈希
2021-08-30 21:15:36

如果我在将密码存储到数据库之前对其进行哈希处理,是否足以防止它们被任何人恢复?

我应该指出,这仅与直接从数据库中检索有关,与任何其他类型的攻击无关,例如暴力破解应用程序的登录页面、客户端上的键盘记录器,当然还有橡皮软管密码分析(或者现在我们应该称之为“巧克力密码分析”)。

当然,任何形式的哈希都不会阻止这些攻击。

4个回答

注意:这个答案是在 2013 年写的。在接下来的几年里,很多事情都发生了变化,这意味着这个答案应该主要被视为 2013 年的最佳实践。


理论

我们需要散列密码作为第二道防线。可以对用户进行身份验证的服务器必须在其内部的某处包含一些可用于验证密码的数据。一个非常简单的系统只存储密码本身,验证将是一个简单的比较。但是,如果一个敌对的局外人对包含密码的文件或数据库表的内容有一个简单的了解,那么攻击者就会学到很多东西。不幸的是,这种部分的、只读的破坏在实践中确实发生了(一个放错的备份磁带、一个退役但没有被清除的硬盘、SQL 注入攻击的后果——可能性很多)。有关详细讨论,请参阅此博客文章

由于可以验证密码的服务器的全部内容必须足以真正验证密码,因此获得服务器只读快照的攻击者可以进行离线字典攻击:他尝试潜在的密码直到匹配成立。这是不可避免的。所以我们想让这种攻击尽可能地困难。我们的工具如下:

  • 密码散列函数:这些是迷人的数学对象,每个人都可以有效地计算,但没有人知道如何反转它们。这看起来很适合我们的问题——服务器可以存储密码的哈希值当出现假定密码时,服务器只需对其进行哈希处理以查看它是否获得相同的值;然而,知道哈希并不会泄露密码本身。

  • Salts:攻击者相对于防御者的优势之一是并行性攻击者通常会获取完整的哈希密码列表,并有兴趣破解尽可能多的密码。他可能会尝试同时攻击几个。例如,攻击者可能会考虑一个潜在的密码,对其进行哈希处理,然后将该值与 100 个哈希密码进行比较;这意味着攻击者分担对多个受攻击密码进行散列的成本类似的优化是预计算表,包括彩虹表这仍然是平行性,坐标的时空变化。

    所有使用并行性的攻击的共同特征是它们对使用完全相同的散列函数处理的多个密码起作用加盐不是使用一种散列函数,而是使用许多不同的散列函数理想情况下,密码散列的每个实例都应该使用自己的散列函数。是一种在一大群散列函数中选择特定散列函数的方法。正确应用盐将完全阻止并行攻击(包括彩虹表)。

  • 缓慢:计算机随着时间的推移变得更快(英特尔的联合创始人戈登摩尔在他著名的定律中将其理论化)。人脑没有。这意味着随着时间的流逝,攻击者可以“尝试”越来越多的潜在密码,而用户却无法记住越来越复杂的密码(或断然拒绝)。为了应对这种趋势,我们可以通过定义散列函数来使用大量内部迭代(数千次,可能是数百万次) ,从而使散列本质上变慢。

我们有一些标准的加密哈希函数;最著名的是MD5SHA家族从基本操作中构建一个安全的散列函数远非易事。当密码学家想要这样做时,他们会努力思考,然后更加努力,并组织一场比赛,让功能之间激烈地相互竞争。当数以百计的密码学家对一个功能进行了数年的磨擦和打击,并没有发现任何不好的地方时,他们开始承认,也许这个特定的功能或多或少可以被认为是安全的。这正是SHA-3 比赛中发生的事情。我们使用这种设计哈希函数的方式,因为我们不知道更好的方式。在数学上,我们不知道安全散列函数是否真的存在;我们只有“候选人”(这就是“它不能被打破”和“世界上没有人知道如何打破它”之间的区别)。

基本散列函数,即使作为散列函数是安全的,也不适合密码散列,因为:

  • 它是无盐的,允许并行攻击(可以免费获得MD5 或 SHA-1 的彩虹表,您甚至不需要自己重新计算它们);
  • 它太快了,并且随着技术的进步而变得更快。使用最近的 GPU(即每个人都可以购买的现成消费产品),哈希率以每秒数十亿个密码计算

所以我们需要更好的东西。碰巧将散列函数和盐放在一起并对其进行迭代并不比设计散列函数更容易 - 至少,如果您希望结果是安全的。再一次,您必须依赖在报复性密码学家的持续攻击中幸存下来的标准结构。

良好的密码散列函数

PBKDF2

PBKDF2来自PKCS#5它使用迭代计数(整数,至少 1,无上限)、salt(任意字节序列,对长度没有限制)、所需的输出长度(PBKDF2 可以生成可配置长度的输出)进行参数化,和“基础PRF”。在实践中,PBKDF2 总是与HMAC一起使用,它本身就是建立在底层哈希函数之上的构造。所以当我们说“PBKDF2 with SHA-1”时,我们实际上是指“PBKDF2 with HMAC with SHA-1”。

PBKDF2 的优点:

  • 已经指定了很长时间,现在似乎毫发无损。
  • 已经在各种框架中实现(例如,它与.NET一起提供)。
  • 高度可配置(尽管某些实现不允许您选择散列函数,例如 .NET 中的一个仅用于 SHA-1)。
  • 收到NIST 的祝福(以散列和密钥推导之间的差异为模;见后文)。
  • 可配置的输出长度(再次,见后文)。

PBKDF2 的缺点:

  • 仅 CPU 密集型,因此可以使用 GPU 进行高度优化(防御者是一个基本的服务器,它做通用的事情,即 PC,但攻击者可以将他的预算花在更专业的硬件上,这会给他带来优势)。
  • 您仍然必须自己管理参数(盐生成和存储,迭代计数编码......)。PBKDF2 参数有一个标准编码,但它使用 ASN.1,因此大多数人会尽可能避免使用它(ASN.1 对于非专家来说可能很难处理)。

bcrypt

bcrypt是通过重用和扩展名为Blowfish的分组密码的元素而设计的。迭代次数是 2 的幂,与 PBKDF2 相比,可配置性稍差一些,但也足够了。这是OpenBSD操作系统中的核心密码散列机制。

bcrypt 的优点:

  • 各种语言的许多可用实现(请参阅维基百科页面末尾的链接)。
  • 对 GPU 更有弹性;这是由于其内部设计的细节。bcrypt 作者自愿这样做:他们重复使用 Blowfish,因为 Blowfish 基于内部 RAM 表,该表在整个处理过程中不断访问和修改。对于想要使用 GPU 加速 bcrypt 的人来说,这使得生活变得更加困难(GPU 不擅长并行进行大量内存访问)。请参阅此处进行一些讨论。
  • 标准输出编码,包括盐、迭代次数和作为一种简单存储可打印字符的字符串的输出。

bcrypt 的缺点:

  • 输出大小是固定的:192 位。
  • 虽然 bcrypt 擅长阻止 GPU,但它仍然可以使用FPGA进行彻底优化:现代 FPGA 芯片具有许多小的嵌入式 RAM 块,非常便于在一个芯片内并行运行许多 bcrypt 实现。它已经完成。
  • 输入密码大小限制为 51 个字符。为了处理更长的密码,必须将 bcrypt 与散列函数结合起来(你散列密码,然后使用散列值作为 bcrypt 的“密码”)。已知组合密码原语是危险的(见上文),因此不能普遍推荐此类游戏。

加密

scrypt是一种更新的结构(设计于 2009 年),它建立在 PBKDF2 和称为Salsa20/8的流密码之上,但这些只是围绕 scrypt 核心优势(即RAM )的工具。scrypt 被设计为固有地使用大量 RAM(它生成一些伪随机字节,然后以伪随机序列重复读取它们)。“大量 RAM”是难以并行的东西。基本的 PC 擅长 RAM 访问,不会尝试同时读取数十个不相关的 RAM 字节。拥有 GPU 或 FPGA 的攻击者会想要这样做,并且会发现这很困难。

scrypt 的优点:

  • PC,即防御者在散列密码时将使用的确切设备,是计算 scrypt 的最有效平台(或足够接近)。攻击者不再通过将钱花在 GPU 或 FPGA 上而获得提升。
  • 调整函数的另一种方法:内存大小。

scrypt 的缺点:

  • 仍然是新的(我自己的经验法则是至少等待 5 年的普遍曝光,所以直到 2014 年才生产 scrypt - 但是,当然,最好其他人在生产中尝试 scrypt,因为这会提供额外的曝光)。
  • 各种语言的可用、即用型实现并不多。
  • 不清楚 CPU / RAM 组合是否最佳。对于每个伪随机 RAM 访问,scrypt 仍然计算一个哈希函数。一次缓存未命中大约为 200 个时钟周期,一次 SHA-256 调用接近 1000。这里可能有改进的余地。
  • 还有一个要配置的参数:内存大小。

OpenPGP 迭代和加盐 S2K

我引用这个是因为如果您使用GnuPG进行基于密码的文件加密,您将使用它。该工具遵循OpenPGP 格式,该格式定义了自己的密码散列函数,称为“Simple S2K”、“Salted S2K”和“Iterated and Salted S2K ”。在此答案的上下文中,只有第三个可以被视为“好”。它被定义为一个非常长的字符串(可配置,最大约 65 兆字节)的哈希,由重复的 8 字节盐和密码组成。

就这些事情而言,OpenPGP 的 Iterated And Salted S2K 是不错的;它可以被认为与 PBKDF2 类似,但可配置性较低。你很少会在 OpenPGP 之外遇到它,作为一个独立的函数。

Unix“地穴”

最近的类 Unix 系统(例如 Linux),用于验证用户密码,使用基于良好散列函数的crypt()函数的迭代和加盐变体,具有数千次迭代。这是相当不错的。有些系统还可以使用bcrypt,这样更好。

基于DES 块密码的旧 crypt() 函数不够好:

  • 它在软件上很慢,但在硬件上很快,并且在软件上也可以很快,但只有在并行计算多个实例时(称为SWAR或“bitslicing”的技术)。因此,攻击者处于优势地位。
  • 它仍然相当快,只有 25 次迭代。
  • 它有一个 12 位的盐,这意味着盐重用将经常发生。
  • 它将密码截断为 8 个字符(超过 8 个字符将被忽略),并且它还会丢弃每个字符的高位(因此您或多或少会被 ASCII 卡住)。

但是默认情况下处于活动状态的最新变体会很好。

错误的密码散列函数

关于其他一切,尤其是人们不断发明的几乎所有自制方法。

出于某种原因,许多开发人员坚持自己设计功能,并且似乎认为“安全密码设计”意味着“将所有可以想到的密码或非密码操作放在一起”。有关示例,请参见此问题。基本原则似乎是,由此产生的完全错综复杂的指令的绝对复杂性会使攻击者感到困惑。然而,在实践中,开发者自己会比攻击者更容易被自己的创作弄糊涂。

复杂性不好。自制不好。新的不好。如果您记住这一点,您将避免 99% 的与密码散列、密码学甚至一般安全性相关的问题。

Windows 操作系统中的密码散列曾经非常糟糕,现在变得非常糟糕(无盐、非迭代 MD4)。

密钥派生

到目前为止,我们考虑了哈希密码的问题。一个密切相关的问题是将密码转换为可用于加密的对称密钥。这称为密钥派生,是“使用密码加密文件”时要做的第一件事。

可以制作人为的密码散列函数示例,这些函数对于存储密码验证令牌是安全的,但在生成对称密钥时却很糟糕;反之亦然。但这些例子非常“人为”。对于如上所述的实用功能:

  • 在可能截断到所需大小之后,密码散列函数的输出作为对称密钥是可接受的。
  • 只要“派生密钥”足够长以避免“通用原像”(攻击者很幸运并找到了产生相同输出的密码),密钥派生函数就可以用作密码散列函数。超过 100 位左右的输出就足够了。

事实上,PBKDF2 和 scrypt 是 KDF,而不是密码散列函数 - NIST “批准” PBKDF2 作为 KDF,而不是明确地作为密码散列器(但有可能,只需非常微小的虚伪,就可以阅读 NIST 的散文以这样的方式,似乎说 PBKDF2 适合散列密码)。

相反,bcrypt 实际上是一个分组密码(密码处理的大部分是“密钥计划”),然后在CTR 模式下使用它来产生三个伪随机输出块(即 192 位),使其成为一种哈希功能。通过在 CTR 模式下使用块密码获取更多块,bcrypt 可以通过一些手术变成 KDF。但是,像往常一样,我们不能推荐这种自制的变换。幸运的是,对于大多数用途来说,192 位已经绰绰有余(例如,使用GCMEAX进行对称加密只需要 128 位密钥)。

杂项主题

多少次迭代?

越多越好!这种加盐和缓慢的哈希是攻击者和防御者之间的军备竞赛。您使用许多迭代来使每个人都更难对密码进行散列处理为了提高安全性,鉴于服务器必须完成的任务,您应该在服务器上设置尽可能高的数字。越高越好。

碰撞和 MD5

MD5坏了:在计算上很容易找到许多不同的输入对,它们散列到相同的值。这些被称为碰撞

但是,冲突不是密码散列的问题密码散列要求散列函数能够抵抗原,而不是冲突。冲突是关于寻找能够无限制地给出相同输出的消息对,而在密码散列中,攻击者必须找到产生攻击者无法选择的给定输出的消息。这是完全不同的。据我们所知,MD5 在原像方面仍然(几乎)和以往一样强大(理论上的攻击在实践中仍然非常不可能运行)。

MD5的真正问题是它通常用于密码散列,它非常快,而且没有加盐。但是,与 MD5 一起使用的 PBKDF2 将是稳健的。您仍应将 SHA-1 或 SHA-256 与 PBKDF2 一起使用,但用于公共关系。人们在听到“MD5”时会感到紧张。

盐生成

盐的主要和唯一一点是尽可能独特每当在任何地方重用盐值时,这都有可能帮助攻击者。

例如,如果您使用用户名作为 salt,那么攻击者(或多个共谋的攻击者)可能会发现构建彩虹表来攻击密码散列函数是值得的,当 salt 为“admin”(或“root”或“joe”时) ") 因为世界上会有几个,可能很多站点都有一个名为“admin”的用户。同样,当用户更改密码时,他通常会保留自己的名字,从而导致盐重用。旧密码是有价值的目标,因为用户有在多个地方重复使用密码的习惯(这被认为是一个坏主意,并因此宣传,但他们仍然会这样做,因为它让他们的生活更轻松),而且人们倾向于“按顺序”生成他们的密码:如果你知道 Bob'当前密码可能是“SuperSecretPassword38”或“SuperSecretPassword39”。

获得唯一性廉价方法是使用随机性如果您从操作系统提供的加密安全 PRNG/dev/urandom ( , CryptGenRandom()...) 中将 salt 生成为随机字节序列,那么您将获得“具有足够高概率的唯一性”的 salt 值。16 个字节就足够了,这样你就永远不会在你的生活中看到盐冲突,这有点矫枉过正,但也足够简单。

UUID是生成“唯一”值的标准方式。请注意,“版本 4”UUID 仅使用随机性(122 个随机位),如上所述。许多编程框架提供了简单易用的函数来按需生成 UUID,它们可以用作盐。

盐保密

盐并不意味着秘密。否则我们会称它们为keys你不需要公开盐,但如果你必须公开它们(例如支持客户端散列),那么不要太担心它。盐是为了独特性。严格来说,盐只不过是在一大类函数中选择一个特定的哈希函数。

“胡椒”

密码学家永远不能单独使用隐喻;他们必须用更多的类比和糟糕的双关语来扩展它。“Peppering”是关于使用秘密盐,即密钥。如果您在密码散列函数中使用“胡椒”,那么您将切换到一种完全不同的加密算法;即,您正在通过密码计算消息验证码MAC 密钥是您的“辣椒”。

如果您可以拥有攻击者无法读取的密钥,则 Peppering 是有意义的。请记住,我们使用密码哈希是因为我们认为攻击者可以获取服务器数据库的副本,或者可能获取服务器的整个磁盘一个典型的场景是在RAID 1中有两个磁盘的服务器. 一个磁盘发生故障(电子板薯条 - 这种情况经常发生)。系统管理员更换磁盘,重建镜像,RAID 1 的魔力不会丢失任何数据。由于旧磁盘功能失调,系统管理员无法轻松擦除其内容。他只是丢弃了磁盘。攻击者在垃圾袋中搜索,取回磁盘,更换板子,瞧!他拥有整个服务器系统的完整图像,包括数据库、配置文件、二进制文件、操作系统……正如英国人所说的那样。要真正应用胡椒粉,您需要在一个特殊的设置中,其中除了带有磁盘的 PC 之外还有其他东西;你需要一个HSM. HSM 在硬件和操作过程中都非常昂贵。但是使用 HSM,您可以只使用秘密的“pepper”并使用简单的HMAC(例如使用 SHA-1 或 SHA-256)处理密码。这将比 bcrypt/PBKDF2/scrypt 及其繁琐的迭代更有效。此外,在进行WebTrust 审计时,HSM 的使用看起来非常专业。

客户端哈希

由于散列(故意)昂贵,因此在客户端-服务器情况下,利用连接客户端的 CPU 可能是有意义的。毕竟,当 100 个客户端连接到单个服务器时,客户端总体上比服务器拥有更多的功能。

要执行客户端散列,必须增强通信协议以支持将盐发送回客户端。与简单的客户端发送密码到服务器协议相比,这意味着额外的往返。这可能会也可能不会很容易添加到您的特定案例中。

客户端散列在 Web 环境中很困难,因为客户端使用 JavaScript,这对于 CPU 密集型任务来说是相当贫乏的。

SRP的上下文中,密码散列必然发生在客户端。

结论

使用 bcrypt。PBKDF2 也不错。如果您使用 scrypt,您将成为“稍微早期的采用者”,并带有此表达式所暗示的风险;但这对科学进步来说将是一个很好的举措(“碰撞假人”是一个非常光荣的职业)。

为了存储密码哈希,您需要一个足够慢的算法,以使暴力攻击不可行。加盐密码将有助于抵御彩虹攻击,但不能抵御暴力攻击。为了存储密码哈希,您需要使用专门为此目的设计的算法;如:

scrypt是新的但很有趣,因为它不仅使用可变工作因子,而且还使用内存硬函数。这大大增加了蛮力攻击的成本,因为运行时间和内存需求都增加了。

作为散列值存储在数据库中的密码可以通过散列的蛮力计算或通过使用彩虹表(特定于所使用的算法)来恢复。

彩虹表是作为字典文件的一系列预先计算的值创建的,或者更常见的是给定字符集 [az, AZ, 0-9] 的每个组合,这是一个常见的例子。

本质上,他们可以通过允许在表中查找哈希值来加速密码破解,而不是要求攻击者为每个密码创建哈希值。可以在网上找到常见密码算法(例如 NTLM、MD5 等)的彩虹表,这使得访问大量此类算法变得相当简单。

有多种方法可以提高存储在数据库中的哈希值的安全性。

首先是使用每个用户的盐值,该值与散列密码一起存储在数据库中。它并不打算保密,但用于减慢暴力破解过程并使彩虹表无法使用。

我看到的另一个附加功能是还添加了所谓的胡椒值。这只是另一个随机字符串,但对所有用户都是相同的,并与应用程序代码一起存储,而不是存储在数据库中。这里的理论是,在某些情况下,数据库可能会受到损害,但应用程序代码不会受到损害,在这些情况下,这可以提高安全性。但是,如果有多个应用程序使用相同的密码数据库,它确实会带来问题。

帮助提高密码安全性的第三种方法是使用慢密码功能,这不会对个人用户产生巨大影响,但会大大减慢攻击者破解从数据库中检索到的密码的速度。有关此方法的更多信息可在此处获得

更新 4:到 2016 年,硬件改进和其他因素导致比特币哈希率在 2011 年首次撰写这篇文章后的 5 年内上升了100,000 多倍(!)。密码破解技术也有所改进软件结束。所以用户应该在他们的密码的最小长度上添加更多的字符,并且需要增加迭代次数,我们都真的需要准备转向更好的算法,比如Argon2

更新 3:2015 年,密码哈希竞赛选出了获胜者: Argon2它被设计成“内存硬”,以使破解者难以实现 GPU;简单的; 高度可配置;抵抗侧通道泄漏等。如果它通过了时间的考验,这可能是向前迈出的重要一步,但正如 Thomas 在是否有比 bcrypt 和 scrypt 更现代的密码散列方法所指出的那样,你应该警惕闪亮的新算法,并可能给专业人士更多的时间来寻找弱点。

更新 2:在 2013 年,几位专家发起了密码哈希竞赛,该竞赛应该会带来改进和更可用的方法,并在 2015 年选出获胜者。有关需要的优秀背景以及临时的良好建议,请参阅密码安全:过去,现在,未来来自Passwords^12请注意,越来越快的硬件(如下所述)的出现意味着需要像 scrypt 这样的内存密集型算法,并且与 PBKDF2 或 crypt 不同,bcrypt 仍然可以抵抗 GPU 攻击。


这里的其他人指出,暴力攻击需要通过盐来防御,尽管 MYSQL 仍然没有弄清楚这一点。迭代的重要性也已被​​注意到,并且自从Robert Morris 和 Ken Thompson于 1978 年发表关于 Unix crypt 的开创性论文以来就为人所知。但是许多人(以及开发人员,比如 Django!)显然仍然认为蛮力必须花费相当长的时间或相当昂贵,因此认为 SHA-1 的单次迭代对于密码散列是可以的。

不对!摩尔定律和云计算已经赶上了我们。在现代台式机上破解长度为 8 的字母数字密码的 SHA-1 哈希((26+26+10)^8 = 62^8 = 218,340,105,584,896 = 218 万亿个组合)需要 5 天,如果您租用则需要 1 小时一堆亚马逊计算节点(实际生成彩虹表需要多长时间?- IT 安全

更新:比特币哈希能力

地球上最强大的有组织的散列能力(不包括可能的分类系统)是比特币 挖矿网络[截至 2011 年 5 月] 以超过11 Thash/s的总速率执行 SHA-256 哈希,即 11 * 10^12 hash/s(到 2016 年为 1700000 Thash/s - 请参阅上面的更新 4),最近这个比率一直在迅速上升(图表)。矿工们正在努力以每比特币(BTC)14 美元的当前价格挖矿每周赚取估计 700,000 美元(图表),每 10 分钟产生 50 BTC。目前流行的硬件包括 Radeon HD 5970 GPU,每个 GPU 总共有 3200 个流处理器,可以处理大约 800 Mhash/s。它的功耗也很节俭,约为 2.3 Mhash/Joule。有关更多选项,请参阅比特币挖矿硬件比较。事实证明,亚马逊 EC2 上的 GPU 节点使用的是 Nvidia Tesla GPU,其哈希效率较低,而且以今天的价格,它们的节点对于挖矿来说并不划算。

这大约是5.5 Thash/s 估计的世界 500 强超级计算机的散列能力总和的两倍,当然,超级计算机通常是为浮点性能设计的,而不是散列。

作为当前的一个极端情况,如果这种哈希能力被重定向到试图破解密码,例如在比特币价格暴跌之后,那么对于非迭代密码算法将是可怕的。使用所有 94 个打印字符的完全随机分类的 8 个字符密码将在不到 10 分钟内下降 (94^8 / (11 * 10^12 * 60) = 9.2)。10 个字符的密码需要不到 57 天 (94^10 / (11 * 10^12 * 3600 * 24) = 56.7)。10 个字符的大小写字母数字密码(26+26+10 = 62 个可能的字符)即使随机化也只需不到一天的时间(62^10 / (11 * 10^12 * 3600 * 24) = 0.88)。

但是,如果程序员像 Thomas 建议的那样简单地使用例如 2000 次迭代计数,那么好的 10 个字符的密码将持续数年。虽然 8 个字符的密码很容易被破解,但在 13 天内(2000 * 94^8 / 11 10^12 / 3600 / 24 = 12.8 天)。

也可以看看:

  • 暴力破解算法的问题与其他从影子文件中恢复密码的方法相比 - IT 安全
  • 如何在应用程序/服务器级别防止暴力破解