密码散列的演化
你可能听过关于选择密码散列算法的建议,但你是否思考过为什么它们被推荐?在这篇文章中,我们将探讨密码散列算法的演变及其背后的原因。
介绍
如其名,密码散列是从密码计算出一个散列值的过程。散列值通常存储在数据库中,在登录(签入)过程中,计算用户输入密码的散列值,并与存储在数据库中的散列值进行比较。如果他们匹配,用户就会被认证。
在深入研究密码散列算法的演变之前,我们首先要理解为什么需要它。
明文密码:一个重大的安全风险
想象一下你是一个网站的用户,并且你在其中注册了一个账户。有一天,这个网站被黑客攻破,数据库被泄露。如果网站以明文存储密码,黑客就可以直接取得你的密码。由于许多人在多个网站上重复使用密码,黑客可以用这个密码获得你在其他网站上的非授权访问权限。如果你对你的电子邮件账户使用同样或类似的密码,情况就会变得更糟,因为黑客可以重置你的密码,并接管你所有关联的账户。
即使没有数据泄露,在大型团队中,任何有数据库访问权限的人都可以看到密码。与其他信息相比,密码非常敏感,你绝对不希望任何人可以访问到它们。
无散列存储密码是一个新手错误。不幸的是,如果你搜索 "密码泄露明文",你会发现像 Facebook、DailyQuiz 和 GoDaddy 等大型公司都曾因明文泄露密码。很可能还有许多其他公司犯了同样的错误。
编码 v.s. 加密 v.s. 散列
这三个术语常常被混淆,但它们是不同的概念。
编码
编码是密码存储排除的第一项。例如,Base64是一个编码算法,它将二进制数据转换为一个字符串:
知道了编码算法,任何人都可以解码这个编码字符串,获取原始数据:
对于黑客来说,大部分编码算法等同于明文。
加密
在散列流行之前,加密被用来存储密码,例如使用 AES。加密涉及使用一个密钥(或一对密钥)来加密和解密数据。
加密的问题在于 "解密" 这个词。加密是可逆的,这意味着如果黑客获得了密钥,他们就可以解密密码,取得明文密码。
散列
散列,编码和加密的主要区别在于,散列是 不可逆的。一旦一个密码被散列,它就无法被解密回原始形式。
作为一个网站所有者,您实际上并不需要知道密码本身,只要用户可以用正确的密码登录就行。注册过程可以简化如下:
- 用户输入密码。
- 服务使用散列算法计算密码的散列值。
- 服务将散列值存储在数据库中。
当用户登录时,过程是:
- 用户输入密码。
- 服务使用同样的散列算法计算密码的散列值。
- 服务将散列值与存储在数据库中的散列值进行比较。
- 如果散列值匹配,用户就被认证。
这两个过程都避免了以明文存储密码,而且由于散列是不可逆的,即使数据库被攻破,黑客也只能获得看起来像是随机字符串的散列值。
散列算法入门包
散列可能看起来像密码存储的完美解决方案,但事实并非如此。为了理解为什么,让我们探讨一下密码散列算法的演变。
MD5
1992 年,Ron Rivest 设计了 MD5 算法,这是一个可以从任何数据计算出128位散列值的消息摘要算法。MD5 在各个领域都得到了广泛的应用,包括密码散列。例如, "123456" 的 MD5 散列值是:
如前面所述,散列值看起来像一个随机字符串,是不可逆的。而且,MD5 快速且易于实现,使得它成为最受欢迎的密码散列算法。
然而,MD5 在密码散列中的优势也是其弱点。它的速度使得它容易受到蛮力攻击。如果黑客拥有常用密码和你的个人信息的列表,他们可以计算每种组合的 MD5 散列值,然后将它们与数据库中的散列值进行比较。例如,他们可能会将你的生日与你的名字或你的宠物的名字组合。
在现代,计算机的性能比以往任何时候都要强大,这使得轻易地蛮力攻击 MD5 密码散列变为可能。
SHA 系列
那么,为什么不使用生成较长散列值的不同算法呢?SHA 系列似乎是一个不错的选择。SHA-1 是一个生成160位散列值的散列算法,SHA-2 是一个散列算法家族,它生成的散列值长度有 224位、256位、384位和 512位。让我们看一下 "123456" 的 SHA-256 散列值:
SHA-256 的散列值比 MD5 长得多,并且它也是不可逆的。然而,还有另一个问题:如果你已经知道散列值,比如上面的那个,而你在数据库中看到了完全相同的散列值,你就知道密码是 "123456"。一个黑客可以创建一个常用密码和它们对应散列值的列表,然后将它们与数据库中的散列值进行比较。这个列表被称为彩虹表。
盐
为了缓解彩虹表攻击,引入了盐的概念。盐是一个在散列之前添加到密码中的随机字符串。例如,如果盐是 "salt",你想使用 SHA-256 与盐一起散列密码 "123456",你就要这样做,而不是直接这样做:
你会这样做:
如你所见,结果与不使用盐的散列完全不同。通常,在注册过程中,每个用户会分配一个随机盐,该盐将与散列值一同存储在数据库中。在登录过程中,将使用盐来计算输入密码的散列值,然后将该值与存储的散列值进行比较。
迭代
尽管加入了盐,随着硬件的越来越强大,散列值仍然易于蛮力破解。为了使其更难,可 以引入迭代(即,运行多次散列算法)。例如,你可以使用:
增加迭代次数可以使蛮力破解变得更难。然而,这也影响了登录过程,使其变慢。因此,需要在安全性和性能之间寻找平衡。
中场休息
让我们休息一下,总结一下好的密码散列算法的特性:
- 不可逆(抗原像攻击)
- 难以蛮力破解
- 抵抗彩虹表攻击
你可能已经注意到,盐和迭代对于满足所有这些要求都是必要的。问题在于 MD5 和 SHA 家族并未专门为密码散列而设计;它们被广泛用于完整性检查(或 "消息摘要")。因此,每个网站可能都有自己的盐和迭代的实现,这使得标准化和迁移变得困难。
密码散列算法
为了解决这个问题,有几个散列算法专门为密码散列而设计。让我们来看一下其中的一些。
bcrypt
bcrypt 是由 Niels Provos 和 David Mazières 设计的一个密码散列算法。它在许多编程语言中得到了广泛的应用。以下是一个 bcrypt 散列值的例子:
虽然它看起来像另一个随机字符串,但它包含了额外的信息。让我们来分解一下:
- 第一部分
$2y
表示的是算法,这里是2y
。 - 第二部分
$12
表示的是迭代次数,这里是12
。这意味着散列算法将会运行 212=4096 次(迭代次数)。 - 第三部分
wNt7lt/xf8wRJgPU7kK2ju
是盐。 - 最后一部分
GrirhHK4gdb0NiCRdsSoAxqQoNbiluu
是散列值。
bcrypt 有一些限制:
- 密码的最大长度是 72 字节。
- 盐限制为 16 字节。
- 散列值限制为 184 位。
Argon2
鉴于现有密码散列算法的争议和限制,2015 年举行了一个 密码散列竞赛。跳过细节,我们只关注赢家:Argon2。
Argon2 是由 Alex Biryukov、Daniel Dinu 和 Dmitry Khovratovich 设计的一个密码散列算法。它引入了几个新的概念:
- 内存硬度:算法被设计为难以并行化,使 GPU 蛮力破解变得困难。
- 时间硬度:算法被设计为难以优化,使得针对 ASIC(应用特定集成电路)的蛮力破解变得困难。
- 抗侧信道攻击:算法被设计为抵抗侧信道攻击,比如定时攻击。
Argon2 有两个主要版本,Argon2i 和 Argon2d。Argon2i 对抗侧信道攻击最安全,而 Argon2d 提供了最高的抵抗 GPU 破解攻击的能力。
-- Argon2
这是一个 Argon2 散列值的例子:
让我们分解一下:
- 第一部分
$argon2i
表示的是算法,这里是argon2i
。. - 第二部分
$v=19
表示的是版本,这里是19
。 - 第三部分
$m=16,t=2,p=1
表示的是内存消耗、时间消耗和并行度,分别是16
、2
和1
。 - 第四部分
$YTZ5ZnpXRWN5SlpjMHBDRQ
是盐。 - 最后一部分
$12oUmJ6xV5bIadzZHkuLTg
是散列值。
在 Argon2 中,密码的最大长度是 232-1 字节,盐限制为 232-1 字节,散列值限制为 232-1 字节。这对于大多数情况应该是足够的。
Argon2 现在已经在许多编程语言中可用,如针对 Node.js 的 node-argon2 和针对 Python 的 argon2-cffi。
结论
多年来,密码散列算法经历了重大的演变。我们要感谢安全社区多年来的努力,使得互联网变得更加安全。因为他们的贡献,开发者可以更多地关注建立更好的服务,而不用担心密码散列的安全性。虽然在一个系统中实现100%的安全性可能是不可达成的,但我们可以采用多种策略来最小化相关的风险。
如果你想避免实施身份验证和授权的麻烦,欢迎免费试用 Logto。我们提供安全(我们使用 Argon2!)、可靠、可扩展的解决方案,让你可以专注于构建你的产品。