字如其名,密码哈希是从密码计算哈希值的过程。哈希值通常存储在数据库中,在登录过程中,用户输入的密码的哈希值会被计算并与存储在数据库中的哈希值进行比较。如果匹配成功,则验证通过。
在我们深入了解密码哈希算法的演进之前,我们先简单探讨一下为什么需要它。
明文密码:一个重大的安全风险
想象一下,你是一个网站的用户,并在该网站注册了一个账户。有一天,该网站被黑客攻击,数据库泄漏了。如果网站以明文形式存储密码,黑客可以直接访问你的密码。由于许多人在多个网站上重复使用密码,黑客可以使用这个密码来未经授权地访问你的其他账户。如果你在电子邮件账户上使用相同或类似的密码,情况会变得更糟,因为黑客可以重置你的密码并接管你所有关联的账户。
即使没有数据泄漏,在大型团队中,任何有数据库访问权限的人都可以看到密码。与其他信息相比,密码具有高度敏感性,你绝对不希望任何人能够访问它们。
很明显,明文存储密码是一个业余的错误。不幸的是,如果你搜索“password leak plaintext”,你会发现像 Facebook、DailyQuiz 和 GoDaddy 这样的大公司都曾遭遇明文密码泄漏。很可能还有许多其他公司犯了同样的错误。
编码( encoding ),加密( encryption ),哈希( hashing )
这三个术语经常被混淆,但它们是不同的概念。
编码( encoding )
编码是密码存储中首先要排除的内容。例如,Base64 是一种编码算法,将二进制数据转换为字符字符串( Node.js ):
const data = 'Hello, world!';
const encoded = Buffer.from(data).toString('base64');
console.log(encoded); // SGVsbG8sIHdvcmxkIQ==
知道编码算法后,任何人都可以解码编码后的字符串并恢复原始数据:
const encoded = 'SGVsbG8sIHdvcmxkIQ==';
const data = Buffer.from(encoded, 'base64').toString();
console.log(data); // Hello, world!
对于黑客来说,大多数编码算法等同于明文。
加密( encryption )
在哈希算法流行之前,加密也被用于存储密码,例如使用 AES 。加密涉及使用密钥(或一对密钥)对数据进行加密和解密。
加密的问题在于“解密”一词。这意味着加密是可逆的,如果黑客获得密钥,他们可以解密密码并获取明文密码。
哈希( hashing )
哈希、编码和加密之间的主要区别在于哈希是不可逆的。一旦密码经过哈希处理,就无法解密回其原始形式。
作为网站所有者,你实际上不需要知道密码本身,只要用户可以使用正确的密码登录即可。注册过程可以简化如下:
[ol]
[/ol]
用户登录时的过程是:
[ol]
[/ol]
这两个过程都避免了以明文形式存储密码,并且由于哈希是不可逆的,即使数据库被入侵,黑客只能获取到看起来像随机字符串的哈希值。
哈希算法入门包
哈希可能看起来是密码存储的完美解决方案,但事实并非如此简单。为了了解其中的原因,让我们探讨一下密码哈希算法的演进。
MD5
1992 年,Ron Rivest 设计了 MD5 算法,这是一种消息摘要算法,可以从任意数据计算出一个 128 位的哈希值。MD5 已广泛用于各个领域,包括密码哈希。例如,"123456" 的 MD5 哈希值为:
e10adc3949ba59abbe56e057f20f883e
如前所述,哈希值看起来像随机字符串,并且是不可逆的。此外,MD5 速度快、易于实现,使其成为最流行的密码哈希算法。
然而,MD5 的优点也是密码哈希中的弱点。它的速度使得它容易受到暴力破解的攻击。如果黑客拥有常见密码列表和你的个人信息,他们可以计算每个组合的 MD5 哈希值,并将其与数据库中的哈希值进行比较。例如,他们可能将你的生日与你的姓名或宠物的名字组合起来。
如今,计算机的计算能力远远超过以前,使得暴力破解 MD5 密码哈希变得容易。
SHA 家族
那么,为什么不使用生成更长哈希值的算法呢?SHA 家族 似乎是一个不错的选择。SHA-1 是一种生成 160 位哈希值的哈希算法,而 SHA-2 是一系列生成 224 位、256 位、384 位和 512 位长度哈希值的哈希算法。让我们看看 "123456" 的 SHA-256 哈希值:
8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
SHA-256 哈希值比 MD5 长得多,而且也是不可逆的。然而,还存在一个问题:如果你已经知道哈希值,比如上面那个哈希值,并且在数据库中看到了完全相同的哈希值,那么你就会知道密码是 "123456"。黑客可以创建一个常见密码和它们对应的哈希值列表,并将其与数据库中的哈希值进行比较。这个列表被称为彩虹表。
盐值( salt )
为了减轻彩虹表攻击,引入了盐值的概念。盐值是在哈希之前添加到密码的随机字符串。例如,如果盐值是 "salt",那就使用 SHA-256 将密码 "123456" 与盐值一起哈希。假设原有函数:
sha256('123456');
改成这样即可:
sha256('salt123456'); // 9898410d7f5045bc673db80c1a49b74f088fd7440037d8ce25c7d272a505bce5
正如你所看到的,结果与不带盐值的哈希完全不同。通常,每个用户在注册过程中被分配一个随机的盐值,并且存储在数据库中的密码哈希值旁边。在登录过程中,盐值用于计算输入密码的哈希值,然后将其与存储的哈希值进行比较。
迭代( iteration )
尽管添加了盐值,但随着硬件的增强,哈希值仍然容易受到暴力破解的攻击。为了增加难度,可以引入迭代(即多次运行哈希算法)。例如,将算法:
sha256('salt123456');
改成这样:
sha256('salt' + sha256('salt123456'));
增加迭代次数使暴力破解更加困难。然而,这也会影响登录过程,使其变慢。因此,需要在安全性和性能之间取得平衡。
中场休息
让我们休息一下,并总结一下一个好的密码哈希算法的特点:
你可能已经注意到,盐值和迭代都是满足所有这些要求所必需的。问题在于,MD5 和 SHA 家族都不是专门为密码哈希而设计的;它们广泛用于完整性检查(或“消息摘要”)。因此,每个网站可能都有自己的盐值和迭代实现,使得标准化和迁移具有挑战性。
密码哈希算法
为了解决这个问题,一些专门为密码哈希而设计的哈希算法已经出现。让我们来看看其中一些。
bcrypt
bcrypt 是由 Niels Provos 和 David Mazières 设计的一种密码哈希算法。它在许多编程语言中广泛使用。下面是 bcrypt 哈希值的一个示例:
$2y$12$wNt7lt/xf8wRJgPU7kK2juGrirhHK4gdb0NiCRdsSoAxqQoNbiluu
尽管它看起来像另一个随机字符串,但它包含了额外的信息。让我们拆分一下:
[$2y][$12][$wNt7lt/xf8wRJgPU7kK2ju][GrirhHK4gdb0NiCRdsSoAxqQoNbiluu]
bcrypt 有一些限制:
Argon2
考虑到现有密码哈希算法的争议和限制(也许是大家累了),在 2015 年举行了一场 密码哈希竞赛。其中细节在此不表,让我们聚焦于获胜者:Argon2 。
Argon2 是由 Alex Biryukov 、Daniel Dinu 和 Dmitry Khovratovich 设计的密码哈希算法。它引入了几个新特性:
Argon2 有两个主要版本,Argon2i 和 Argon2d 。Argon2i 对 side-channel 攻击最安全,而 Argon2d 对 GPU 破解攻击提供了最高的抵抗力。
-- Argon2
下面是 Argon2 哈希值的一个示例:
$argon2i$v=19$m=16,t=2,p=1$YTZ5ZnpXRWN5SlpjMHBDRQ$12oUmJ6xV5bIadzZHkuLTg
让我们拆分一下:
[$argon2i][$v=19][$m=16,t=2,p=1][$YTZ5ZnpXRWN5SlpjMHBDRQ][$12oUmJ6xV5bIadzZHkuLTg]
在 Argon2 中,密码的最大长度为 2^32-1 个字节,盐值的长度限制为 2^32-1 个字节,哈希值的长度限制为 2^32-1 个字节。这在大多数情况下应该够用了。:-)
现在,Argon2 已经在许多编程语言中可用,比如 Node.js 的 node-argon2 和 Python 的 argon2-cffi。
结论
多年来,密码哈希算法经历了显著的演进。我们应该感谢安全社区几十年来在使互联网更安全方面所做的努力。由于他们的贡献,使得广大开发者可以更加专注于构建更好的服务和产品,而不用担心密码哈希的安全性。于此同时,虽然无法构建 100% 安全的系统(例如 social engineering 总是防不胜防),但是我们可以通过不同的手段不断降低安全风险发生的概率。
如果你想避免实现身份验证和授权的麻烦,可以尝试 Logto 。我们提供安全( Logto 使用 Argon2 !)、可靠和可扩展的开源及 cloud 解决方案。
网站 https://logto.io
GitHub https://github.com/logto-io