如何安全地存储一个密码(译)

前言

如何安全地存储一个密码(译)。原文地址:How To Safely Store A Password

正文

使用 bcrypt 库

使用 bcrypt
使用 bcrypt
使用 bcrypt
使用 bcrypt
使用 bcrypt

为什么不使用诸如 MD5,SHA1,SHA2,SHA3 等等算法?

这些都是通用的散列函数,它们被设计用来尽可能使用少的时间来计算一个大数据的摘要。这意味着它们能够很好地确保数据完整性,但在存储密码上完全就是垃圾。

一个现代的服务器每秒可以计算 330MB 大小的 MD5 值。如果你的用户使用 6 位小写字母数字组成的密码的话,你可以在大约 40 秒内对所有可能的密码进行尝试。

而且这无需投资任何东西。

如果你愿意的话,花费 2000 刀以及一两周时间来购买 CUDA ,你可以组建你自己的小型超算集群,这样你可以在 1 秒内尝试大约 7 亿个密码。在这样的速度下你将以每秒超过 1 个的速度破解这些密码。

加盐并不能帮助你

需要注意的是,加盐对预防字典攻击或者暴力攻击是无效的。你可以使用巨大的盐值或者多个盐值,甚至是手动筛选的,位于阴影处,有机的喜马拉雅粉红盐(这里应该是打趣而已,没有搜到关于喜马拉雅粉红盐在编程方面的信息),对于从数据库中给定的散列值和盐值,它并不会影响攻击者尝试候选密码的速度。

如果你是使用一个通用的为速度设计的散列函数,是否加盐没有意义。

bcrypt 解决了这些问题

bcrypt 怎么样?基本上来说它慢的要死。它使用了 Blowfish 加密算法的密钥表的一个变体,并且引入了工作因子,工作因子允许你决定散列函数需要的执行成本。由于引入了它, bcrypt 可以跟上摩尔定律。当电脑执行速度变快,你可以增加工作因子来让散列过程变慢。

bcrypt 有多慢?对比 MD5 ?这依赖工作因子参数。使用一个值为 12 的工作因子, bcrypt 在我的笔记本上散列一个 yaaa 的密码需要大约 0.3 秒。而 MD5 只需要不超过 1 毫秒。

所以这里是 5 个左右的数量级。破解一个密码不再是 40 秒,而是要 12 年左右。你的密码可能不需要这样的安全性,并且你可能需要的是一个更快的比较算法,但是 bcrypt 允许你在速度和安全间选择一个平衡,所以,使用它就对了。

总结

使用 bcrypt

2011.02.24 更新

在过去的一年中,我收到了很多关于这篇文章的邮件,我觉得我应该在这里解决一些问题,而不是一次又一次的重复对话。

bcrypt 不就是 Blowfish 吗?你是在哪里存储密码的?

请阅读这篇文章。 bcrypt 是一个使用了 Blowfish 密钥表的一个自适应密码散列算法, 不是对称加密算法。

你说盐没有用,那彩虹表呢?
为什么你不建议使用盐呢?

正如这篇文章讨论的一样, bcrypt 内置了盐来防止彩虹表攻击。所以我没说盐是没有目的的,我的意思是它们不能防止字典攻击或者暴力攻击。

尽管在博客帖子中把彩虹表作为主题很受欢迎,但它已经落伍了。 基于 CUDA/OPENCL 的密码破解程序可以大量利用 GPU 内的并行能力, 1 秒可以验证数十亿个候选密码。你可以按照字面上的意思来测试一个长度不大于 7 的只包含小写的密码,花费的时间不会超过 2 秒。现在你还可以租用硬件,每小时的费用不超过 3 刀。大约需要每小时 300 刀,你就可以在 1 秒内破解大约 5000 亿个候选密码。

鉴于加密攻击经济学的巨大转变,对于任何人来说,浪费数 TB 的硬盘空间来希望受害者不使用盐是没有意义的。破解密码要容易的多。即使是一个“好”的比如 SHA2256 的散列方案,仍然完全容易受到这些廉价但有效的攻击,因此,像 bcrypt 这样的自适应散列算法就很重要了。

后记

这篇文章是我在看 nestjs 文档的时候,在 SECURITY - Authentication 一节中看到的,觉得挺有意思的,就写了下来。

作者的观点还是挺“偏激”的,我是觉得即使 MD5 的计算速度很快,但密码验证依然得基于 client - server 的形式,在 server 上肯定是不会让用户能够以一个极快的速度对一个类似登录验证的接口进行调用的,在接口上完全可以做一些策略来防止这种情况,比如某个 ip 多少次验证登录请求失败就封禁一段时间,给登录加上短信验证,邮箱验证等等。

不过我也不是专门写后端的,可能还有一些没有想到的情况…,刚好这种情况就是需要强安全的密码散列的。

不过我们公司目前都是使用加盐 MD5 来进行密码散列的。