编码,加密,与密码哈希演进简史

301 天前
 logto

前言

字如其名,密码哈希是从密码计算哈希值的过程。哈希值通常存储在数据库中,在登录过程中,用户输入的密码的哈希值会被计算并与存储在数据库中的哈希值进行比较。如果匹配成功,则验证通过。

在我们深入了解密码哈希算法的演进之前,我们先简单探讨一下为什么需要它。

明文密码:一个重大的安全风险

想象一下,你是一个网站的用户,并在该网站注册了一个账户。有一天,该网站被黑客攻击,数据库泄漏了。如果网站以明文形式存储密码,黑客可以直接访问你的密码。由于许多人在多个网站上重复使用密码,黑客可以使用这个密码来未经授权地访问你的其他账户。如果你在电子邮件账户上使用相同或类似的密码,情况会变得更糟,因为黑客可以重置你的密码并接管你所有关联的账户。

即使没有数据泄漏,在大型团队中,任何有数据库访问权限的人都可以看到密码。与其他信息相比,密码具有高度敏感性,你绝对不希望任何人能够访问它们。

很明显,明文存储密码是一个业余的错误。不幸的是,如果你搜索“password leak plaintext”,你会发现像 FacebookDailyQuizGoDaddy 这样的大公司都曾遭遇明文密码泄漏。很可能还有许多其他公司犯了同样的错误。

编码( 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 )

哈希、编码和加密之间的主要区别在于哈希是不可逆的。一旦密码经过哈希处理,就无法解密回其原始形式。

作为网站所有者,你实际上不需要知道密码本身,只要用户可以使用正确的密码登录即可。注册过程可以简化如下:

  1. 用户输入密码。
  2. 服务使用哈希算法计算密码的哈希值。
  3. 服务将哈希值存储在数据库中。

用户登录时的过程是:

  1. 用户输入密码。
  2. 服务使用相同的哈希算法计算密码的哈希值。
  3. 服务将哈希值与存储在数据库中的哈希值进行比较。
  4. 如果哈希值匹配,用户得到验证。

这两个过程都避免了以明文形式存储密码,并且由于哈希是不可逆的,即使数据库被入侵,黑客只能获取到看起来像随机字符串的哈希值。

哈希算法入门包

哈希可能看起来是密码存储的完美解决方案,但事实并非如此简单。为了了解其中的原因,让我们探讨一下密码哈希算法的演进。

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.jsnode-argon2 和 Python 的 argon2-cffi

结论

多年来,密码哈希算法经历了显著的演进。我们应该感谢安全社区几十年来在使互联网更安全方面所做的努力。由于他们的贡献,使得广大开发者可以更加专注于构建更好的服务和产品,而不用担心密码哈希的安全性。于此同时,虽然无法构建 100% 安全的系统(例如 social engineering 总是防不胜防),但是我们可以通过不同的手段不断降低安全风险发生的概率。

如果你想避免实现身份验证和授权的麻烦,可以尝试 Logto 。我们提供安全( Logto 使用 Argon2 !)、可靠和可扩展的开源及 cloud 解决方案。

网站 https://logto.io GitHub https://github.com/logto-io

2410 次点击
所在节点    程序员
20 条回复
Mutoo
301 天前
文章讲得不错,能不能再讲讲啥是 side-channel 攻击?
cheese
301 天前
文章阅读很顺畅,喜欢这种推广
willxiang
301 天前
喜欢这种推广
logto
301 天前
@Mutoo 谢谢,有机会另写一篇文章介绍一下
@cheese 谢谢
dearmymy
301 天前
其实程序员都应该看看 日本人写那本 密码学。
aikilan
301 天前
真不错
huangqihong
301 天前
@dearmymy 哪本啊
dearmymy
301 天前
@huangqihong https://book.douban.com/subject/26265544/ 跟看小说一样,非常经典,一点不枯燥。
huangqihong
301 天前
@dearmymy 感谢
wangjiang
301 天前
写的很好,感谢
duke807
301 天前
我和大家最后还是选择 sha256 这样的通用算法
xiaochuaner
301 天前
额,一般来说“密码哈希”这个词是指密码算法领域的哈希算法,即具有抗碰撞、原相等性质的哈希算法,应该跟数据库里的密码(正确翻译应该是口令,password )没啥关系吧。没想到还有这种用法。
kasusa
301 天前
有类似 cmd5 这种网站,可用解密常见的 hash 值
原理是他建立了巨大的数据库,把常见的口令、盐 都计算好结果,然后与你给出的结果比较。
如果你的密码是 p@ssword123 这种,hash 之后也能很轻松倍破解开。
如果你的密码是 Z'n|GW"@.;Y<Lh}sH:&d 这种(我用密码生成器随机生成的) 那么 99.9% 是无法被破解的。
dode
301 天前
@kasusa
bcrypt 算法
盐值的长度限制为 16 个字节

盐是每个密码随机产生的,整个数据库里所有的密码都不一样,同一个密码多设置几次都不一样,md5 撞库是不行的
mitoop
301 天前
行文很好👍
oColtono
301 天前
很赞,这个推广吃下了
chenjia404
300 天前
我的这个方案,抗脱库,抗彩虹表,计算速度快 https://v2ex.com/t/956852
CodeCodeStudy
300 天前
加密那里可以展开讲讲,对称加密,非对称加密
stamhe
248 天前
@kasusa 黑客和安全组织手上一大把。
stamhe
248 天前
邮箱验证码登录,手机验证码登录。。。怎么想的呢?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/957713

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX