安全代码审查是我每天都要做的一项任务,在过去的十三年半中,我一直在做这项任务。在这期间,我审查了几百个代码库,并多次遇到加密代码。我审查过的加密代码,经常存在安全问题。我追溯这些伪造的代码片段,经常会追溯到在 StackOverflow 上得到高票支持的答案。在本博文中,我会指出这些糟糕的代码片段,并解释为什么它们是错误的。我还会就此给出正确代码的建议。
我这样做不是为了羞辱那些犯了错误的人,相反,我想尽自己的一份力量来帮助修复这些问题。作为一名应用程序安全(AppSec)专家,我真的厌倦了一遍又一遍讨论相同的问题。我非常努力地想让人们做正确的事情:我给他们指出可以安全使用的代码,例如 Luke Park 的安全的兼容的加密示例(Secure Compatible Encryption Examples)。尽管如此,偶尔还会有团队进行抵制,甚至在代码进入生产之前,而这往往是修复这个问题的最佳时机。因此,我不得不花费时间来向他们解释为什么这些代码是错误的,因为一旦糟糕的加密技术投入生产,就需要一个迁移计划来修复它。
我相信未来的加密安全问题会少很多。许多加密实现都有改进的 API,并在很大程度上由Dan Bernstein’s NaCl驱动。此外,StackOverflow不再将选中接受的答案作为置顶的答案,这使人们有机会投票选出比最初接受的答案更好的答案。
现在让我们步入正题。
示例 1:JAVA 中的 AES-128 CBC 模式
链接在此。在撰写本文时,这个答案有 248 个赞成票:它既是最受欢迎的答案,也是被选中的答案。要理解它为什么是错误的,请查看 key 和 initVector 的类型:它们是字符串。
密钥(Keys)和初始化向量(IVs)应该是字节数组,而不是字符串。
这也是我经常会遇到的问题。如果你使用一个字符串,那么你就有了一个密码。密码不是加密的密钥,但它们可以使用一个基于密码的密钥派生函数来转换成加密密钥。这里的做法正是应该被避免的做法:将密码字符串(被错误地标记为一个“密钥”)复制到 SecretKeySpec 结构中。
初始化向量(IV)是字符串的错误是一个不那么常见的错误,但它仍然是错误的。在我解释修复方法之前,让我们先看看调用函数:
硬编码的密码(错误地标记为一个“密钥(key)”)和硬编码的初始化向量(IV)。
这里的问题是:
硬编码的密码(错误地标记为一个密钥)。
硬编码的初始化向量(IV)是字符串类型。
我知道有些人会争辩说,这只是一个概念证据,任何理智的人都知道如何设置正确的密码和初始化向量。我的回答是,这些人显然不以审查代码为生,因为我在著名的地方看到过这样的代码,你绝对不会想到这些地方也会犯这样的错误。这是一个真正的问题,并且非常普遍。
为了说明密钥和密码之间的区别:128 位密钥应该有 128 位的熵,因此破解它需要次尝试。这里的解决方案是从大写字母、小写字母和数字集中选择了密码。如果他们从这组集合中选择了一个完全随机的密码,那么这有种可能性,这是量级。这意味着破解的次数最多为次,但实际上情况更糟,因为人们不会随机选择密码。我经常看到用于加密的密码是非常可预测的,后面跟着类似“123!”的字符。令人遗憾的是,人们已经习惯于相信,遵循不推荐的密码安全指南(“密码组合规则”)会使密码变得安全。事实并非如此,不要这样做。
很重要的一点是:不要使用同一个初始化向量 IV 两次——这会破坏安全属性。这在我的博客《开发者十大加密错误(Top 10 Developer Crypto Mistakes)》中有解释。关于这件事,我争论过很多次了。特别是当有人回答说“没关系,我们会把 IV 放到保险库”时,我的沮丧程度如火箭般冲天而起。不,这并不好:IV 并不是设计为保密的,即使它们被隐藏,也不能修复安全问题。停止使用你自己的加密程序!不幸的是,这么多人不明白他们正在推出他们自己的加密程序。
这种代码还有另一个问题:它使用未经验证的加密程序。在我的《开发者十大加密错误(Top 10 Developer Crypto Mistakes)》博客中,我列举的第 7 条就是“假设加密提供了消息的完整性”。我现在接受NaCl设计中使用的哲学建议:开发者不需要知道两者的区别,而应该始终使用经过验证的加密程序,这隐藏了这两个基础。在这种情况下,GCM 模式下的 AES 解决了这个问题,CBC 模式则没有。
用户Patrick Favre在同一个帖子中提供了一个更好的答案。你可以自己看一下,如果你像我一样觉得这是一个好答案,那么就给它投票吧。
示例 2:C#中的 AES-256 CBC 模式
代码在此。在撰写本文时,这是第二受欢迎的答案,拥有 117 个投票。它不是被选中的答案,但是仍然很受欢迎。
代码中的第一个问题是很明显的,其它问题可能不太明显。截图如下:
一个硬编码的密码(错误地标记为密钥),但 IV 有什么问题呢?请往下看。
是的,另一个硬编码密码的例子,它被错误地标记为一个密钥。但好消息是,他们使用了基于密码的密钥派生函数(Rfc2898DeriveBytes,这是使用 HMAC_SHA1 的 PBKDF2),来将其转换为真正的密钥!这很好,但是因为他们没有指定迭代次数(你需要在 StackOverflow 页面上向右滚动才能看到),使用的默认值 1000 按照现代标准非常低(即不是很安全)。
现在,让我们向右滚动,我们看到他们为 Rfc2898DeriveBytes 输入的加盐内容:
某人的名字被用作硬编码的加盐(salt)。
硬编码的加盐!加盐不需要保密,但在许多情况下,你不应该使用同一个加盐两次。无论如何,最好不要把加盐硬编码。
真实故事:我在一个客户端的代码审查中看到了这个片段。我记得,看着加盐,我想“这对我来说看起来非常有趣”。因此我用 C 写了一个程序,将它打印成字符串,结果是“Ivan Medvedev.”。然后我用谷歌搜索了 IV,在 StackOverflow 上找到了这个代码片段!我只是想知道这个可怜的 Ivan Medvedev 是谁,他的名字永远刻在不安全的密码里!
那么让我们进入最后一个问题:IV 有什么问题?如果加盐和密钥是常量输入,那么你总是得到相同的 IV 输出。因此,你会多次使用相同的 IV,这破坏了 CBC 模式下任何区块加密的安全性。
如果作者只是简单地复制Microsoft Rfc2898DeriveBytes示例,那么他们将更接近一个好的答案。但是 CBC 仍然存在未经验证的加密问题,微软的例子迭代次数也太少。
好一点的消息是,StackOverflow 上被选中的答案更好,且拥有更多的投票。但是它也存在问题:Rfc2898DeriveBytes( )的迭代次数太小,并且它们使用未经验证的加密(CBC 模式)。
什么是好的迭代次数不是一成不变的。NIST说“迭代次数应与验证服务器性能允许的次数相同,通常至少为 10000 次迭代。”OWASP说至少为 720000 次迭代。 这两者之间有很大的差距:你可能想看看Thomas Pornin怎么说。无论如何,以现代标准衡量,任何低于 10000 的数字都绝对太低了。
示例 3:JAVA 中的 TRIPLE DES
代码在此。这个问题是在 2008 年提出的,所以你可以原谅其中的一些问题。此时,NIST正在推荐AES,但仍在某些情况下允许Triple DES。现在,triple DES 完全不被推荐,因为块太小了,但有时我仍然在代码中看到它。
让我们仔细看看在撰写本文时获得 68 个赞成票的最佳答案:
永远不要使用 MD5,尤其是这里使用的方式。此外,硬编码密码和 zero IV 也是问题。
同样,这是一个硬编码的密码,但这个密码是使用 MD5 转换成密钥的,MD5自1996年以来就被弃用了。然而,另一个问题是,MD5 不是一个基于密码的密钥派生函数,因此它不应该在这里被这样使用。PBKDF2自2000年以来就成为标准,本来应该使用它。
这段代码使用了一个 zero IV,这是我在许多代码库中看到的一个非常常见的问题。它也是未经验证的,但这可能是情有可原的,因为未经验证的加密概念在当时并不广为人知。
请不要使用这样的代码。
示例 4:JAVA 中的 AES
代码在此。在撰写本文时,这个答案有 15 个赞成票。
你可能注意到,他没有指定 IV。这可能是因为他没有指定操作模式,对于大多数加密提供者来说,这意味着使用默认的 ECB 操作模式。你永远不应该使用 ECB:还记得加密的企鹅图片吗?
在这个帖子中,一个更好的答案有 11 个赞成票。他选择了 CBC 模式和恰当的 IV。这不是经过验证的加密,但这是一个相当合理的答案。
示例 5:请不要这样做!
答案在此。我不打算截图:这张太糟糕了。
问题是如何在 Java 中加密一个字符串。在撰写本文时,被选中的答案有 23 个赞成票,它试图实现 one-time-pad。在答案的顶部贴了一条警告,建议永远不要使用它。这是 100%正确的:one-time-pads 的问题是,实际上它们往往是 n-time-pads,其中 n > 1。使用 pad 多次会很容易破解加密。
这是一个很好的例子,StackOverflow 使得票最高的问题置顶的改变取得成效。在撰写本文时,目前得票最多的问题有 187 票。这个答案很好地讲解了如何正确进行加密,绝对值得一读。我唯一要指出的是 SecureRandom()混淆,作者知道这是不对的。这里的风险非常小,但使用SecureRandom()的正确方法非常简单。
我是在故意挑错吗?
你可能会说我在找的是老代码,但老实说,这些代码片段很好地代表了我在日常工作中经常看到的问题。我很少看到加密做得正确的。
你可以通过通过投票给更好的答案来改进加密现状。这比否定上面强调的坏示例更可取,让我们以友好的方式而不是卑鄙的方式解决这个问题。StackOverflow 已经做出更改,来允许我们这样做。
为什么会有这么多糟糕的加密实现?
为什么会有这么多糟糕的加密例子,可以归为历史原因。历史上,加密社区和开发者社区之间存在很大的脱节。当免费提供的加密库从 1990 年代开始可用时,API 假定开发者知道如何安全地使用它们。这当然是一个错误的假设,再加上使用的复杂性,开发人员花费了大量的精力来实现这些功能。一旦他们有了可用的代码,他们非常慷慨地与其他人分享——而没有意识到他们是在一个雷区里。
加密的实现的未来看起来很好,但实现的第一步是提高对好答案的认识,并非常清楚哪些答案是无效的。这篇博文是我尝试成为帮助做出这种改变的一个小声音的几个方面之一。
附言
我想感谢 reddit 用户 cym13对这篇博文的宝贵反馈,这有助于改进最终的结果。我还要感谢cryptohack.org的作者指出本文先前版本中的一个错误:对于示例 3,我曾说当时还没有经过验证的加密。我错了。所以更新了博客,说它在当时并不广为人知。我还要感谢 Ivan Medvedev 对本博客的回复。
StackOverflow 上有许多加密义务警员,他们经常就安全问题以及如何解决这些问题发表评论,如Maarten Bodewes和ArtjomB。他们是这方面的专家。
在发布本博客的第一天,就取得了一个预期的结果:在上面的示例 4 中,较好的答案被提升到足以超过较差的答案。感谢帮助实现这一目标的社区读者。在示例 1 的更好的答案也有大量的赞成票,但要超过另一个答案还有很长的路要走。无论如何,我们已经取得了进步!
原文链接
IF YOU COPIED ANY OF THESE POPULAR STACKOVERFLOW ENCRYPTION CODE SNIPPETS, THEN YOU CODED IT WRONG
评论