167-实用密码学

开篇词 | 人人都要会点密码学

你好，我是范学雷。

2020 年，庚子年。如果你问我，2020 年有什么愿望，我想说，这一年，咱们能不能重启？如果一切真的可以重启，哪些事情是我们可以做得更好的？哪些问题是我们依然无法避免的？

如果可以重来，我想，Zoom 最想做的，一定是聘用密码学领域的专家，提高研发人员的密码学见识，让研发人员都学会和用好密码学。

Zoom 怎么了？

Zoom 怎么了？为什么 Zoom 要和密码学较劲？

熟悉在线会议系统的都应该知道，Zoom 是这个领域的市场领先产品。2020 年初，随着 COVID-19（新冠肺炎）在全球的蔓延，高质量的在线会议变成了一个居家办公、远程工作的必需品。

这时候的 Zoom，也做好了所有的准备，它很好地满足了人们远程工作的急切需求。直到 2020 年 3 月底，Zoom 都是主流市场的主要选择之一。

然而，到 3 月底，事情急转直下。2020 年 3 月 31 日，研究者公开了 Zoom 的重大安全漏洞，认为 Zoom 并不适用于办公会议。最严重的问题，就是和密码算法与密码管理相关的问题。

比如说，Zoom 使用了 ECB 加密模式，而这种加密模式并不是一种安全的加密模式。而且，这件事也不是秘密，ECB 加密模式的安全问题至少披露了二三十年了。

随后，有不少知名的公司、教育机构以及政府组织，出于信息安全的考虑，禁止使用 Zoom，转而寻找替代产品。本该大展身手、攻城略地的时候，Zoom 被闷头打了一棒，把大量的机会拱手让给了竞争对手。而这种市场机会，我认为前五十年未见，后五十年难寻。

不过，Zoom 马上聘用了安全领域的专家，全力以赴地解决掉了这些问题。幸运的是，对 Zoom 来说，这些问题发现得并不算晚，应对得也不算拖拉。不幸的是，由于算法选择失当这样的“小问题”，Zoom 给了竞争者充分的空间，自身的品牌和信誉也受到了很大的伤害。

Zoom 是个特例吗？

从安全漏洞的角度来看，单个问题本身并不可怕。重要的是我们该怎么快速反应，该怎么杜绝后患。杜绝后患的思路之一，就是要求同样的安全问题，一定不要重复上演。

我相信，Zoom 可以做到这一点。但是，其他的公司呢？其他的产品呢？很遗憾，我不相信 Zoom 会是最后一个用错了 ECB 加密模式的产品或者公司。现在和未来，依然会有人用错 ECB 加密模式，以及其他的密码算法。

今天，用错密码算法的产品和公司，能够快速反应并及时修复它们的安全问题吗？明天的产品和服务，能够做到在合适的场景下，使用合适的密码算法吗？对于答案，我依然有些悲观。

我之所以悲观，是因为大部分产品的研发人员，甚至都不知道 ECB 加密模式有什么问题，当然在使用它的时候也就很无畏。

如今，能够做到在不同的场景，使用不同的密码算法的研发人员，数量还很少；懂得密码学的安全领域的专家，数量更少；能够跟得上密码学发展进程的研发人员，数量更是少得可怜可叹。

为什么要学密码学？

为什么会这样？其实，这里面既有历史原因，也有学科原因。

十多年以前，操作系统和通信协议这样的基础设施，通过内嵌的安全机制，就可以满足大部分的点对点的信息安全需求了。所以，那时候的信息安全直接需求并没有那么大。

再加上现代密码学门槛高、出道晚，它的这种特殊性让其一直都是少数人的游戏。即使是数字化时代，隐私保护和信息安全已经成为重要议题的时候，密码学也没有成为每个程序员的必修课。

但是，十年以前，这或许不是问题。可是，从今以后，这会是一个越来越严重的技术债。

因为，现代信息安全的需求，越来越多地跳出了基础设施的范畴，进入了应用程序层面。

操作系统、通信协议这样的基础设施，早就已经满足不了应用的多样化需求了。比如，像 Zoom 这样的在线会议产品的安全需求，底层的基础设施提供的安全保障能力，是远远不够的。

不过，好在机遇与挑战总是并存的。近年来我参加的每一次密码学会议，都会有人提到密码学专业人才的短缺。的确是这样的，市场的需求和供给之间有一个巨大缺口。

总的来说，现在密码学的市场情况主要有以下两个特点：

密码学领域难以招聘，即使平均报酬已经高出了一大截；不过这也使得与密码学相关的工作超级稳定，很少会看到一个信息安全工程师有 35 岁的忧愁，抑或 45 岁的哀伤。

密码学应用很尴尬，由于研发团队缺乏专业的密码学素养，算法场景错配或使用有安全漏洞的算法和协议的问题层出不穷。遗憾的是，这个缺口还没有停止扩张的迹象。

密码学虽是一个小门类，但是涉及内容庞杂。从踏进门到能够使用密码学技术去设计一个像样的、安全的系统，需要短则两三年、长则七八年的积累。所以，无论是领取报酬的工程师或者是支付薪酬的公司，有耐心的实在是少数。

当然，好的耐心是有回报的。不同于其他的软件工程师，密码学是一个需要深度积累的领域，年头越长，见识越多，越了解其中的坑坑洼洼，生产效率和产品质量也越高。可以这么说，有经验的信息安全工程师，是每一个公司的关键人力资源。

45 岁，好日子才刚刚开始。

如何学习密码学？

既然密码学这么难，如果我们想学习密码学的知识，该从哪里开始呢？有没有办法降低门槛？

学习的途径，不外乎两个：读书和培训。

密码学最好的书籍，当属Bruce Schneier 的《应用密码学：协议、算法与 C 源程序》。这应该没有太多争议。很多同学的密码学入门，就是从这本书开始的。二十多年前，我也是从这本书开始的。即使是现在，它也是我能在市场上找到的、最好的密码学入门书籍。

不过，和其他密码学书籍一样，这本书虽然叫做应用密码学，对于普通的软件工程师，它依然太难了。除此之外，这本书的内容，是 1996 年之前的密码学世界，和现在的密码学相差有点远。

其中提到的算法，很少还能够继续使用了；书中的源代码，无论是理论上还是实践上，都有很多的安全漏洞，也已经不能用在现在的产品中了。所以，如果你发现，一个产品还在使用这本书的算法和源代码，它的安全性是值得你担心的。

这就需要我们重新审视和评价现在的密码算法：哪些还能用？哪些要淘汰？不同的场景，应该使用什么样的算法？使用算法的时候，有哪些常见的陷阱？作为软件开发者，我们应该掌握哪些密码技术？掌握到什么程度？这些问题，都是我们要重新梳理，重新认识的。

一般来说，非密码学专业领域的研发人员，是不需要了解密码算法的数学细节和实现细节的。那么，我们应该了解哪些密码学知识呢？我认为主要有以下三点：

密码学可以解决什么样的问题？

也就是说，我们需要了解密码学的基本概念和体系，知道密码学能解决什么样的问题，不能解决什么样的问题。这就算是敲了敲密码学的大门了。

面对具体的问题，我们应该使用什么样的密码技术？

也就是说，如果遇到具体的问题，我们应该选择什么样的算法，当心什么样的问题。这需要我们了解每一个算法的适用场景，以及它的局限性。这算是跨进大门，打怪晋级，修炼十八般武艺。

面对真实的产品，应该怎样组合不同的密码技术？

一般来说，一个产品里需要组合多种密码技术，才能够实现信息安全。单独耐看的技术，搭配起来可能就是一团糟。这需要我们组合、搭配好不同的密码技术，甚至包括非密码的信息安全技术。也就是把密码技术用起来，解决真实的问题。

简单地说，就是要学会、用好密码学。

这个专栏教你什么？

我会带你厘清密码学及其算法背后的基本概念和基本逻辑。只有基本概念和基本逻辑弄清楚了，我们才会降低用错密码学技术的风险。这个专栏也是围绕基本概念和基本逻辑安排的。

不过，你不用担心基础知识枯燥难学。不同于一般的密码学课程和教材，我会把重点放在各种密码技术的适用场景和局限性上。因为，单纯地学习密码学的知识，其实没有特别大的用处，弄清楚基本概念和基本逻辑最好的办法，就是了解支持它们的实际问题和现实需求。

所以，这个专栏的每一篇文章，都是从问题开始的，也是以问题结束的。可以说，实际问题和现实需求是领着我们一步一步深入密码学内部的线索。

你也知道，用错密码学的后果很严重。所以，教你用好密码学，就是这个专栏的目标。我想通过这个专栏帮助你把握住基本概念、弄明白适用场景、躲得开常见错误。

不过，要学好这个专栏，还需要你做好两件事。

第一件事情是关注问题。当你阅读的时候，如果遇到提出问题的文字，停下来思考一小会儿，看看自己有没有什么想法，或者更多的问题。专栏里，我们会提出很多问题，每一个问题，都会引出更多的内容，甚至更多的问题。

你只有认真地思考了每一个问题，才能掌握这个问题，才能对后续的解决方法有更深入的了解。在密码学的世界里，了解问题总是比掌握解决方法还要重要。

第二件事情是提出问题。除了专栏里提出的问题之外，你会不会发现新问题，有没有新思路？密码学算法最神奇的地方之一，就是外行看觉得好完美，内行看觉得好无奈。

因为，密码学的每一个基本概念，都有它的缺陷；每一个算法，都有要命的缺点。专栏里，我试着向你去提出最关键的问题。但是毫无疑问，我不能覆盖所有的问题。

但我相信专栏里提问题的角度能给你一些启发，帮助你从不同的角度，掂过来倒过去地琢磨每一个基本概念和算法，看看你有没有新问题，有没有新思路。你能够找到的无奈的地方越多，你对问题的了解就越深入，学到的东西也就越多。

说实话，这个专栏有点硬，不是一个讲故事、听故事的专栏。我尽了自己最大的努力使它平实易懂，但是它依然是一个硬得硌人的专栏。这个硬度的主要来源，不是因为难懂，是因为我们要不断地思考各种各样的问题，而不是我塞给你一堆知识。

另外，本专栏会主讲密码学的基础知识和逻辑，对于密码学中较难的部分：非对称密码技术，我们课程暂时不会过多涉及。你可以先打好基础，再去拓展进阶的知识，这对于你来说，才是一条更好的学习路径。

我是谁？

最后介绍一下我自己吧。

我是范学雷，在密码学应用领域已经工作了二十多年了。目前，担任 Oracle 的首席软件工程师，是 Java 安全组的成员，OpenJDK 安全评审成员，也是 Java 安全的主要推动者和贡献者之一。

我的日常工作包括关注信息安全威胁与技术进展，参与信息安全领域国际合作，制定与实现 Java 安全规范，提升 Java SE 生态安全水准，促进 Java 技术的普及与运用等。

我曾在极客时间做过第一季专栏《代码精进之路》，在那个专栏里，我总结了自己 20 多年的编程心得和代码评审经验。而这一次，我希望可以带你走一小段密码学的旅程。

如果你还没有密码学的基础，那我们就可以从这里开始；如果你已经学习了《应用密码学》，或者有密码学应用的经验，那我们可以一起去看看密码学最新的进展，以及最新的密码学算法。

欢迎在留言区留言，记录、讨论你的想法，或者写下你的期待，让我们一起努力！

好，我们开始吧！

01 | 学习密码学有什么用？

你好，我是范学雷。

从今天开始，我就和你一块儿去试着揭一揭密码学的面纱，看看里面的机关门道。

每当我们看到一个新鲜事物时，“这东西是什么，有什么用”这样的问题总是会最先浮现在我们的脑海里，诱惑着我们更进一步地去了解它。

那么，密码学是什么呢？它有什么用呢？这就是我们首先要讨论的事情。为了使问题更加直观，我们先从一个例子开始。

约个会能有多难？

我们先来做一个有趣的假设，假如马上要到七夕节了，河东的牛郎想要给河西的织女发一条信息，七夕相约鹊桥会。信息的内容是这样的：

织女：

七月初七晚七点，鹊桥相会。不见不散。

牛郎

可是，这封信该怎么送出去呢？银河又长又宽，两个人当然不能面对面地口头表达。这封信要想送给织女，只能通过中间媒介。传说中，有人认为这个中间媒介是喜鹊，也有人认为这个中间媒介是流星。反正不管怎么说，都没有证据表明，牛郎可以亲手把信交到织女手里。

不过，这次约会能不能成行，很大程度上就取决于这封信的送达能不能成功。特别是，银河这么长、这么宽，王母娘娘这么神通广大，这封信就更不好送了。

那么，牛郎和织女需要考虑哪些问题呢？

问题一：怎么证明双方的身份？

第一个问题，牛郎一定要确认，收信人是织女，而不是丈母娘王母娘娘。只有织女收到邀约，约会才有可能成功。换个正经的说法，就是牛郎要确认收信人的身份。

从织女的角度看，她也要确认发信人是牛郎，而不是喜欢恶作剧的少年郎，或者是其他的姊妹淘。也就是说，织女要确认发信人的身份。

可是，如果不能面对面，牛郎怎么知道收信的一定是织女；织女又怎么知道发信的一定是牛郎？

问题二：怎么能使消息不泄露？

第二个问题，牛郎一定要确认，这封信的内容王母娘娘看不到。约会的时间、地点一旦外泄，约会恐怕就会节外生枝。或者换个说法，只有牛郎和织女才能够看得到这封信的内容，其他人都不能阅读这封信。

问题是，如果织女能够看得到这封信，为什么王母娘娘就一定看不到？无论中间媒介是喜鹊还是流星，王母娘娘可是有遮天的权势，她难道就没有办法看一眼信件的内容吗？

问题三：怎么防止内容被篡改？

更坏的情况是，如果消息不仅泄露了，内容还被篡改了呢？这是牛郎织女要考虑的第三个问题。牛郎怎么才能确保，织女收到的信的内容是七夕鹊桥会，而不是分手宣言；约定的时间是晚七点，而不是早八点；约会的对象是织女，而不是嫦娥？

而织女也一定要确认，收到的信的确是牛郎写的，而且一字不多、一字不拉和一字不差。牛郎怎么做，才能确保信件的内容不被篡改呢？

问题四：怎么确保信件能收到？

事情纵有千般好，可如果织女不能及时收到信，牛郎是会被放鸽子的。这封信能不能在七夕前送到？这是牛郎织女要考虑的第四个问题。

如果是喜鹊传递信件，这个可怜的小东西会不会中途被请去喝茶吃酒，醉醺醺地忘了托付？如果是流星传递信件，织女会不会在七夕前看看流星雨，解读哪一颗流星携带了牛郎的信息？万一夜空忽然像开了万花筒，红的蓝的紫的烟花可劲地绽放，织女只有一双眼睛，可如何是好？

所以，怎么才能保证牛郎发送的信息可以被及时地送达、拆阅？

问题五：怎么防止翻脸不认账？

最后一个问题就是，牛郎一定要确认，织女收到了信，不能抵赖说没收到；织女也一定要确认，牛郎发出了约会邀请，不能抵赖说没发出邀请。

做过的事，说过的话，要有防翻脸、防撕扯机制。有什么办法防止翻脸不认账呢？

所以，你看，我们和附近的朋友打个招呼很容易：走到面前，说句你好；如果有别人，小心翼翼地问，七月初七有空没有？这信息就算是送到了。

可是，一旦我们拉开了距离，打个招呼、送一封信就变成了一件有点复杂的事情。

约会难题有现实意义吗？

在无线电技术发明之前，大部分的信息传递都是在人与人之间进行的，尤其是在熟人之间。人与人之间的信任是解决信息安全传递问题的最关键因素，信息传递的速度和半径也有很大局限。

比如说，在历史战争剧中，我们经常可以看到需要一名勇冠三军的猛将突围搬救兵的情节。作为敌方，只要能劫杀到突围的猛将，信息传递的路径就算是掐死了。

好在无线电技术发明之后，信息传递的办法就彻底改变了。即便是赵云再世，现在也没有七进七出的战争场景了。总体来说，现在的信息传递变得更迅速、更经济、几乎是无处不在了，家书值万金的场景也是一去不复返了。

无论你身在何处，只要你有一部智能手机，你就可以联系到家人和朋友。这时候你会问了，那现在都有手机了，发个微信不就全解决了吗？哪里还有什么信息传递问题？

是的，作为一款即时通信的工具，微信已经帮助我们解决掉很多问题了。那我们上面讨论的牛郎织女约会面临的问题还有现实的意义吗？有的。

因为，牛郎织女送信的障碍不独独横亘在他们之间，它们是信息传递的普遍问题。即使是今天看起来用户得到简单、便捷、便宜、即时的通信背后，依靠的也是复杂的现代信息技术。

现在很大一部分信息流，都是在人与机器之间进行的。无论是淘宝购物，还是银行转账，甚至发送微信，其实都是你在和机器交流。机器同意后，你的信息才有可能传递给另一个机器，或者你期望交流的具体的一个人。

既然，信息安全的问题一直和人类历史相生相伴，那我们该如何解决呢？

如果我们能够把具体的问题抽象出来，简化成易于理解的、方便表达的模型，你就能够知晓这些问题的普遍性，并且能够把它扩展到千变万化的场景里去。

那么，对于上述的这些问题，我们换个角度，换个说法，把它们表述成需求，并且放到信息系统这个虚拟的代码世界里看看，就可以找到这个具有普遍意义的模型了。

需求一：识别身份，确定牛郎就是牛郎

织女收到信，一看落款是牛郎，她就知道这个牛郎应该是孩子他爹，是董永，而不是王小二。你想一想，如果是一台机器，它该怎么解释“牛郎”这两个字。“牛郎”代表的是一个人，还是一群人？是一只蚊子，还是一盏路灯？没有更多的信息，这个机器是没有办法做进一步判断的。

这就是我们要考虑的第一个需求，就是怎样表述身份，使得这个身份可以被准确标识。这也就是所谓的身份识别（Identification），表述的是身份如何呈现和表述，说的是身份的记号。

扩展开来，这里的身份可以是信息的参与者，比如微信账户，也可以是资产的持有者，比如银行账户，网站域名。

需求二：认证身份，验证牛郎就是牛郎

织女收到信，看到落款是牛郎，再细看笔迹，她可能就有八分把握断定这信是孩子他爹写的。之所以还有两分疑问，那是因为对于神通广大的王母娘娘，伪造笔迹算不上是了不起的事情。

机器该怎么办？网络传递的都是二进制代码，你的键盘输入的牛郎和我的键盘输入的牛郎，并没有笔迹的差异。另外，如果只有八分把握，你敢不敢使用手机银行转账？

这就是我们要考虑的第二个需求，就是怎样认证身份，验证信里面所宣称的牛郎就是写信的那个牛郎。这也就是所谓的身份认证（Authentication），是识别和验证身份的过程，也就是验明正身的意思。

需求三：管理特权，授予织女看信权利

这封信为什么会送到织女的手里，而不是王母娘娘的手里？很显然，这封信的收信人是织女，所以喜鹊会把信投递给织女而不是王母娘娘。其实，这也就意味着，织女独享收信、看信的权利，而王母娘娘没有。

机器会怎么办呢？理论上，互联网的每个信息转发节点都有可能截获这封信，看到信的内容，这就破坏了看信者的独享权利。那我们怎么才能把权利授予织女，而排除其他人看信的权利？

这就是我们要考虑的第三个需求，就是怎么管理权限，使得特定的身份有特定的权限。这也就是所谓的授权（Authorization），是指定对资源的访问权限，或者使用特权，说的是权利。

把上面三个需求放在一起，我们就解决了权限管理的基本问题：该怎么标识身份？该怎么验证身份？以及一个身份拥有什么样的权利？

需求四：信息保密，没有权限不能看信

牛郎织女约会的第二个问题是，怎么能使消息不泄露？换个说法，就是怎么保密信息。所谓的信息保密，也就是数据的“机密性”（Confidentiality），指的是数据未经授权，不得访问。授权的对象既包括个人、实体，也包括处理过程。

如果没有授权王母娘娘拆阅信件，即使王母娘娘劫杀了送信的喜鹊，她也不能看到信件的内容。这就是信息保密的需求。

需求五：信息完整，保护内容不被篡改

牛郎织女约会的第三个问题是，怎么防止内容被篡改？换个说法，就是怎么保持信息的完整性。所谓的“完整性”（Integrity），指的是数据未经授权，不得更改。信息的完整性，也意味着信息的准确性，信息不能残缺不全，或者来源不明、去向不明。

如果我们能做到保护信息不被篡改，即使王母娘娘神通广大，可以请喜鹊去喝茶吃酒，她也没有办法改动信件的哪怕一个字、一个标点符号。这就是信息完整的需求。

需求六：信息可用，保持信息获取能力

牛郎织女约会的第四个问题是，怎么确保信件能收到？换个说法，就是怎么保持信息的可用性。所谓的“可用性”（Availability），指的是数据经过授权，可以访问。也就是说，需要的时候，经过授权，可以获取信息，能够获取信息。

如果喜鹊被请去喝茶吃酒，醉醺醺地忘了托付，牛郎的信送不到，织女的信收不到，这就破坏了信息存在的意义。该发送的信息能够及时发送出去，该收到的信息能够及时收到，这就是信息可用的需求。

我们把上面三个需求放在一起，就解决了信息安全的基本问题：信息能够完整地、秘密地传递出去和接收进来。

在这六个需求里面，前三个需求，说的是授权的过程，后三个需求，说的是安全的要素。

哪一个需求最重要？

不知道你有没有疑问，这六个需求里，哪一个需求最重要，该优先考虑呀？这是一个好问题。

信息安全这件事，具有典型的木桶效应，安全不安全是由最短的一块木板决定的。什么意思呢？就是每一个要素、每一个需求实现都不能有明显的弱点。任何一个要素，如果我们没有清清楚楚地落到实处，都会带来一把鼻子一把泪的。

这六个需求就像是六个维度，就像一间屋子的长宽高（三维更便于理解）。如果很长、很宽，但是高度为零，它就没有体积。没有人能住高度为零的屋子，或者高度 10 厘米的屋子。

同样，也没有人能住长度为零或者宽度为零的屋子。一个舒适的屋子，长度、宽度、高度都要合适。这就好比，牛郎和织女约会的例子中，任何一个问题真实发生了，这场约会就会走向失败。

所以，有没有办法可以满足所有的需求呢？这时候，密码学就登场了。

密码学有办法吗？

如果放在 50 年前，这些需求理论上是没有办法完全解决的。但是现在，密码学有好几套体系办法，可以漂亮地解决掉这些问题，满足这些需求。

我们知道，信息安全是一个庞大复杂、涵盖广泛的学科，而密码学只是信息安全技术的一个小门类。也有人说密码学是信息安全的核心技术，我并不反对这样的说法。

虽然密码学只是一个小门类，但是如果你钻进了密码学领域，你就会发现，密码学也是一个大森林，枝枝蔓蔓的有很多技术、规范、协议和体系。

密码学最基础的分支有三个，第一个是单向散列函数，第二个是对称密码技术，第三个是非对称密码技术。这三项基础技术的组合运用，诞生出了丰富的安全协议和体系，比如说数字证书、安全传输、区块链、数字货币等。

回到我们上面的约会问题，如果粗陋地来看，对称密码技术可以通过加密、解密，解决“机密性”的问题；单向散列函数，可以解决“完整性”问题；非对称密码技术可以解决授权和认证的问题；通过我们对这三项基础技术的综合运用，就可以提高系统的“可用性”。

那这些密码学技术都是什么呀？是怎么解决这些问题的呀？这就是我们整个专栏要关注的重点，后面我们会接着拆解这些密码学概念和技术，开始进入密码学的细节。你准备好了吗？

Take Away（今日收获）

英文中，有一个词汇叫“Take Away”，指的是一个活动结束后，比如会议或者讨论，大家要记住的关键事实、观点或者想法。用在每篇文章的结尾，我觉得是一个很贴切的表述。但是，我没有找到合适的中文翻译，我们先使用英文词汇。谁有好的翻译，可以给我留言。

今天，通过牛郎织女这个小例子，我们探讨了牛郎织女约会信息传递的五个障碍：双方身份难以证明、消息会泄露、内容会被篡改、信件无法及时送到以及存在双方翻脸不认账的风险。

虽然在现代社会，传递一个消息已经不是一件困难的事，但这五个障碍仍是信息安全里的重要议题，我们也由此总结了信息安全的六个基本需求：

身份识别、身份认证和授权。这三个需求解决了权限管理的基本问题：该怎么标识身份？该怎么验证身份？以及一个身份拥有什么样的权利？

信息的机密性、完整性、可用性。这三个需求解决了信息安全的基本问题：信息怎么能够完整地、秘密地传递出去、接收进来？

最后，通过今天这一讲，我希望你：

理解信息安全的基本问题和基本需求；

建立起来均衡考量信息安全基本需求的意识；

明确学会用好密码学的意义。

思考题

假设牛郎织女面前都有一部电脑，电脑都是连接着互联网的。你能不能给牛郎织女一些建议，该怎么做才能摆脱上述的种种问题，把约会信息传递成功？你的建议有没有其他的缺陷？会不会带来新的问题？

这个问题现在对你来说可能有点难。不过，我就是想让你打开脑洞，尽情地发挥自己的想象力，看看有没有办法，有没有问题。专栏快结束的时候，我还会再一次问这个问题。到时候，你可以比较一下，你的想法有没有变化。

欢迎在留言区留言，记录、讨论你的想法。你的每一次发言都是思维碰撞出的火花。

也欢迎把这一讲分享给你的朋友。好的，今天就这样，我们下次再聊。

02｜单向散列函数：如何保证信息完整性？

你好，我是范学雷。

从今天开始，我就要和你一起逐渐接触密码学的具体细节了。在这个过程中，我会讲到很多密码学相关的概念和诉求，这是我们打好基础的关键。不过，你也不用紧张，我会和你一起分析，一步步带你掌握这些知识点。

还记得上一讲，我们讨论的话题吗？我们通过牛郎织女约会送信的小例子，探讨了“密码学有什么用”这个问题，从而理解了信息安全的基本问题和基本需求。

问题出现了，我们也知道了该用密码学。现在就要来解决问题了，接下来的几讲，我们就先来解决“信息的完整性”这个问题。你还记得解决完整性的工具是什么吗？——单向散列函数。

可是，什么是单向散列函数？它是怎么解决完整性问题的？今天，我们就来讨论这两个问题。

什么是单向散列函数？

首先，我们从名字上看，一眼就能看出来单向散列函数有两个关键修饰词，“单向”和“散列”。

其实，在数学上，单向函数和散列函数是两个不同类型的函数。所以，我们要想理解单向散列函数，我们就要先知道什么是单向函数，什么又是散列函数。

什么是单向函数？

如果你没有了解过什么是单向函数，你可以先猜一下，为什么它叫单向函数？

单向函数（One-way Function）是正向计算容易，逆向运算困难的函数。也就是说，给定你一个输入，你很容易计算出输出；但是给定你一个输出，你却很难计算出输入是什么。

还有这样的函数？是不是感觉有点神奇？

《应用密码学》有一个很生动的例子来解释单向函数。把盘子打碎是一件很简单的事情，但是把这些碎片再拼接成一个完整的盘子，就是一件非常困难的事情。

也许，你会想，虽然把盘子碎片再拼接起来非常困难，但是仅仅就是非常困难而已，无论是手工还是计算机辅助，碎盘子还是可以拼接起来的。是的，这就是这个例子巧妙的地方。

单向函数就是这样的一个盘子。虽然我们强调，单向函数只能正向计算，不能逆向运算。但其实，这只是一个美好的愿望。为什么我这么说？

因为，我们能找到的、谈到的所谓的单向函数，都是正向计算容易，逆运算困难的函数。是的，我用的词语是“困难”，而不是“不能”，可能性只是很小，但不是没有。

在我们的日常生活里，泼出去的水再也收不回，说过的话、做过的事也没地方买后悔药，单向似乎才是生活的常态。但在数学领域，有很多函数看起来像是严格的单向函数，我们既证明不了它是单向函数，也暂时找不到逆向运算的办法。到底有没有逆向运算的办法，我们现在还不知道。

为什么我要和你强调“逆向运算困难”这件事？因为密码理论领域里很多棘手的问题，密码应用领域里的很多错误，都是来源于单向函数的这种不确定性。

比方说吧，每一个被破解的单向散列函数的密码学算法，在它被发明的时候，人们都没有找到逆向运算的办法，可是被破解的时候，人们就发现原来还是有办法去逆向运算的。

今天还是安全的算法，明天就可能被破解。这虽然使得密码学充满了挑战，但同时也使密码学充满了乐趣。

不过，需要注意的是，我们要对这种不确定性保持足够的警惕，采取足够的防范措施。比如说，一个应用程序，至少要支持两种单向函数，当一种出现问题时，另外一种可以替补。

现在你知道了，单向函数是一个正向计算容易，逆向运算困难的函数。那我要是问你，对于我们来说，什么样的单向函数会更实用呢？我想，你应该可以回答出来：

一个更实用的单向函数，正向计算会更容易，容易程度就是这个函数的计算性能；

一个更实用的单向函数，逆向运算会更困难，困难程度就是这个函数的破解强度。

同样，我还是要强调一下，一个实用的单向函数，计算强度和破解强度要均衡考量，不可偏废。

以后我们谈到单向函数，指的都是正向计算容易，逆向运算困难的函数，除非特别声明。

什么是散列函数？

讲完了单向函数，我们再来看什么是散列函数。

散列函数（Hash Function）是一个可以把任意大小的数据，转行成固定长度的数据的函数。比如说，无论输入数据是一个字节，或者一万个字节，输出数据都是 16 个字节。

我们把转换后的数据，叫做散列值。因为散列函数经常被人们直译为哈希函数，所以我们也可以称散列值为哈希值。通常的，对于给定的输入数据和散列函数，散列值是确定不变的。

你可能会说，我懂了，不就是输入数据任意长度，输出数据固定长度吗？

是的，可问题也来了，既然输入数据的大小没有限制，而输出结果的数据长度固定，那么你觉得，会不会存在散列值相同的两个或者多个数据呢？——是确定存在的。

通常，我们把这种情况称为散列值碰撞。对于散列函数，散列值碰撞可不是一件好事情。

如果你学过 Java 语言或者数据结构和算法，应该对哈希值这个概念不陌生。Java 语言里的 hashCode() 方法，或者数据结构和算法里的哈希值，就是一个散列函数的运用。

如果 hashCode() 的实现出现散列值碰撞，就会影响应用程序的性能，比如 HashMap 的检索时间会显著加长。再比如说，如果我们使用 hashCode 作为键值或者索引，散列值碰撞会导致检索错误，从而带来数据安全问题。

我在第一季《代码精进之路》专栏里，也讨论过散列值碰撞的性能基准测试。如果 10,000 个对象，只有 10 个不同的散列值，它的集合运算的性能是令人担忧的。因为这样和使用了没有散列值碰撞的实现相比，在性能方面，会有几百倍的差异。

现在，你应该意识到这个问题的重要性了，那么，我们应该怎样避免散列值碰撞呢？其实，因为输入数据的大小没有限制，输出数据的长度固定，理论上，我们是无法避免散列值碰撞的。

我们只能在降低散列值碰撞的可能性上想办法。也就是，我们要思考，如果我们不能避免散列值碰撞，我们会有什么办法可以降低散列值碰撞的风险呢？你可以先想一想。

最直观的办法，就是在输出数据的长度上想办法。虽然散列值长度固定，但是，我们可以让数据变得更长，散列值越长，存在相同散列值的概率就越小，发生碰撞的可能性就越小。

比如说，32 位固定长度的散列值就要比 16 位固定长度的散列值发生碰撞的可能性更小。

是不是觉得我们可以解决问题了？问题是解决了，但从另一个角度来说，散列值越长，通常也就意味着计算越困难，计算性能越差。而且，你想一想，为什么当初我们要使用固定长度的散列值？不就是为了减少计算本身的性能损耗，从而获得性能优化吗？

所以，散列值也不是越长越好。那么，我们到底该如何选择散列值的长度呢？

其实，散列值的长度选择，应该是权衡性能后的结果。比如 Java 语言里，hashCode() 的返回值是 32 位的整数，也就意味着散列值的长度是 32 位。由于 hashCode() 的返回值主要是用来检索，32 位的整数已经足够大了，所以这是一个合适的选择。

除了散列值长度之外，想要降低散列值碰撞的可能性，我们还要考虑散列值的质量。一个好的散列函数，它的散列值应该是均匀分布的。也就是说，每一个散列值出现的概率都是一样的。

如果不这样的话，一部分散列值出现的概率就会较高，另一部分散列值出现的概率会较低，别人就更容易构造出两个或者多个数据，使得它们具有相同的散列值。这种行为，叫做碰撞攻击。

如果你要实现在 Java 的 hashCode() 方法，就需要考虑散列值的均匀分布问题。你可以看看《Effective Java》这本书，里面有专门的文章介绍如何实现 hashCode() 方法，降低散列值碰撞的风险。

什么是单向散列函数？

我们说完了什么是单向函数和什么是散列函数，现在我们可以探讨什么是单向散列函数了。

单向散列函数既是一个单向函数，也是一个散列函数。它不仅要满足单向函数的要求，还要满足散列函数的要求。你还记得这两种函数的要求吗？其中，最要紧的就是：

逆向运算困难；

构造碰撞困难。

大部分的 hashCode() 方法的实现，都满足不了逆向运算困难的要求，所以它们是不能算作单向散列函数的。比如说，按照 Java 的 hashCode() 方法的实现，32 位整数的哈希值是这个整数本身，所以逆向运算一点难度都没有，当然不能算作单向散列函数。

单向散列函数是一定要逆向运算困难的。

至于构造碰撞困难，我用现成的单向散列函数给你举一个例子，比如 SHA-1 算法，它是一个常见的适用于密码学的单向散列函数。

现在，你面前有两句话，分别是“Hello, world!”和“Hello, vorld!”，这两句话只有一位的差异 (w: 119/01110111, v: 118/01110110)，我把它们的 SHA-1 算法计算出来的散列值，列在了下面。

你可以对比两个散列值，感受一下一个位的输入数据差异，计算出的散列值能有多大的差异。

SHA-1("Hello, world!):
10010100 00111010 01110000 00101101 00000110 11110011 01000101 10011001 10101110 11100001 11111000 11011010 10001110 11111001 11110111 00101001 01100000 00110001 11010110 10011001

SHA-1("Hello, vorld!):
11001011 11111111 11111011 10010011 01010111 11000010 10001101 01011000 00100010 11000100 01010110 10000110 00101010 00110011 01010000 10111110 10000010 01111111 00100000 10101010

是不是差异还挺大的？这种现象，我们把它叫做雪崩效应。

雪崩效应（Avalanche Effect）是密码学算法一个常见的特点，指的是输入数据的微小变换，就会导致输出数据的巨大变化。严格雪崩效应是雪崩效应的一个形式化指标，我们也常用来衡量均匀分布。严格雪崩效应指的是，如果输入数据的一位反转，输出数据的每一位都有 50% 的概率会发生变化。

一个适用于密码学的单向散列函数，就要具有雪崩效应的特点，也就是说，如果一个单向散列函数具有雪崩效应，那么对于给定的数据，构造出一个新的、具有相同散列值的数据是困难的。

在这一讲的一开始，我们说过，密码学的单向散列函数是用来解决数据完整性问题的。那么，单向散列函数是怎么解决数据完整性问题的呢？

怎么解决完整性问题？

想要解决完整性问题，我们就要知道完整性问题的背后逻辑是什么。

完整性意味着什么？完整性的核心是数据未经授权，不得更改。对于“不得更改”这四个字，你最直观的感受是什么？是不是无论如何，数据都没有办法改动？这是一个很强的解读。一般情况下，也很难有满足的场景。

还有一种站在反面看的、曲线的解读，就是如果数据有变动，能够被检测出来，我们就不采纳被篡改的数据。使用单向散列函数，就可以通过检查数据是否有变动，来解决数据完整性问题。

我们刚才说了，在单向散列函数里，一段数据，无论它是少了一个字，多了一个字，或者修改了一个字，原始数据和修改后的数据的散列值都可能相差巨大。

而且，由于逆向运算困难，虽然存在具有相同散列值的两个或者多个数据，但是对于一个好的单向散列函数来说，刻意寻找这样的数据是困难的。如果困难程度足够大，我们就有足够信心认为，如果散列值没有变化，它对应的输入数据也没有变化。

所以，单向函数和散列函数的组合，单向散列函数，就可以帮助我们解决完整性问题。

假如我们收到了一段数据，我们就可以重新计算这段数据的散列值。如果我们还可以获得数据发送者计算的散列值，我们就可以对比新计算的散列值和接收到的散列值。如果两个散列值是相同的，我们就可以认为这段数据是完整的；否则，这段数据就是被篡改过的。

输入：
    1、数据D
    2、原始数据的散列值H
    3、计算散列值使用的散列函数
输出：
    数据D是不是完整的？

运算：
    1、使用散列函数计算数据D的散列值H';
    2、对比数据的散列值H和计算获得的散列值，如果两个散列值相同，则数据D是完整的；否则，数据D是修改过的数据。

可是，这里面依然有两个遗留问题，也是我们使用单向散列函数需要特别关注的两个问题。

第一个问题是，我们该选择什么样的散列函数，它的破解难度才能足够大？这样，我们才有足够的信心根据散列值判断数据的完整性。

第二个问题是，我们怎么能够安全地获得数据发送者计算的散列值？如果我们接收到的是被修改过的数据和修改过的散列值，我们是没有办法判断数据是不是完整的。

第二个问题，我们放在稍后一点讨论。下一次，我们讨论第一个问题。

Take Away（今日收获）

今天，我们讨论了单向函数、散列函数以及单向散列函数，还有怎么使用单向散列函数来解决数据和信息的完整性问题。

为什么我要先讲单向散列函数？因为，单向散列函数是密码学的基础。在一个应用系统里，如果单向散列函数选择失误，整个系统的安全性就无从谈起。之后，我们还会讨论单向散列函数是怎样和加密算法以及签名算法结合起来，构建宏大的信息安全基础架构的。

我们常说，铁打的营盘流水的兵。在密码学里，最基础的概念就像是铁打的营盘，具有长久的生命力；而密码学算法就像是流水的兵，隔一阵儿就会换一茬。

所以，每一次讨论，我总是会先交代清楚基本概念和基础诉求，然后再带你去看具体的算法。基本概念和基础诉求可以跟随你几十年，随着你对它们理解的加深，会逐渐加厚你的功力。理解了基本概念和基础诉求，你就可以得心应手地调度、安排生命只有十数年的密码学算法了。

这一讲，通过对单向散列函数的讨论，我们要：

理解单向散列函数的以下三个特点：

单向散列函数正向计算容易，逆向运算困难；

单向散列函数运算结果均匀分布，构造碰撞困难；

对于相同的单向散列函数， 给定数据的散列值是确定的，长度是固定的。

知道单向散列函数解决数据完整性问题的基本思路。

思考题

我们回头看看上一次讨论过的牛郎织女的约会问题。牛郎要给织女发信息，七夕相约鹊桥会。

织女：

七月初七晚七点，鹊桥相会。不见不散。

牛郎

你能够帮助牛郎想想吗？该怎么使用单向散列函数，来防范约会信息被恶意修改？然后，你再想想，你建议的办法还有没有缺陷？欢迎在留言区留言，记录、讨论你的想法。

好的，今天就这样，我们下次再聊。

03｜如何设置合适的安全强度？

你好，我是范学雷。

上一讲，我们讨论了单向散列函数，以及它是怎么解决数据完整性问题的。你还记得它解决问题的背后逻辑吗？就是因为单向散列函数有两个重要的特点：逆向运算困难和构造碰撞困难。

这两个特点使得我们仅仅修改数据中的一位，所得到的散列值和之前的相比，就会发生很大的变化。所以我们说，这两个困难也决定了一个单向散列函数的破解难度。

逆向运算越困难，破解难度越难；构造碰撞越困难，破解难度也越难。这点你应该懂了，但是，你有没有想过，困难程度要多大，才算困难？有什么指标可以衡量单向散列函数的破解难度？

一下出现这么多问题，是不是有点意外？其实，密码学就是在和千奇百怪的问题的纠缠中获得进展的。这一次，我们来讨论困难有多难以及和破解难度相关的问题。

困难要有多难？

我们要探讨的第一个问题就是，一个单向散列函数的逆向运算和构造碰撞要困难到什么程度，它才能算是一个合格的单向散列函数呢？如果凭感觉，在密码学的实践中，我们心中“完美”的单向散列函数，应该困难到没有人可以逆向运算，也没有人可以构造碰撞。

可是，只要有人发现了有那么一对数据具有相同的散列值，不管这个人什么出身、什么来历，也不管这对数据有多么的千奇百怪，更不管破解方式是多么的不合常理，这个结果就意味着这个单向散列函数被破解了，不再安全了。

比如说，下面的两段数据具有相同的 MD5 算法散列值（MD5 是一个单向散列函数）。

细心看的话，你会注意到例子中的 afbfa202 和 afbfa200，以及 6da0d1d5 和 6da0d155 这两段数据是有差异的，但是结果显示，它们的散列值却是相同的。在这个例子里，我们并不需要深入了解破解 MD5 的具体算法和实现，我们只需要知道 MD5 被破解了，MD5 就不能够继续使用了。

M1:
4dc968ff 0ee35c20 9572d477 7b721587 d36fa7b2 1bdc56b7 4a3dc078 3e7b9518 afbfa202 a8284bf3 6e8e4b55 b35f4275 93d84967 6da0d1d5 5d8360fb 5f07fea2

M2:
4dc968ff 0ee35c20 9572d477 7b721587 d36fa7b2 1bdc56b7 4a3dc078 3e7b9518 afbfa200 a8284bf3 6e8e4b55 b35f4275 93d84967 6da0d155 5d8360fb 5f07fea2

Hash: MD5(M1) = MD5(M2)
008ee33a 9d58b51c feb425b0 959121c

我所了解的现代单向散列函数在算法意义上的破解，都是通过宣布找到一对散列值碰撞的数据的形式发布的。还记得什么是散列值碰撞吧？就是指两份散列值的数据是相同的。

只有当你找到了这样的一对碰撞，你才能验证破解算法的有效性，算法的破解才能让人信服。

不过，话说回来，这固然是一个好的办法，可是对于还没有被破解的算法，有没有更直观的指标让我们感受它有多安全呢？对于已经破解的算法，有没有直观的指标让我们感受它有多脆弱呢？

在密码学这么讲究量化的领域，当然不会缺少了这样的指标。其中，最常用的指标就是安全强度（Security Strength）。

什么是安全强度？

在密码学中，安全强度通常使用“位”（字节位）来表述。比如说，安全强度是 32 位。这里的“位”是什么意思？N 位的安全强度表示破解一个算法需要 2^N(2 的 N 次方) 次的运算。

为什么要使用“位”来表示安全强度？因为这样的话，我们就可以很方便地比较不同算法的安全级别，在同一个安全级别上组合不同的安全算法。比如说，MD5 的安全强度是不大于 18 位，1024 位的 RSA 密钥的安全强度是 80 位， SHA-256 算法的安全强度是 128 位。

在这里给你出个小问题，如果我们把上面这几个算法安排成一个组合，这个组合的强度是怎样的？这个组合的强度并不高，因为组合的强度，由最弱的算法和密钥决定。所以，把它们安排成一个组合，不是一个好的想法。你可以先记下来，我们后面会再讨论算法组合的基本原则。

回到安全强度这个话题，谈论单向散列函数算法之前，让我们先来感受一下安全强度。比如 MD5，我们说了，它的安全强度最多 18 位，也就是说，我们运算 2^18=262144 次就可以破解，按现在的计算机一毫秒一次运算的速度计算，需要 262144 毫秒，折合 4.34 分钟。

嗯，MD5 现在就是这么弱。其实，在 2006 年，就有研究者宣布研究成功，即使是那时候的笔记本电脑，在一分钟之内也可以找到一对散列值碰撞的数据了。

那 128 位的安全强度呢？假设我们现在有一台速度快 1000 倍的计算机，它能做到 1 纳秒运算一次。如果我们做类似上面的运算，即使我们同时使用 10 亿台计算机，破解它也需要一千万个十亿年。80 位的安全强度，同样的条件，破解大概需要 38 年。

从上面的计算，相信你可以感受到，只是稍微增加几十位的安全强度，破解难度就有巨大的提升。因为，破解难度是安全强度位数的指数（2^N）。所以，在实践中，我们应该优先选择安全强度足够高的算法。

安全强度会变吗？

每一个密码算法诞生的时候，都有一个理论上的设计安全强度。注意，理论上的意思就是有可能与实际情况不符。比如单向散列函数 SHA-1 在 1993 年发布的时候，它的设计安全强度是 80 位。

12 年后，在 2005 年 2 月，中国密码学家王小云教授带领的研究团队发现，SHA-1 的安全强度小于 69 位，远远小于设计的 80 位。从此，SHA-1 的安全强度开始一路衰减。很快，2005 年 8 月，王小云教授的团队又改进了破解算法，发现 SHA-1 的安全强度只有 63 位了。

2015 年 10 月，密码学家马克·史蒂文斯（Marc Stevens），皮埃尔·卡普曼（Pierre Karpman）和托马斯·佩林（Thomas Peyrin）的研究团队发现 SHA-1 的安全强度只有 57.5 位。

更要紧的是，他们估算，如果使用云计算，按照 2015 年亚马逊 EC2 云计算的定价和算力，57 位的安全强度，2015 年的破解成本大致是 10 万美元，你可以感受下密码强度和破解成本的数字。

2020 年 1 月，密码学家盖坦·勒伦（GaëtanLeurent）和托马斯·佩林（Thomas Peyrin）又发现， SHA-1 的攻击复杂度是 63.4 位，攻击成本大约为 4.5 万美元。

根据上面的数字，我们可以感受到，一个 64 位安全强度的密码算法，它现在的破解成本大概是 5 万美元左右。不同类型的算法，破解成本也许有很大偏差，但是我们依然可以大致估算攻击成本。5 万美元，无论是对于一个有组织的研究机构，还是犯罪集团，都是一个很小的数目。

这可以说明什么？如果一个系统的安全强度低于 64 位，它的安全性几乎形同虚设。

通过 SHA-1 的例子，我想强调的就是，一个算法的安全强度不是一成不变的。随着安全分析的进步，几乎所有密码学算法的安全强度都会衰减。今天看起来安全的算法，明天也许就有破解的办法。所以，一个好的安全协议，应该考虑备份计划和应急计划（参见极客时间《代码精进之路》专栏第 41 讲，“预案，代码的主动风险管理“里提到的双引擎和降落伞设计）。

使用多大的安全强度？

现在，我们已经知道了什么是安全强度，也感受了一下不同密码算法的安全强度，知道了安全强度是会变的。那么，我们今天要讨论的最后一个话题是，我们该使用多少位的安全强度？

多少位的安全强度算是安全的呢？其实，我们要是想找到一个确切的答案，我们不仅要看具体的使用场景，还要综合考虑性能和安全强度。是不是觉得会有点复杂和困难？

不过，我可以给你一个建议，就是参考、遵循常用的推荐指标。

业界内最新推荐的三个常用指标分别是：

美国的 NIST（国家标准技术研究所)；

德国的 BSI（联邦信息安全办公室）；

欧洲的 ECRYPT-CSA（欧洲卓越密码网络）。

为了让你更直观地了解这三个指标，我还给你做了一个小结。

看到这个表，是不是感觉还是摸不到头脑？该怎么使用这个表呢？我们一起来看一个例子。

假设，我们现在要设计一个新系统，预期寿命十年，也就是，我们要从 2020 年开始运营，运营到 2030 年结束。而且我们还要保证到 2030 年，这个系统还是足够安全的。

首先，我们按照 NIST 的建议，2030 年后，112 位的安全强度已经不能使用了，所以，如果我们遵守 NIST 的推荐指标，这个系统就不建议选择 112 位安全强度的算法。

在 BSI 建议里，2030 年之前够用的话，我们应该选择 256 位的安全强度。

我们再看 ECRYPT-CSA 的建议，128 位的安全强度只能用于 2028 年之前。到了 2030 年，128 位的安全强度就不能满足 ECRYPT-CSA 的建议了。所以，如果我们遵循 ECRYPT-CSA 的建议，这个系统就需要使用 256 位的安全强度。

你发现了吗，ECRYPT-CSA 的建议为什么这么保守？其实，这种保守的姿态背后，隐含了对量子计算时代来临的担忧。在量子计算时代，128 位的安全强度稍显脆弱，可是 256 位的安全强度还是足够的。虽然量子时代还没有到来，但是我们现在就要开始考虑量子时代的挑战了。

从上面的推荐，我们可以看到，128 位的安全强度，目前来说是安全的。不过，一个需要长期运营的系统，如果性能瓶颈不是问题，现在就可以开始考虑使用 256 位强度的密码算法了。

还记得我们上面提到的安全强度不足 18 位的 MD5 函数吗？这么弱的安全强度，几乎已经没有实用价值了。那么，有哪些单向散列函数能达到 128 位，甚至 256 位的安全强度？这些问题，我们下一次来讨论。

Take Away（今日收获）

今天，通过讨论单向散列函数的“两个困难程度”，我们知道了困难有多难，还分析了破解强度的计量办法、安全强度的衰减、常见的安全强度推荐指标，以及一些可以直观感受的数字。

这些直观感受的数字可以帮助你建立对密码算法安全强度的印象。比如，一个 64 位安全强度的密码算法，它现在的破解成本大概是 5 万美元左右。再比如，128 位的安全强度，按照现有的计算能力，破解它需要一千万个十亿年。

这一讲，通过对安全强度的讨论，我们要：

知道密码学安全强度通常使用位来表示；

知道 128 位的安全强度暂时还是安全的；

知道长期的系统可以考虑开始使用 256 位安全强度的算法了。

思考题

如果你能够使用你知道的所有的计算机，包括你的个人计算机和公司的计算机系统（比如亚马逊的云系统），你能不能大概估算一下，破解 64 位的安全强度、80 位的安全强度、128 位的安全强度，分别都需要多长时间？

这是一个能够帮助你建立对安全强度直观概念认知的办法。

欢迎在留言区留言，记录、讨论你的估算数据。

好的，今天就这样，我们下次再聊。

04｜选择哈希算法应该考虑哪些因素？

你好，我是范学雷。

上一讲，通过讨论单向散列函数的两个困难程度，我们了解到了安全强度的计量办法，安全强度的衰减，以及常见的安全强度推荐指标，我们还对安全强度有了一个更直观的感受。

你还记得上一讲提到的，现在的应用程序要使用 128 位或者更高安全强度的算法吗？那么，对于单向散列函数来说，哪些算法能够满足这样的安全强度要求呢？我们在选择这些算法的时候，应该去考虑哪些因素呢？这就是我们这次要解决的问题。

首先，让我们先来分析一下，还有哪些算法是可用的算法。

有哪些可用的算法？

为什么要先分析有哪些可用的算法呢？因为，在选择哈希算法的时候，我们的确需要综合考虑很多因素，但是如果这个算法是不可用的，其他因素也就无足轻重了。

所以，判断一个现存的算法，还能不能继续使用是我们选择算法的第一步。根据这个标准，我把常见的算法分为了以下三类：

退役的算法；

遗留的算法；

现行的算法。

退役的算法，就是那些已经退出了历史舞台的算法，它们的安全强度很弱，你一定不能再用了。如果你看到退役的算法还在使用，往往意味着这是一个过时的系统，或者是它的开发者缺少密码学常识（这怪不得它的开发者，毕竟密码学常识一直没有得到普及）。

如果是我们自己能够掌控的系统，一定要尽最大努力、尽快地升级算法。

什么是遗留的算法？你只要记住，它们存在明显的安全问题，已经不足以支撑现在的安全强度需求了，你一定不要用在新系统中了。因为，遗留的算法，已经走在退役的路上了。

那为什么有的人还在保留遗留的算法？因为，保留遗留算法，还是会让系统有更好的兼容性和互操作性，给现有系统升级到新算法留有一段时间。但是，新的代码和项目，就不要再使用遗留算法了。现在，还要继续运营的系统，也要想办法尽快升级算法。

到了最后，只有现行的算法，没有明显的安全问题，是我们现在可以使用的算法。这是因为现行算法的安全性，是经过很多密码学专家分析验证的。一般到目前为止，还没有人能发现明显的安全缺陷。现行的算法，才是我们应该使用、可以放心的算法。

不过，不同的推荐指标，对于算法的选择也有不同的考量和倾向。我们这里使用上一讲，我们提到过的较为保守的 ECRYPT-CSA 的 2018 年建议。

在下面的表格里，我罗列了一些常见的算法，以及一些相关的信息。其中，计算性能参考的是 ECRYPT 在 2020 年 7 月和 2019 年 10 月对 4096 个字节数据的性能基准测试结果。

什么是计算性能？它表示在数据运算时，处理一个字节处理需要执行的微处理器的时钟周期数。它使用的度量单位是每字节周期数（CPB，Cycles Per Byte）。

每字节周期数是一个常用的密码算法实际性能的参考指标。每字节花费的时钟周期数越小，表示这个算法运算得越快，性能越好。

为什么有处理能力限制？

在上面的表格里，你能看到，有一列说的是数据处理能力。数据处理能力指的是对应的单向散列函数能够处理的最大的输入数据。比如，SHA-256 能够处理的最大数据是 2^64 位。

我们前面说过，单向散列函数可以把任意大小的数据，转行成固定长度的数据。那为什么有的单向散列函数还有处理能力限制呢？上限不应该是无限大吗？为什么有的单向散列函数，比如 SHAKE128，又没有处理能力限制呢？

问题虽然有点多，不过还是值得我们关注的。我们要想了解这个数据处理能力的限制是什么意思，就要知道它的由来。也就是说，我们需要了解单向散列函数是如何处理输入数据的。

一个典型的单向散列函数，由四个部分组成：数据分组、链接模式、单向压缩函数和终结函数。

单向散列函数处理过程

我们来看数据分组，数据分组负责把输入数据分割成压缩函数能够处理的数据块。在上面的表里，有一项是“数据分块”，按照“位”来计量，指的就是压缩函数能够处理的数据块尺寸。

一般来说，压缩函数能够接收的数据块大小是固定的。比如 SHA-256 的压缩函数只能处理 512 位的数据，多一位不行，少一位也不行。

可是，实际单向散列函数的输入数据的大小不一定就是一个完整的、压缩函数可以接收的数据块。比如说，我们可能使用 SHA-256 处理一个字节，可能处理一千个字节，也可能处理一百万个字节。输入数据可能是 512 位的整数倍，也可能不是。

然后，整数倍的数据，可以送给压缩函数分批处理；不足整数倍的数据，就需要填充、补齐，变成压缩函数可以处理的数据块大小。

在下图 SHA-1 和 SHA-2 的数据补齐方案里，输入数据长度是补充数据的一部分。其中，SHA-1、SHA-224、SHA-256 使用 64 位来表示输入数据长度；SHA-384、SHA-512、 SHA-512/224 和 SHA-512/256 使用 128 位来表示输入数据长度。

但是，如果输入数据长度超过了数据补齐方案的限制，数据就没有办法分组了。这就是单向散列函数数据处理能力限制的来源。

而我们说，SHA-3 的设计，放弃了在数据补齐方案里使用固定位数表示输入数据长度的做法，它也就不再有数据处理能力的限制。

幸运的是，到目前为止，这个数据处理能力限制是很高的，一般的应用程序很难超越它。不过，我们心里还是要有一根弦，需要考虑数据处理能力限制的时候，我们千万不能疏忽了它。

选择什么样的数据补齐方案，是密码学里一个棘手的问题。很多针对密码算法的攻击，都是从数据补齐方案下手的。我们关注这个问题，更多地是为了简单地了解单向散列函数的内部计算。

之后，我们会详细讨论单向散列函数的一个常见的算法错配问题：长度延展攻击。

算法的性能是怎么决定的？

我们再来看一个在选择哈希算法时应该重点考虑的因素，算法的性能问题。

从理论上讲，一个算法的性能主要是由算法的复杂度决定的。这里有一个假设，就是不考虑其他因素的影响。但是在实践中，其他因素有时候才是影响算法性能的主要因素，比如实现细节。

在一个算法的实现细节中，通常影响计算性能的因素有：

算法实现的内存使用影响；

算法实现有没有使用优化的步骤，比如并行计算或者预运算？

算法实现有没有使用硬件加速，比如使用 CPU 关于算法的扩展指令？

一个规规矩矩的算法实现，它的性能一般落后于 CPU 扩展指令数十倍。遗憾的是，并不是每一个算法都有 CPU 扩展指令，或者每一个实现都支持 CPU 扩展指令。

另外，计算机本身的指令集，比如是使用 32 位还是 64 位的指令，是否和算法匹配，也是影响算法性能的一个重要因素。我们经常可以看到，SHA-512 的计算速度，比 SHA-256 还要快。SHA-256 使用 32 位的数据进行计算，而 SHA-512 使用 64 位的数据进行计算。

现在的计算机，一般都是 64 位的。所以运行在 64 位的计算机上，基于 32 位的计算可能反而比基于 64 位的计算还要慢。

这对我们选用算法有什么启示呢？一个应用程序，一般而言，应该选用现行的、流行的算法。现行推荐的算法，保证了算法的安全性。流行的算法，成熟的实现会考虑实现优化，包括 CPU 扩展指令的支持。选用流行的算法，也是获得较好计算性能的一个实践办法。

对于单向散列函数，目前现行的、流行的算法有：

SHA-256

SHA-384

SHA-512

使用现行的、流行的算法是不是就万无一失了？遗憾的是，我们依然需要小心谨慎，不要掉进已知的安全漏洞陷阱。下一次，我们讨论单向散列函数在应用中常见的问题，包括我们上面提到的“长度延展攻击”。

Take Away（今日收获）

今天，通过罗列常见的单向散列函数算法，我们讨论了退役的、遗留的和现行的算法分类；知道了单向散列函数的处理能力限制，以及处理能力限制的来源；我们还简单讨论了影响算法性能的常见因素。这都是我们在选择哈希算法时需要考虑的。

这一讲，我们要：

了解三类单向散列函数算法：退役的算法、遗留的算法以及现行的算法；

知道要尽量选用现行的、流行的算法。对于单向散列函数，它们是SHA-256，SHA-384和 SHA-512。

思考题

今天留给大家的是一个需要动手的思考题。我们罗列了常见的单向散列函数算法，知道了退役的、遗留的和现行的算法。知道了这样的概念，我们就要把它用起来。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索一下这些算法，看看哪些退役的算法还在使用，哪些遗留的算法还在使用。如果发现了退役算法和遗留算法的使用，你有没有什么建议？

这是一个能够帮助你理解算法生命阶段、解决现有项目历史遗留问题的好办法。欢迎在留言区留言，记录、讨论你的发现和建议。

好的，今天就这样，我们下次再聊。

05｜如何有效避免长度延展攻击？

你好，我是范学雷。

上一讲，我们列举了常见的单向散列函数，我们还知道了退役的、遗留的和现行的算法，通过对处理能力限制和算法的性能的讨论，我们对如何选择哈希算法有了更明确的认知。

还记得我们留了一个小尾巴吗？我们提到了“长度延展攻击”。“长度延展攻击”是怎么一回事？我们为什么要了解它？在单向散列函数的使用上，我们需要注意哪些安全问题？

这就是我们这一次要解决的事情。

什么是长度延展攻击？

我们先来看看什么是“长度延展”，这样会有利于你理解“长度延展攻击”。

现在，假设我们有两段数据，S 和 M，以及一个单向散列函数 h。如果我们要把这两段数据合并起来，并且还要计算合并后的散列值，这就叫做单向散列函数的长度延展。

不过，问题来了，是 S 放在前面（h(S|M)），还是 M 放在前面（h(M|S)）？既然，我们说，散列值是无法预测的，那么，数据编排的顺序有意义吗？

如果 S 和 M 都是公开的信息，顺序是不重要的。可如果 S 是机密信息，M 是公开信息，这两段数据的排列顺序就至关重要了。如果机密信息放在了前面，就存在“长度延展攻击”的风险。

弄清楚了长度延展，长度延展攻击就很好理解了，就是说我们可以利用已知数据的散列值，计算原数据外加一段延展数据后的散列值。也就是说，如果我们知道了 h(S|M)，我们就可以计算 h(S|M|N)。其中，数据 N 就是原数据追加的延展数据。

如果 S 和 M 都是公开的信息，能够计算延展数据的散列值也没什么紧要的。但是，如果 S 是机密数据，它的用途一般就和机密有点关系。比如说，因为没有人知道我拥有的机密数据 S，所以，当我给定一段公开信息 M 后，只有我自己才能计算 S 和 M 的散列值。

通过验证 S 和 M 的散列值，我就知道一个给定散列值是我计算、派发出去的，还是别人伪造的。

比如下面的这段数据：

key_id=44fefa051fc1c61f5e76f27e620f51d5&perms=read&hash_sig=38d39516d896f879d403bd327a932d9e

其中，key_id 表示机密数据的编号，perms 表示操作权限，hash_sig 是使用机密数据 key 对 perms 的签名。签名的计算，就是使用单向散列函数：

sig = h(key|perms)

由于使用了机密数据 key，按照设想，这段数据只能由机密数据的持有者生成，然后分发出去，供授权的人使用。机密数据的持有者接收到这样的数据后，重新计算数据签名，然后对比请求数据里的签名。如果两个签名相同，就表示这是一个自己生成的、合法的授权，就可以授予请求数据所要求的权利。

不过，这个设计就存在“长度延展攻击”的风险。攻击者并不需要知道机密数据，就可以通过一个已知的 URL，构造出一个新的合法的 URL，从而获得不同的授权。

伪造的数据看起来像下面的样子：

key_id=44fefa051fc1c61f5e76f27e620f51d5&perms=read\0x80\0x00...\0x02&delete&hash_sig=a8e6b9704f1da6ae779ad481c4c165a3

在这段伪造的数据中，0x80 到 0x02 之间的数据是数据块补齐数据，而且新添加了删除的权限，并且重新计算、替换了数据签名。

其中，数据签名需要使用机密数据，而攻击者并不知道机密数据，那么攻击者怎样伪造数据签名呢？要解决这个疑问，我们需要先看看单向散列函数的构造。

我们在上一讲简单地提到过，一起来重新回顾一下。一个典型的单向散列函数，应该由四个部分组成：数据分组、链接模式、压缩函数和终结函数。

我们之前着重说了数据分组，我们现在来看看其他的部分：

单向散列函数处理过程

压缩函数是单向函数，负责着算法的单向性要求；

终结函数不是单向函数，负责着整理压缩函数的输出，形成散列值的任务；

链接模式，负责把下一个数据分组和上一个压缩函数的输出结果结合起来，确保算法的雪崩效应能够延续。

值得一提的是，在 MD5，SHA-1，SHA-256 和 SHA-512 的算法设计中，终结函数就是把压缩函数的输出向量排列成一个字节串。知道了字节串，我们也就知道了压缩函数的输出向量。

压缩函数接收一个数据分组和上一个压缩函数的运算结果。如果知道了上一个压缩函数的运算结果，我们就能够计算下一个分组数据的压缩函数运算结果。这里，就是出现安全漏洞的地方。

单向散列函数压缩函数示意图

我们把原来的散列值作为压缩函数的一个输入，我们再按照数据补齐规范，去补齐原来数据到数据分组的整数倍，然后加入新的数据，我们就可以计算原数据和扩展数据的散列值了。

单向散列函数长度延展攻击示意图

新的散列值的计算，不需要知道预先设想的机密数据。但是整个散列值的计算，又的确使用了机密数据。只不过，这个计算过程需要两个部分，第一部分由机密数据的持有者计算，第二部分是攻击者使用第一部分的结果，伪造了一个使用了机密数据的散列值。

但是，如果我们把数据编排顺序换一下，把公开信息 M 放在前面，机密信息 S 放在后面，长度延展攻击就不起作用了。这就是数据编排顺序对数据安全性的影响。

怎么有效避免长度延展攻击？

一个单向散列函数，只要使用了类似上述的压缩函数和链接模式，都是“长度延展攻击”的可疑对象。我们上一次提到的 MD2、MD5、SHA-0、SHA-1、SHA-2，都有长度延展攻击的风险。其中，对于下列算法，长度延展攻击是完全有效的：

MD2

MD5

SHA-0

SHA-1

SHA-256

SHA-512

对于下列算法，长度延展攻击虽然不是完全有效，但是算法的安全级别显著降低了：

SHA-224

SHA-384

对于下列算法，长度延展攻击没有效果（包括所有的 SHA-3 算法）：

SHA-512/224

SHA-512/256

SHA-3

上面这么长的列表，你是不是觉得好多，有点烦？其实，我们讨论长度延展攻击，目的不是让你记住上述的列表。

我们要从中学会、理解一个实用的经验：不要单纯使用单向散列函数来处理既包含机密信息、又包含公开信息的数据。即使我们把机密信息放在最后处理，这种使用方式也不省心。

如果我们需要使用机密数据产生数据的签名，我们应该使用设计好的、经过验证的算法，比如我们后面会讨论的消息验证码（Message Authentication Code）和基于单向散列函数的消息验证码（Hash-based Message Authentication Code）。

另外，如果需要设计算法，我们还要理解另外一个实用的原则：算法要皮实、耐用，不能有意无意地用错了就有安全漏洞。你看，SHA-1 和 SHA-2 已经很简单、皮实了，用错了场景还是有严重的问题。相比之下，SHA-3 同样简单，但是更皮实。

这和我们在《代码精进之路》的专栏里反复讨论的 API 要简单、直观、皮实，是一个道理。

既然我们不能单纯地使用单向散列函数处理混合了机密信息和公开信息的数据。那我们能不能单纯地使用机密信息，或者单纯地使用公开信息？回答这个问题，还要看具体的使用场景。

有哪些典型的适用场景？

我们已经知道了，单向散列函数是密码学的核心。下面是一些典型的使用单向散列函数的场景：

校验数据完整性；

数字签名，和非对称密钥及其算法结合使用；

消息验证码，和对称密钥及其算法结合使用；

生成伪随机数；

生成对称密钥。

还记得我们在之前，讨论过了怎么使用单向散列函数校验数据完整性。

输入：
    1、数据D
    2、原始数据的散列值H
    3、计算散列值使用的散列函数
输出：
    数据D是不是完整的？

运算：
    1、使用散列函数计算数据D的散列值H';
    2、对比数据的散列值H和计算获得的散列值，如果两个散列值相同，则数据D是完整的；否则，数据D是修改过的数据。

如果我们单纯地使用单向散列函数校验数据完整性，是要对比数据的散列值的。既然是对比，也就意味着有两个散列值。这时候，我们需要考虑的主要问题就是：给定的散列值有没有被更改？

散列值的计算是公开的，给定一段数据，谁都可以计算它的散列值。如果数据可以被修改，而且给定的散列值也是修改后的数据的散列值，这个数据完整性校验是没有意义的。

所以，单纯使用单向散列函数去校验数据的完整性，我们需要确保给定的散列值是不能被修改的，这就是这个使用场景的限制。

其余的单向散列函数的使用场景，我们后面还会接着讨论。

Take Away（今日收获）

今天，我们讨论了单向散列函数的长度延展攻击，以及使用单向散列函数需要注意的事项，还列举了典型的单向散列函数使用场景。

通过今天的讨论，我们要：

知道单向散列函数存在长度延展攻击；

了解避免长度延展攻击的办法；

尽量不要单纯使用单向散列函数来处理包含机密信息的数据。

另外，今天也是单向散列函数这一模块的最后一讲了。我们也来小结一下这一模块要注意的知识点，拉个清单。

在这一模块里，我们要掌握下面的基本概念和最佳实践：

知道单向散列函数的三个特点：正向计算容易，逆向计算困难，散列值长度固定。

如果散列值不能被恶意修改，单向散列函数可以用来解决数据完整性问题。

知道有退役的算法、遗留的算法和现行的算法；并且，不要使用退役的算法，尽快升级遗留的算法。

了解密码学算法常用的三个推荐系统，美国的 NIST、德国的 BSI 和欧洲的 ECRYPT-CSA，要养成定期查看推荐指标的习惯，跟得上密码学的进展。

知道安全强度，以及现在要使用 128 位的安全强度的密码学算法，长期系统要考虑使用 256 位的密码学算法。

知道要尽量选用现行的、流行的算法。对于单向散列函数来说，它们是 SHA-256，SHA-384 和 SHA-512。

尽量不要单纯使用单向散列函数来处理包含机密信息的数据；如果不得已，要尽量避免长度延展攻击。

思考题

好的，又到了留思考题的时间了。

今天的思考题是一个拓展题，你要自己去发现单向散列函数的更多适用场景。

我们一直强调，使用单向散列函数校验数据完整性，需要保证原始的散列值不能被更改。你能不能找到一些场景，可以让我们不用担心原始的散列值被更改，单纯使用单向散列函数就可以校验数据完整性？

除了我们上面列出来的一些场景，你能不能找出更多的单向散列函数使用场景？比如说，利用散列值长度固定的特点，利用碰撞困难的特点？

欢迎在留言区留言，记录、讨论你发现的新使用场景。

好的，今天就这样，我们下次再聊。

06｜对称密钥：如何保护私密数据？

你好，我是范学雷。

在上一个模块，我们学习了单向散列函数。从今天开始，我们将开启一个新的模块，在这个模块里，我将跟你讨论加密技术的相关知识。是不是感觉上一个模块的学习还意犹未尽？

别着急，单向散列函数还会出现在我们的视野里。那么，加密技术是用来做什么的呢？

还记得上一讲，我们讨论了单向散列函数的使用场景吗？其中，一个重要的限制是我们需要确保给定的散列值不能被修改。这个简单、直观的限制，给单向散列函数的使用套上了一个紧箍咒。

这说明在很多场景下，我们并不能仅仅使用单向散列函数来解决数据的完整性问题。要想去掉这个紧箍咒，扩大单向散列函数的适用场景，我们还需要其他技术，比如加密技术。

那加密技术是怎么帮助单向散列函数解决完整性问题的？这个疑问立即就来到了我们面前。不过不用担心，我们需要一点时间来了解这个问题，以及解决问题的办法。

今天，我们先来讨论第一类加密技术：对称加密技术。

什么是加密？

在讨论对称加密技术之前，我们要先了解加密、解密和密钥这几个概念。

其实这几个概念还是很容易理解的。把信息或者数据伪装、隐藏起来，转换成难以解释的信息或者数据，这个过程叫做加密。和加密这个过程相反的过程，就叫做解密。

一般来说，加密产生的那个难以解释的信息或者数据，我们把它叫做密文（Ciphertext）。对应的，加密前的数据，我们通常把它叫做明文（Plaintext）。

密文信息通常看起来都是晦涩难懂、毫无逻辑的，所以我们一般会通过传输或者存储密文信息，来保护私密数据。当然，这建立在一个假设基础上：没有经过授权的人或者机器，很难通过密文计算出明文；经过授权的人或者机器，才能够通过密文计算出明文。

那经过授权的人或者机器，是怎样通过密文计算出明文的？对，就是使用密钥。

在现代密码学里，密钥是在加密和解密运算里，决定运算结果的一段信息。因为，加密要使用密钥把明文信息转换为密文；解密要使用密钥把密文复原为明文。

也就是说，加密运算需要两个输入：密钥和明文。

解密运算也需要两个输入：密钥和密文。

如果没有密钥，我们就没有办法执行解密运算，也就很难把密文转换成明文。同理，如果只有授权的人或者机器才知道密钥，那么没有授权的人也很难通过密文计算出明文。

你可能会觉得，密钥太重要了！但现代密码学之前的加密，其实不是这样设计的。

历史上的加密，是没有密钥的。数据的保密性，依赖于算法的保密性。一旦算法被破解，数据也就被破解了。如果有一天，时光穿梭机真的实现了，我们穿越回去，偷听一下、偷看一下当初设计者的算法设计，算法就被破解了（时光穿梭机的梗，你可以自己搜索一下）。

其实也用不着这么科幻，就算时光穿梭机实现不了，我们也还有很多更有效的办法：

当初算法的设计者还健在吗？

当初算法的实现者还健在吗？

算法实现的代码还在吗？

算法运行的环境还在吗？

解决掉其中任何一个问题，我们就能破坏掉算法的保密性。而且，这些破解办法通常没有什么难度，比制造时光机有效率多了。除此之外，还要说一点，虽然算法保密看起来很安全，但是这也意味着只有很少的人知道算法，这样的算法质量也是值得担忧的。

到了现代密码学，加密数据的安全性就依赖于加密算法的质量和密钥的保密性这两个因素。密钥部分，是私有的部分，需要严格保密；算法部分，变成了公开的部分，要接受公开讨论、评测，接受各种分析和攻击。一个算法，如果在接受了公开的分析、评测和各种各样的攻击之后，还依然被认为是安全的，我们才能说，这个算法的安全性是真的经得起考验的。

为什么算法一定要公开？不公开不行吗？为了可以让你更直观地了解使用公开算法有多重要，我们一起来看看公开算法的遴选过程是怎样的。

所以，你看，仅仅单向散列函数的遴选，就花费了 7 年时间，还聚集了世界上最出色的密码学家和密码分析专家。在遴选标准中，有一个重要指标，就是有没有足够多的密码分析。

什么是密码分析？密码分析，指的是分析、评测一个密码学算法，有没有安全缺陷和适用场景的限制。如果一个算法，没有人对它展开分析、评测，或者缺少足够的分析，它的安全性很难获得信任。63 个落选的算法中，不乏知名密码学专家，或者知名团队和组织的撑腰。

我相信，这些算法在提交之前，它的发明者都是信心满满的。可是一旦接受了公开的分析和评测，很多意想不到的安全缺陷就暴露出来了。

但是，保密的算法，如果没有经过大量密码分析专家的分析，是很难给人信心的。可如果经过了大量的、不同的密码分析专家的分析，保密算法也算不上保密了。的确，这是一件很矛盾的事情，有时候却又不得不这样。

所以，渐渐地，使用公开的算法是密码学领域的一个基本常识。不过，一个现代密码学算法的安全性，都是基于密钥的保密，而不是算法保密要求。遗憾的是，仍然有很多保密算法的存在和使用。对于这样的使用，我们很难有信心相信它的安全性。

为什么我们要花费这么大篇幅去讨论公开算法的遴选过程呢？

其实是因为，我想让你对以下两个密码学常识留下深刻的印象：

不要自己发明密码算法，尤其是在没有经过充分讨论、充分分析的情况下。大部分情况下，我们自行发明的密码学算法都是灾难。

不要把安全性寄托在算法的保密上。大部分情况下，保密的算法都是无法保密，并且是不堪分析的。

在这个部分里，我最后再强调一下：现代的密码学算法的安全性，都是基于密钥的保密，而不是算法保密要求。管理好密钥，做好密钥的保密，才是密码学系统最关键的任务。

什么是对称密钥？

讨论完加密和密钥，我们就要来看看对称密钥技术。

说起来对称密钥，就不得不提它的对立面，非对称密钥。1976 年，惠特菲尔德·迪菲（Whitfield Diffie）、马丁·赫尔曼（Martin Hellman）发表了基于非对称密钥技术的密钥交换算法，但是在这之前，并没有对称密钥、非对称密钥的说法。

1976 年之前，密码学就是一门研究对称密钥的学问。所以，我们看的二战时期的谍战片，如果里面提到了发报机和密钥，用的肯定不会是非对称密钥技术。这种影响，到现在还有，比如，当我们使用密钥这个词汇时，一般指的就是对称密钥。

对称密钥，顾名思义，就是每一个参与者都持有相同的密钥，使用相同的密钥。

非对称密钥，就是指每一个参与者都持有不同的密钥，使用不同的密钥。

经常有人问我，密码属于对称密钥技术吗？如果属于的话，为什么密码学不叫密钥学？密码和密钥有什么区别吗？所以，在这里，我要稍微地强调一下。

密钥和密码是两个特别容易混淆，而且经常混淆的概念。比如密码学明明是研究密钥的，偏偏叫“密码”学；密码分析明明是研究加密算法的，偏偏叫“密码”分析。怎么理解呢？

有一个技巧，就是借助英语词汇，这两个概念一下子就会清晰。通常地，密码的英语词汇是 Password，加解密算法的英语词汇是 Cipher，密钥的英语词汇是 Key。

密码（Password）使用中文里的“口令”，更为贴切。比如三国时期的曹操，据说就使用过“鸡肋”作为口令。口令该怎么用呢？如果执勤的士兵问：“口令？”杨修回答“鸡肋”，这就可以获得通行许可了。如果回答的不是“鸡肋”，就不能获得通行，还可能被逮起来进一步审查。

和对称密钥类似，需要每一个参与者都知道相同的口令，使用相同的口令。

而密钥（Key）和密码（Password）的区别，在于它们的用法。口令的用法是对照口令本身。士兵知道口令是“鸡肋”，然后对照他人的回答是不是“鸡肋”。

我们上网输入的用户名和密码，也是系统要直接地或者间接地进行对照，登录者是不是使用了系统记录的口令。而密钥的用法，则是参与加密运算或者解密运算。

通常地，我们也不用纠结别人是不是能准确地使用好“密码”和“密钥”这两个词汇。我们只要从它们的使用场景来判断，到底是用作对比的口令，还是用来运算的密钥就好。

另外，在生活中，我们总是在记口令，比如我们登录网站的密码。和自然界其他生物相比，人类的记忆能力值得自豪。我们通常可以记住六位的数字，或者学过的单词。不过，即便如此，我们还是倾向于选择“123456”， “888888”或者生日这样的简单口令。

稍微复杂的口令，就超越我们的记忆能力了，更别提要记住很多网站的很多口令了。在现代计算机的眼里，这样的记忆能力实在太渣了，用最不讲究技巧的蛮力攻击也就是分分钟钟的事情。

密码需要记，但是一般来说，我们不需要记住密钥，事实上，我们也记不住。现代的密钥，通常需要至少 128 位没有规律的字符，而且频繁更换。比如，下面的 5 个密钥，其实是质量不太好的、便于记住的 128 位的密钥，你可以挑战挑战，看看能不能记得住：

密码1: Yq3t6w9z$C&F)J@N
密码2: gVkYp3s6v9y$B&E)
密码3: NdRgUkXp2s5v8y/B
密码4: -JaNcRfUjXn2r5u8
密码5: C&F)J@NcQfTjWnZr

我们当然记不住这么复杂的密钥，除非是不世的天才。所以，我们才要拜托计算机替我们记住这么复杂的信息，并且自动地更换。

密钥管理的烦恼

如果我们拜托计算机替我们管理密钥，方便是方便了，但是也会立即衍生出很多现实的问题：

计算机替我们记住了密钥，计算机能够保持密钥的保密性吗？

计算机会不会出卖我们？

使用密码的程序会不会泄漏密钥？

运行算法的环境能不能泄漏密钥？

退役的机器里，会不会有密钥存留导致密钥泄漏？

……

无论哪个问题没有处理好，密钥的保密性可能都只是空谈。

既然现代的密码学算法的安全性依赖于密钥的保密，那么，管理好密钥，做好密钥的保密，就是密码学系统最关键的任务。不过，密钥的管理，部分内容已经超出了密码学的范畴，我们需要在计算机基础的操作系统和编程语言里找答案。

比如，在《代码精进之路》里，我们提到的管理敏感信息的原则，同样适用于密钥的管理。要把密钥当作超级敏感的信息来看待，在我们的代码里保护好密钥，不要泄漏密钥信息。

之后，我们还会讨论对称密钥的管理，以及怎么使用密码学的技术降低密钥管理的难度和风险。

像单向散列函数一样，不同的加密算法也有不同的安全强度。那么，到底哪些对称密钥的算法是我们可以信赖的呢？这是我们下一次要讨论的问题。

Take Away（今日收获）

今天，我们讨论了什么是加密解密、什么是对称密钥、密钥保密的必要性，以及密钥管理的困难。通过今天的讨论，我们知道要把密钥当作超级敏感的信息来处理。

这需要我们在编写代码的时候，要特别留意密钥的无意识泄露，以及在内存、硬盘里的长时间驻留。比如说，用完密钥后，我们的代码一定要把密钥占用的内存清零，而不要依赖类似 Java 垃圾收集器这样的机制。再比如说，我们千万不能把密钥写到系统或者应用的日志里，日志可是泄露密钥的最便捷路径之一。

通过今天的讨论，我们要：

理解什么是对称密钥；

知道对称密钥的安全性，取决于密钥的保密性和算法的安全性，而不是算法的保密性。

要把密钥当作超级敏感的信息来处理，做好密钥的保密。

思考题

你还见过哪些坑？处理敏感信息，你有哪些经验？另外，你有没有发明过密码算法？你了解的项目有没有发明过密码算法？这些算法有能够替代的公开算法吗？

欢迎在留言区留言，分享你的经验。参与讨论的人越多，我们互相学习、互相启发的就越多。

好的，今天就这样，我们下次再聊。

07 | 怎么选择对称密钥算法？

你好，我是范学雷。

上一讲，我们讨论了什么是对称密钥，你还记得决定对称密钥系统安全性的两个关键因素吗？密钥的保密性和算法的安全性。和单向散列函数一样，我们也要分析如何选择对称密钥算法。

所以，有哪些算法是值得我们信任的呢？这就是我们这一次要解决的问题。首先，我们还是先来一起看看曾经流行的和现在流行的对称密钥算法。

数据加密影响性能吗？

像单向散列函数一样，我们可以把对称密钥算法也按照退役的、遗留的以及现行的算法来分类。

在下面的表格里，我给你总结了常见的一些算法，以及一些相关的信息。其中，计算性能参考的是 ECRYPT 性能基准测试在 2020 年 7 月对较长数据的运行结果。

对于不同的系统，这个计算性能数据差距可能很大，不过，它足以让我们有一个大致的感受。

（文末附各个算法的参考文献链接）

如果我们关注一下上表里的计算性能，每个字节的加密、解密运算需要大约 0.5 个时钟周期。你看这个数字，对比我们在前面提到的单向散列函数的计算性能，我们可以感受到，加密、解密运算是一种很快的运算。之后，我们还会交代非对称密钥的计算性能，你也会有同样的感受。

很快的运算，也是需要额外的运算的。对比数据不加密和数据加密，计算性能的影响到底有多大呢？这个答案当然依赖于具体的环境。不过，我们可以看一个最常见的场景，感受一下数据加密给计算性能带来的影响。这个场景就是我们常用的互联网 Web 服务。

现在的 Web 服务，大部分都是运行在 HTTPS 协议上的。和 HTTP 协议相比，在 HTTPS 协议通道上传输的应用数据，都是加密的数据。有测试数据表明，和 HTTP 相比，一个合理配置的 HTTPS 服务，客户端的响应时间没有明显的变化；服务器的吞吐量大约减少了 5% 到 8%，CPU 的使用大约增加了 2% 到 5%。

我们可以看到，数据加密会影响性能，但是这个影响并不显著。这也是近年来，绝大多数 Web 服务迁移到 HTTPS 协议的背后技术支撑。十多年前，大部分 Web 服务还是运行在 HTTP 协议上，传输数据没有加密，用户的数据经常就莫名其妙地丢失了，Web 服务网页动不动就被黑了。

现在，如果一个 Web 服务还没有转换到 HTTPS 协议上，这个服务的安全性是不能让人放心的。

接下来，我们看下面的这张图，显示的是在不同的地域，使用 HTTPS 的网页的百分比。这张图是由 Google 根据 Chrom 浏览器加载 Web 页面的数据统计绘制的。

我们能看到，在 2015 年，不足 50% 的 Web 页面使用 HTTPS 协议。到 2020 年，领先的几个地域，已经有大约 95% 的 Web 页面使用了 HTTPS 协议。总的来说，在短短的五年时间里，Web 服务整体的安全性有了巨大的提高。

也许，再过五年，就很难找到不使用加密通道的 Web 服务了。这是互联网消费者的幸事！

序列算法和分组算法

不知道你注意到没有，在上面的表格里，RC4 和 ChaCha20 没有数据分块数据，而 3DES 和 AES 有数据分块数据。这是因为 RC4 和 ChaCha20 是序列算法，3DES 和 AES 是分组算法。那么，问题来了，序列算法和分组算法有什么不一样的呢？

我们前面讲过，为了能够处理任意大小的数据，并且输出结果长度固定，单向散列函数需要对数据进行分组，然后按数据组进行运算。在对称密钥算法里，因为输出结果的长度没有限制，对数据的处理方式，也就有了更多的想象空间。

如果我们从数据是否分组这个角度考虑，就有两种处理方式。

进行数据分组，然后按数据组运算，这就是分组算法；

不进行数据分组，按照原始数据的大小进行运算，这就是序列算法。

那么，怎么计算序列算法呢？

序列算法的基本思路，就是从对称密钥里推导出一段和明文数据相同长度的密钥序列，然后密钥序列和明文进行亦或运算得到密文，和密文进行异或运算得到明文。

序列算法的关键，是怎么从固定长度的对称密钥推导出参与运算的任意长度的密钥序列。一般来说，密钥序列的推导和异或运算都是快速的运算。所以，序列算法通常被认为是更高效的算法。

比如说，在类似的条件下，相同安全强度的对称密钥，ChaCha20 算法要比 AES 算法快四到六倍，比 Camellia 算法快近十倍。如果你关注过浏览器底层技术细节，你可能会注意到，ChaCha20 是现代主流浏览器优先选择的加密算法。

由于不需要数据分组，序列算法的安全性主要取决于密钥序列的推导算法，而不用考虑数据分组带来的种种陷阱。对于应用程序而言，这是一个便于使用，不易出错的选择。

分组算法的安全性，除了算法本身之外，还取决于数据分组的策略。应用程序需要同时指定分组算法和分组策略。对于不太了解密码技术的细节的程序员来说，这实在是一个不小的挑战。

良好的性能，以及皮实的用法，这是我倾向于优先使用序列算法的两个基本原因。同样，按照这两个标准，同时考虑安全强度，我建议你使用下面的对称密钥算法，按照优先级从高到低排列：

ChaCha20

AES-256

AES-128

不过，这个建议的实用性还欠考虑，因为我们还没有考虑数据分组的影响，以及其他的一些因素。对于分组算法，数据分组到底有什么样的麻烦？对于序列算法，还有什么因素需要考虑？上面的推荐该如何修正？是我们之后要讨论的话题。

Take Away（今日收获）

今天，我们罗列了常见的对称密钥算法，讨论了数据加密对应用性能的影响，还知道了分组算法和序列算法这两个概念。

这一讲，我们要：

知道什么是序列算法，什么是分组算法；

知道现在优先选择的三个算法：ChaCha20，AES-256，AES-128

思考题

今天留给大家的也是一个需要动手的思考题。我们罗列了常见的对称密钥算法，其中有退役的、遗留的和现行的算法。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索一下这些算法，看看哪些退役的算法还在使用，哪些遗留的算法还在使用。如果我们发现了退役算法和遗留算法的使用，你有没有什么建议？

这是一个能够帮助你解决现有项目历史遗留问题的好办法。欢迎在留言区留言，记录、讨论你的发现和建议。

好的，今天就这样，我们下次再聊。

附：本讲表格中的各个算法参考文献链接

DES：RFC 18229

RC2：RFC 2268

3DES：RFC 1851

Camellia-128、192、256：RFC 3713

AES-128、192、256：FIPS 197

ChaCha20：ChaCha

08 | 该怎么选择初始化向量？

你好，我是范学雷。

上一讲，我们讨论了对称密钥的常见算法，还讲到了序列算法和分组算法。还记得吗？当时，我建议你优先使用序列算法，因为它有着良好的性能和皮实的用法。另外，我还向你推荐了 AES-256 和 AES-128。

但是，由于我们还没有考虑数据分组等因素的影响，所以这个建议的实用性还有待商榷。那么，这一讲，我们就来看看对于分组算法，到底有哪些麻烦？我们又该怎么避免这些麻烦？

其实，这是一个解决起来很复杂的问题。不过，今天我们可以先对问题建立一个初步的认知。

要知道分组算法有哪些麻烦，就要先知道该怎么计算分组算法。

分组算法怎么计算？

我们在上一讲说过，分组算法要对输入数据进行分组，然后按数据分组来进行运算。一个典型的分组算法，一般要由三个部分组成，数据分组、分组运算和链接模式。

我们先来看看数据分组是怎么一回事。

数据分组在加密时，会把明文的输入数据分割成加密函数能够处理的数据块。比如，AES 算法能够处理的数据块大小是 128 位，那么，输入数据就要被分割成一个或者多个 128 位的小数据块。

如果不能整分，就要把最后一个分组补齐成 128 位。这些分组数据的运算结果，组合起来就是密文数据。解密时，执行相反的操作，把补齐数据去掉，再把数据分组组合成完整的明文数据。

理解了数据分组，我们再来看分组运算和链接模式。

分组运算，意思就是把每一个明文数据分组通过加密函数，转换成密文数据分组。而链接模式，指的是如何把上一个分组运算和下一个分组运算联系起来。

有一点需要说，第一个分组运算并没有上一个分组运算可以使用，这时候，我们就需要引入一个初始化的数据，来承担“上一个分组运算”向下链接的功能。这个初始化的数据，我们一般称为初始化向量。

那你有没有想过，我们为什么要把上一个分组运算和下一个分组运算联系起来呢？其实，我们在前面讨论过单向散列函数的链接模式，我们说它是为了确保雪崩效应能够延续。

在分组运算里，链接模式也承担类似的功能：

不同的明文数据，它的密文数据应该是完全不同的，即使明文数据里包含相同的数据分组；

相同的明文数据，每一次的加密运算，它的密文数据也应该是完全不同的。

什么影响算法的安全性？

现在，我们已经梳理了一遍分组算法的运算过程了。这样，我们就能够在其中找到影响分组算法的关键因素。这些因素，也就是影响分组算法安全性的因素。

在数据分组里，把输入数据分割成固定大小的数据块这一部分，除了数据补齐之外，没有什么变数。所以，我们可以发现，数据补齐方案才是影响分组算法的关键部分。

这样，我们就不难找出下面的五个因素：

加密函数和解密函数；

密钥；

初始化向量；

链接模式；

数据补齐方案。

通过上一讲的讨论，我想我们都了解加密函数、解密函数和密钥在分组算法中的重要地位了。如果加密函数不安全，整个分组算法的安全性也就坍塌了；如果密钥没有做好保密或者密钥质量不好，数据的保密性也就无从谈起。

比如说，我们经常看到宣传，说什么采用了 AES-256 算法，安全强度有保障；说什么只有造一台时光机，穿越回历史现场，才能破解一个应用。这些说法，有它的道理，但是仅仅依据这些信息，还不能确认一个算法的使用和运算是不是安全的。

另外三个因素，就是经常被我们忽视的因素。那么，它们是怎么影响算法安全性的呢？

初始化向量怎么选？

我们今天先讨论第一个影响的因素：初始化向量。

我们要想了解初始化向量对算法安全性的影响，就要先看看第一个数据块是怎么计算的，第一个数据块和初始化向量关系紧密。第一个数据块的计算，它的输入信息包括：

密钥；

初始化向量；

第一个明文数据分组。

如果我们能够确定这些输入信息，那么输出的第一个密文数据分组也就确定了。

一般来说，一个对称密钥要使用多次，对多个明文数据进行加密运算。如果存在第一个明文数据分组相同的两段数据，并且使用了相同的初始化向量，那么第一个密文数据分组就是相同的。

也就是说，相同的输入，就会有相同的输出。

对于大部分算法而言，分组数据块都比较小，比如，AES 算法的分组数据块大小是 16 个字节。这样，在实际应用中，就比较容易构造相同的数据块，或者存在相同的数据块。

在现实的应用里，也存在大量的、重复的、已知的数据，比如 HTTP 协议的头部数据。如果我们需要保密的数据恰好重复了一段已知的明文，攻击者就可以根据密文数据是不是相同，来猜测、寻找明文数据。这样的话，就破坏了数据的保密性。

但是，我们在使用加密运算时，大部分时候都没有办法确定明文数据会不会有重复数据，以及重复数据会不会是一次加密运算的第一个数据块。所以，如果不想暴露重复数据的机密性，我们只能在初始化向量这一个因素上想办法。因为，密钥是相同的，如果第一个明文数据分组也是相同的，只剩下初始化向量这一个输入信息可以控制了。

在一个对称密钥的生命周期里，初始化向量不能重复，这是使用对称密钥算法的第一个要求。

这个要求看似简单，其实做起来并不容易。一个单纯的加密算法的实现，一般没有办法记住一个初始化向量有没有用过。这就需要应用程序的开发者自己想办法，常见的办法有两种：

使用安全强度足够的随机数作为初始化向量；

使用序列数，下一次的初始化向量的数值，比上一次的数字自动加一或者自动减一。

不过，这两种初始化向量的选择，还是各有各的问题，我们需要注意。

第一种，随机数的获取，有时候不是一个有效率的运算。如果随机数发生器选择不当，还会造成加密运算的阻塞，进一步降低加密运算的效率。另外，由于解密需要相同的初始向量，如何在加密端和解密端同步初始化向量，也是一个需要考虑的问题。

一个常见的解决办法，就是把初始化向量和加密数据一起发送给对方。

第二种的话，使用序列数，需要保持序列数的状态，还需要加密运算的同步。不过，序列数状态的保持和同步，除了效率之外，还会衍生出其他的待解决的问题，比如分布式计算环境下的序列数同步问题，比如攻击者会知道每一个初始化向量的问题。

如果你能够看到的问题无法解决，可以考虑使用随机数作为初始化向量。

你看，初始化向量选择充满了复杂性，一般的密钥算法库都不会提供缺省的、自动的初始化向量。应用程序需要根据使用场景来制定适当的初始化向量选择方案，这是一个容易忽略的要求。

一个密钥能用多少次？

在这一讲的最后，我们来讨论一个话题，一个密钥有没有使用次数的限制呢？为什么要在这一讲讨论这个话题呢？因为，我想，这是一个恰当的时机。

前面，我们讨论了，在一个对称密钥的生命周期里，初始化向量不能重复。也就是说，对于一个算法来说，初始化向量的长度是固定的。长度固定，也就意味着初始化向量的个数是有限制的。

比如，一个 128 位的初始化向量，最多有 2^ 128 个不重复的数值。进一步的说，对于这个算法，一个密钥最多只能使用 2^ 128 次。的确看起来，2^128 是一个巨大的数字，一般的应用程序也没有什么机会使用这么多次加密运算。

当然，还有其他因素限制密钥的使用次数。很多限制因素的叠加，就会使得密钥使用的限制数远远低于初始化向量的许可数目。所以，我们心里一定要知道，密钥是有使用次数限制的，并且要有检查密钥使用次数限制的习惯。

这是一个不太引人注意的安全陷阱，也是近几年才受到广泛关注的算法安全问题。我们后面还会讨论其他的限制条件，并且我会罗列出来不同算法的使用限制。

之后的两讲，我们就接着今天的话题，看看除了初始化向量之外，链接模式和数据补齐方案是怎么影响对称密钥算法的安全性的？这两个问题的讨论，需要较大篇幅，不过我会带你一起分析。

Take Away（今日收获）

今天，通过解构分组算法的运算，我们讨论了影响分组算法安全性的五个关键因素。然后讨论了选择初始化向量应该注意的陷阱，也就是说，在使用对称密钥加密时，初始化向量不能重复。

最后，我们还讨论了一个不太容易受关注的问题，就是密钥是有使用次数限制的。一般的应用程序，密钥使用次数限制不是问题，但是如果你要设计一个广泛使用的协议，还是要考虑密钥这个限制的。密钥使用次数用完之前，一定要更新密钥。

今天，我们应该理解、记住：

分组算法的处理过程；

影响对称密钥算法安全性的五个关键因素；

在一个对称密钥的生命周期里，初始化向量不能重复。

思考题

今天的思考题，也是一个动手题。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索一下初始化向量的使用。看一看对于同一个对称密钥，初始化向量会不会重复，有没有可能重复。如果一个对称密钥使用了重复的初始化向量，有没有潜在的安全风险？你有没有什么建议？

欢迎在留言区留言，记录、讨论你的发现和建议。

好的，今天就这样，我们下次再聊。

09 | 为什么ECB模式不安全？

你好，我是范学雷。

上一讲，我们讨论了对称密钥分组算法的计算过程，我们找到了影响对称密钥算法安全性的五个关键因素，以及初始化向量对算法安全性的影响和选择。

不过，还有一些遗留的问题，我们没有来得及讨论，链接模式和数据补齐方案对算法安全性有什么样的影响？它们是怎么影响分组算法安全性的呢？我们又该怎么避免这些安全陷阱呢？

其实，这都是对称密钥分析的核心问题。因为，可以说，每一种链接模式、每一种数据补齐方案都有着不同的构造，当然也就对应着不同的分析办法，而且分析起来都较为复杂。

这一讲，我们先来分析链接模式对安全性的影响，同时，我们还可以借此机会研究一下 ECB 模式到底有什么问题。还记得吧？我们在开篇词提到过，它不是一个安全的加密模式。

在讲 ECB 模式之前，首先，我们先来看看链接模式是怎么一回事。

链接模式怎么连？

我们上一讲说过，链接模式指的是如何把上一个分组运算和下一个分组运算联系起来，使得上一个分组运算可以影响下一个运算。但是，这个联系是怎么建立起来的，上一个运算到底又是怎么影响下一个运算的，这个描述是模糊的。

从道理上来说，上一个分组运算的所有要素，都有可能参与到下一个分组运算里；下一个分组运算的每一个要素，都有可能接收上一个运算的一个要素或者几个要素的组合。

而在这之间，就会形成不同的分配组合，也就形成了不同的链接模式。

当然，你要知道，不是所有的组合都是安全的。其中，有些模式有严格的适用范围。超越了这个范围，这个算法就不再安全了。

我们需要特别关注这些限制，把每个模式的原理理解清楚，使用时不要掉进安全陷阱里。

ECB 模式什么样？

我们先来简单地了解一下 ECB 模式是怎么工作的。

上一次，我们讨论了影响对称密钥算法安全性的五个关键因素。但是，ECB 模式有所不同，它不使用链接模式，因此它也用不着初始化向量。

不使用链接模式，就意味着上一个分组运算不影响下一个分组运算，每一个数据分组的运算都是独立的。不需要初始化向量、每一个数据分组运算都是独立的，这特性令人振奋。

不需要初始化向量，还意味着 ECB 模式没有初始化向量管理的烦恼，有着更简单的代码。很多密码算法的接口设计和实现，为了方便使用，会使用缺省的数据。

比如说，下面的 Java 代码，使用的就是缺省的 ECB 模式。这段代码，看起来还算清爽简单：

Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);

但是，你看，下面的代码，看起来就繁琐的多。因为我们要在加密端和解密端同步初始化向量，而且它实际上的实现要更繁琐：

IvParameterSpec ivParameters = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameters);

每一个分组运算都是独立的，这更是一个充满诱惑的特性。因为，能够独立运算，也就意味着可以并行地运算，也不必按照次序运算。

可以并行运算，意味着运算效率的大幅度提高；没有次序要求，意味着每一段加密数据都可以独立地存取、解密、修改、删除。而且，添加、插入新的数据段，也不会对其他数据段产生影响。

所有的这些，都意味着良好的运算效率。这是每一个大型数据计算场景渴望的特性，比如说数据库、流媒体、视屏会议。遗憾的是，ECB 模式有致命的安全缺陷。

它的致命的安全缺陷却恰好来源于它令人振奋的特性：初始化向量的缺失和链接模式的缺失。这意味着，如果我们不能拒绝它的这些诱惑，去寻找使用替代的方案，我们就会掉进这些致命的安全陷阱。

缺失带来了什么问题？

你可能还不知道初始化向量和链接模式的缺失会带来什么问题，让我们来一起分析下。

还记得初始化向量的特点吗？如果一个加密运算缺失初始化向量，相同的明文分组就会被加密成相同的密文分组。让我们一起来看一个例子，假设有如下一段数据：

ABCDEFGHHIJKLMNO0123456789012345

当我们使用 AES-128/ECB 算法加密时，我们需要把这段数据分组成两个明文分组：

ABCDEFGHHIJKLMNO
0123456789012345

然后，我们加密这段数据，得到的密文（密钥“1234567890123456”）是（十六进制表示）:

1389AE9853633EBF3D35F28987FCD1187B4BFC89DD1700154482BC7EB686BB0E

我们可以把密文按照块大小分成两段：

1389AE9853633EBF3D35F28987FCD118
7B4BFC89DD1700154482BC7EB686BB0E

根据 ECB 加密模式的特点，我们知道第一行密文对应的数据是“ABCDEFGHHIJKLMNO”，第二行密文对应的数据是“0123456789012345”。

如果我们知道数据块对应的密文，我们就可以通过寻找重复的密文，在没有密钥，也不执行解密操作的情况下，知道对应的数据块。比如下面的密文：

7B4BFC89DD1700154482BC7EB686BB0E1389AE9853633EBF3D35F28987FCD118

对应的数据块是：

0123456789012345ABCDEFGHHIJKLMNO

你可能会有疑问，数据都是加密的，攻击者怎么会事先知道数据块和对应的密文呢？其实，互联网世界里，已知重复的、位置确定的数据非常多，HTTP 的头部数据，HTTPS 数据包的头部，URL 等都是重复频率很高的数据。

只要通过定位数据块，锚定对应的密文，就可以利用已知数据块和密文寻找、推断未知数据了。

而且，很多应用的场景，尤其是互联网的应用场景，注入特定明文数据、获取对应密文信息的攻击也是轻而易举的事情。如果攻击者没有“0123456789012345”的密文信息，他可以构造一个这样的明文，然后让密钥持有者加密，然后他就可以获得对应的密文分组。

攻击者也许并不满足于看看数据、窥视隐私。所以，ECB 模式更严重的问题，是由链接模式的缺失带来的。链接模式的缺失，会让每一个分组运算都是独立的，密文数据当然也会是独立的。

我们一起来看下面这段数据：

张三于二零二零年八月二十二日向李四借款人民币三十圆整，立此为证。

假设我们加密运算时，这段数据被分割成如下的四个分组：

张三于二零二零年
八月二十二日向李
四借款人民币三十
圆整，立此为证。

还有下面这段数据：

王二于二零二零年八月二十二日向李四借款人民币三十亿四千五百万六千圆整，立此为证。

假设在加密运算时，这段数据也被分割成如下的五个分组：

王二于二零二零年
八月二十二日向李
四借款人民币三十
亿四千五百万六千
圆整，立此为证。

如果有了明文，还有了对应的密文，我们就可以重新编排密文数据，以获得想要的结果。

如果使用了 ECB，我们可以把“亿四千五百万六千”这段明文分组对应的密文分组删除，那么解密的结果就是：

王二于二零二零年八月二十二日向李四借款人民币三十圆整，立此为证。

我们也可以把“亿四千五百万六千”这段明文分组对应的密文分组插入到第一段数据的密文数据里。那么，修改的密文数据解密结果就是：

张三于二零二零年八月二十二日向李四借款人民币三十亿四千五百万六千圆整，立此为证。

这样的结果，和原始数据相差甚远。这种攻击方式，就是常说的“分组重放”攻击。

什么时候使用 ECB 模式？

现在，你知道了 ECB 模式是一个很不安全的模式了吧，既然 ECB 模式这么不安全，为什么安全应用程序接口还要提供 ECB 模式的编程接口呢？这主要是因为，ECB 模式是分组算法的基础。

分组算法的最基本运算，就是转换一个固定长度的数据块。这个单个数据块的转换，就是 ECB 模式下的运算。有密码学专业知识的算法工程师，可以通过合理地使用 ECB 模式，来构造更复杂、更安全的算法。不过，我们不应该在一般的应用程序使用 ECB 模式。

ECB 从诞生开始，就被认为是一个不安全的加密模式，有密码学基础知识的工程师，一般不会使用它。下一节，我们会讨论一个被广泛使用的模式，以及它现在面临的挑战。

Take Away（今日收获）

今天，通过解构 ECB 模式，我们讨论了在分组运算里，链接模式和初始化向量在分组运算里承担的角色，以及初始化向量缺失和链接模式缺失带来的问题。

最后，我们还讨论了 ECB 模式的实际用途，以及一个实用性的建议：我们不应该在一般的应用程序使用 ECB 模式。

通过今天的讨论，我们要：

了解分组算法里，初始化向量缺失可能带来的问题；

了解分组算法里，链接模式缺失可能带来的问题；

知道 ECB 模式不是安全的模式，应用程序不应该直接使用 ECB 模式。

思考题

今天的思考题，可能是一个稍微需要撕扯一下的挑战题。

我们前面说过，我们不应该在一般的应用程序使用 ECB 模式。可是，如果需要加密的数据小于一个数据分组（也就是说，数据太短，不需要使用链接），那么，我们可以使用 ECB 模式吗？有什么好处？有没有风险？

欢迎在留言区留言，记录、讨论你的想法。

好的，今天就这样，我们下次再聊。

10 | 怎么防止数据重放攻击？

你好，我是范学雷。

上一讲，我们讨论了对称密钥分组算法的链接模式，从链接模式出发，我们还分析了 ECB 模式，初始化向量和链接模式的缺失导致了 ECB 模式的安全缺陷，尤其是数据重放攻击。

我们说，有密码学基础知识的工程师，都应该知道 ECB 模式的安全问题，并且不会在应用程序中使用它。这一讲，我们来讨论一个更广泛使用的加密模式，CBC 模式。

CBC 模式，可能是 2018 年之前最常用、最常见的加密模式。和 ECB 模式不同，由于初始化向量和链接模式的使用，CBC 模式解决了数据重放攻击的问题。可是，从 2018 年开始，由于它的安全问题，CBC 模式开始退出历史舞台，尽管这一进程可能需要十数年，甚至数十年。

为什么还要学习 CBC 模式？

不知道你是不是已经有了一个问题：既然 CBC 要退出历史舞台了，我们还学习它干什么呢？

第一个原因，CBC 的退出进程可能需要十数年才能完成。你现在工作的项目种，可能还存在 CBC 模式的大量使用。我们学习了 CBC 模式，有助于你解决现存项目的安全问题。

第二个原因，学习针对 CBC 的攻击方案， 是我们深入理解加密算法安全问题的最好的切入点。了解这些安全缺陷和攻击方案，有助于你更好地使用密码学的算法。因为，这些缺陷也可能换个面孔，出现在应用程序层面。如果你能够说清楚 CBC 模式的攻击办法，也就意味着你已经试着走入了算法的细节。

第三个原因，也是最重要的原因，就是我们要进一步地理解初始化向量和链接模式对加密算法的影响。学习 CBC 模式会为我们将来讨论更高级的协议和更安全的算法打下基础。

那么，CBC 模式是什么样子的？它是怎么解决数据重放攻击的？它存在哪些安全陷阱？这是我们这一次要解决的问题。

CBC 模式什么样？

和其他模式不同的是，在 CBC（Cipher Block Chaining）模式中，明文分组在加密之前，要与前一组的密文分组进行异或运算，异或运算的结果会参与加密函数的运算。

也就是说，上一次的密文分组要参与下一次的加密运算，每一个密文数据不仅依赖于它对应的明文分组，还依赖于上一次的密文分组。

这样的话，每一个密文分组，都依赖于前面所有的明文分组，包括初始化向量。

所以，我们能够知道，CBC 模式是需要初始化向量的。最显而易见的原因，就是第一个明文分组还不存在所谓的“上一个密文分组”。所以，我们需要一个外部引入的初始化向量来替代“上一个密文分组”参与运算。

不过，我们需要注意的是，在加密过程中，加密函数的输入数据是明文分组（Mi）和上一次的密文分组（Ci-1）的异或运算的结果（Mi ^ Ci-1）。

在解密过程中，解密函数的输出数据，也是明文分组和上一次的密文分组的异或运算结果（Mi ^ Ci-1）。我们要是想得到解密的明文分组，就需要把明文分组和上一次的密文分组分离开来。

我们现在可以确定的是，上一次的密文分组（Ci-1）是已知的。所以，只要我们把上一次的密文分组和解密函数的输出数据进行异或运算，就把明文分组分离出来了。

(Ci-1) ^ (Mi ^ Ci-1) = Mi

所以，我们可以发现，对于解密过程来说，如果我们把解密函数的运算结果与上一次的密文分组进行异或运算，就可以获得对应的明文分组。

不难想象，在解密过程中，我们要想获得第一个明文分组，用来替代“上一个密文分组”的初始化向量就必须要参与解密的过程。也就是说，加密过程的初始化向量和解密过程的初始化向量必须是一样的，否则，我们就没有办法得到第一个明文分组。

注意一点，初始化向量只影响第一个明文分组，并不影响后续的解密过程和明文分组。

类似地，一个密文分组，只影响它的下一个明文分组，并不影响更后面的解密过程和明文分组。

而在加密过程中，每一个密文分组，都依赖于前面所有的明文分组，包括初始化向量。

所以，我们在这里要注意的就是加密过程和解密过程的区别。这是一个重要的特征，我们先在这里打个伏笔，后面我们会再讨论这个特征有什么用。

总的来说，理解 CBC 模式，我们要把握以下三个关键点：

加密和解密要使用初始化向量；

加密和解密的初始化向量是等同的；

上一次的密文分组参与下一次的加密和解密运算。

初始化向量需要保密吗？

我们讨论过初始化向量的选择问题，就是在一个对称密钥的生命周期里，初始化向量不能重复。

如果每一次运算，初始化向量都能不重复，即使是相同的明文数据，它的加密结果也是不同的。但是，如果初始化向量重复使用，相同的明文就会有相同的密文。重复使用的初始化向量，会消解密文反馈的作用，使得 CBC 模式和 ECB 模式一样脆弱。

所以，初始化向量的唯一性在加密运算的安全性中至关重要。

那你会问了，既然初始化向量这么重要，那我们需要对它进行保密吗？初始化向量并不需要保密。如果你对这一点有疑问，不妨换个角度想一想：每一个分组加密的初始化向量都是上一次加密运算得到的密文分组，而密文分组是可以公开的信息。

初始化向量不需要保密，这是我们要打的第二个伏笔。

异或运算会不会有问题？

我在上面的讲解中提到了异或运算，其实，它在密码算法里有广泛的应用，为什么它如此广泛？

第一个原因是异或运算是按位运算，所以在相同的计算环境下，异或运算时间只和数据的位数相关，和数据的实际数值无关。放在密码学算法的世界里，如果运算时间和实际数值无关，那简直再好不过了。

换句话说，如果运算时间和数据数值相关，而且别人还了解到这种相关性，他就可以通过统计学的方法，通过观察、测算运算时间，找到运算时间和数据数值之间的关联，来破解密码。

第二个原因同样是按位运算，在相同的计算环境下，异或运算的复杂度，也就是需要的算力，只和数据的位数相关，和数据的实际数值无关。而且，一个运算需要的算力，在计算机环境中，可以通过占用的 CPU 周期数，以及消耗的内存空间来衡量。

同理，如果占用的 CPU 或者消耗的内存和数据数值相关，别人就可以通过统计学的办法，然后观察 CPU 的占用、电力消耗或者内存的消耗，来破解密码。一般来说，这种相关性，也会影响运算时间，从而使得基于测算运算时间的攻击方式同样有效。

不光如此，如果运算的复杂度和数据数值相关，密码破解的办法可就是千奇百怪的了。记录、测算计算机的噪音、温度、辐射、反应时间等等，都有可能成为有效的攻击手段。

如果让一个一流的黑客，拿着手机进入数据中心，录一段服务器发出的声音，说不定你的服务器就被攻陷了。之所以没有说一定会被攻破，是因为近几年的密码学进展，已经发展出了具有防范能力的算法和实现。

但是，如果你的服务器使用的是十年前的技术和软件，黑客得手的概率还是有的。我们后面会讨论这些新技术和新算法。

第三个原因和异或运算的运算特点有关，也就是相同的数据归零，不同的数据归一。

归零律：如果两段数据完全相同，它们的异或运算结果，就是每一位都是零的数据；

恒等律：如果一段数据和一段全是零的数据进行异或运算，前一段数据中是零的位运算后还是零，是一的位运算后还是一。也就是说，和零进行异或运算，不改变原数据的数值。

正是异或运算的归零律和恒等律，CBC 模式才能成立，解密才能进行。这两个性质，还使得解密运算和加密运算具有相同的运算效率。

然而，CBC 模式的主要安全问题，也来源于异或运算的这两个性质。

如果两段数据中只有一位不同，它们的异或运算结果，就是只有这一位的数据是一，其他的数据都是零。那是不是我们就可以通过构造明文分组或者密文分组，一次改变一位数据，然后把数据交给加密运算或者解密运算来处理，通过观察加密或者解密的结果展开攻击了？

比如说，一个 128 位的密钥，它的强度能承受 2^128 次的运算，是一个强度的指数级别的量级。

如果我们一次改变一位数据的攻击方式得逞，最多需要 128 次的运算；

如果我们一次只能观测一个字节，一次一位的改变需要 2^8 = 256 次，这样的攻击方式得逞，最多需要 255 * 16 = 4080 次的运算。

这样的运算强度，和设计的理论值 2^128 相差太远了，一次有效的破解也就是分分钟的事情。

还别说，这样的攻击方式在实践中真的是可行的。这种攻击方式，把 CBC 模式变成了一个充满陷阱的模式。用的好，它就是安全的；用的不好，它就会惹来麻烦。这实在不符合密码算法要皮实、耐用的要求。

阻断一个攻击的方式之一，就是破坏攻击依赖的路径或者条件。对于上面的攻击方式，其实只要攻击者没有办法一次改变一位数据或者少量的数据，这样的攻击就可以被有效破解了。

也就是要保证攻击者在展开攻击的时候，没有办法一次改变不少于一个数据分组的数据。对于 AES 来说，数据分组大小是 128 位，攻击者需要运算 2^128 次，才可以攻击得逞。

计算量这么大，攻击者的攻击方式就无效了。那我们怎么做才能让攻击者没办法呢？

密文分组、密钥、加密算法、解密算法，这些都是固定的数据或算法，没有考量的空间。剩下的变量，就只有明文分组和初始化向量了。要想解决掉这个安全问题，该怎么控制明文分组和初始化向量？异或运算又是怎样带来麻烦的？

要想深入地了解这些问题，有点烧脑。下一次，我们集中精力来讨论、分析其中的细节和办法。

密钥少一位会有影响吗？

不知道你有没有注意到，我们上面的讨论，提到了数据的位数。

因为分组加密是按照固定的分组进行加解密运算，所以每一次的分组运算，数据的位数都是固定的。比如，AES 算法的分组大小都是 128 位。所以，我们不用担心分组运算的数据位数的变化。

在分组运算中，初始化向量、密文分组和明文分组密钥的数据位数也都是固定的。所以，我们也不需要担心它们的位数的变化。加密算法和解密算法不涉及数据位数，所以我们也不担心算法。剩下的一个变量，就是密钥了。密钥的位数会变化吗？密钥的位数变化有影响吗？

一般来说，我们也不太关心密钥的位数变化，密钥少一位似乎也不是什么无关紧要的事情。所以，出于互操作性的考虑，很多标准和协议（包括应用最广泛的 TLS 1.2 协议）需要把密钥的高位的零清除掉，然后再参与运算。

原来 128 位的密钥，可能就被清除成了 127 位或者 126 位的密钥了。2018 年发布的的 TLS 1.3 版本，不再需要清除密钥高位的零。少一位密码，当然会带来计算性能的差异，以及由此引发的计算时间偏差。可是，似乎 2020 年之前，没有人担心这件事。

直到 2020 年 9 月 8 日，当我正在写这一篇稿的时候，一个名字叫做“浣熊攻击”的安全研究成果发布了。浣熊攻击可以利用密钥高位清零造成的运算时间差，通过观察、测算运算时间，运用统计学的技术破解运算密钥。这实在是一个了不起的发现。

目前来看，这种攻击方式还比较复杂，不容易执行。但是，一旦发现攻击方法，如果业界没有采取及时的措施，攻击技术的改进速度是惊人的。“浣熊攻击”出现，再一次敲了敲大门，警告我们要尽量避免计算时间偏差和计算算力偏差，谨慎地处理不可避免的计算时间偏差和算力偏差。

Take Away（今日收获）

今天，通过解构 CBC 模式，我们讨论了在分组运算里，一个典型的链接模式是什么样子的，以及重申了初始化向量的唯一性要求。使用唯一的初始化向量和恰当的链接模式，可以帮助我们防范数据重放攻击。

还有，通过异或运算和密钥位数的讨论，我们要小心计算时间偏差和计算算力偏差对算法安全性的影响。一般来说，这是一个特别容易忽视的问题。不仅仅是密码学算法，对所有私密数据的运算，都要小心处理计算时间偏差和计算算力偏差。否则，都有数据泄漏的危险。

另外，为了后面更进一步地讨论 CBC 模式的安全问题，我们还在这一次埋了不少的伏笔暗线，比如，初始化向量不需要保密，异或运算的特点等。

通过今天的讨论，我们要：

理解 CBC 模式的三个关键点。

了解计算时间偏差和算力偏差对算法安全性的影响。

思考题

今天的思考题，是一个动手题，也是一个简单的密码算法漏洞扫描的思路。

通过上面的讨论，我们知道密钥的位数很关键，一位也不能多，一位也不能少。找一个你熟悉的密码算法库，这个算法库可以是 Java Script 的，也可以是 Java 的，也可以是你熟悉的项目使用的算法库。

然后，调用它的对称密钥生成接口，试着产生很多 128 位的密钥。你看一看，有没有可能返回 127 位或者 129 位的密钥。 如果你找到了不是 128 位的密钥，这个算法库就有潜在的安全问题。

如果你恰好学过统计学，还能使用统计学的软件，你可以试着多做一道思考题。我假设你知道 RSA 非对称密钥算法，也了解它的调用接口。同样的，找一个你熟悉的 RSA 算法实现，生成一对 1024 位 RSA 非对称密钥，用公钥加密大量的 1024 位的不同数据，然后用私钥解密这些数据，统计解密消耗的时间。

如果解密时间不是大致相同的，这个 RSA 实现就是有问题的。破解起来可能就是分分钟的事情。这是一个让我们了解计算时间偏差和计算算力偏差的练手题，也是个常见的分析 RSA 实现漏洞的攻击办法。

欢迎在留言区留言，记录、讨论你的发现。

好的，今天就这样，我们下次再聊。

11 | 怎么利用解密端攻击？

你好，我是范学雷。

上一讲，我们讨论了对称密钥分组算法的 CBC 链接模式，还提到了异或运算的好处，以及它的风险。但是，我们又留了一个小尾巴，异或运算是怎样带来麻烦的？它是怎么影响 CBC 链接模式的安全性的？我们又有什么有效的办法可以规避异或运算的风险？

这两次讨论，有点烧脑，我们姑且把它当做一次打怪升级的过程。如果弄清楚这些问题，你就可以更得心应手地使用分组算法，而且毫无疑问地超越了大部分人对对称密钥分组算法的理解。

鉴于这两次讨论的难度，我觉得最好的学习方式，就是拿上纸笔，跟着我的思路，自己画一画攻击的流程。学习一段内容，就画一画这一段的思路，扎扎实实把这一段理解下来。

其实，这也是我们学习、理解所有密码学算法攻击的一个办法。密码学算法的攻击，大部分都超乎想象，意想不到。读一段、画一段，理解一段，是一个实用的方法，也是我自己使用的方法。

好，你准备好了吗？我们开始了。

怎么利用解密端攻击？

还记得 CBC 模式吗？攻击者既可以攻击它的解密端，也可以攻击它的加密端。针对解密端的攻击，最常见的方式，就是给定密文数据，观测解密运算是成功还是失败。

那么，解密端是怎么知道解密是成功还是失败呢？到目前为止，在我们已经讨论过的密码学技术中，我们唯一能够利用的，就是数据补齐方案。我们先来看看，数据怎么补齐？

数据怎么补齐？

简单来说，CBC 模式的补齐数据，就是一个数据分组缺失的数据的长度。比如，对于 AES 来说，数据分组是 16 个字节。如果一段数据只有 15 个字节，补齐的数据就是一个 1；如果有 14 个字节，补齐的数据就是两个 2；以此类推，如果只有一个字节，补齐的数据就是十五个 15 。

不过，这个数据补齐方案有明显的缺陷。

当数据分组最后一个字节是 1，解密端不太好判断这个字节到底是补齐数据，还是有效数据。所以，很多标准和协议使用最后一个数据分组的最后一个字节来标明补齐数据。

如果一段数据只有 15 个字节，补齐的数据就只有这个标识字节，数值为 0，表示没有额外的补齐数据；如果有 14 个字节，标识字节为 1，表示还有一个字节的额外补齐数据，额外的补齐数据的数值也是 1；如果有 13 个字节，标识字节为 2，表示还有两个字节额外补齐数据，额外的补齐数据的数值也是 2；以此类推。

需要注意的是，如果一段数据有 16 个字节，也就是刚好一个数据分组大小，这时候，就需要一个全是补齐数据的数据分组，它的每一个字节都是 15。

为了方便，下面的讨论中，我们都使用有标识字节的数据补齐方案，除非特别声明。

补齐怎么校验？

那你会不会有疑问，我们如何在解密端，判断一个数据分组中的数据是不是补齐数据？

可以检查最后的几位数字是不是相同。比如说，如果最后一位是 2，但是倒数第二位和倒数第三位不是 2，解密就失败了。失败的原因，可能是由于篡改的密文分组，或者是篡改的初始化向量，或者是篡改的密钥。不管哪种原因，都可能导致补齐数据的校验失败。

那我们怎么利用数据补齐方案展开攻击呢？

攻击怎么展开？

我们回顾一下 CBC 模式的解密过程。一个密文分组的解密，需要如下的输入数据：

上一次的密文分组 Ci-1；

这一次的密文分组 Ci；

加密和解密共享的密钥 K。

这一次的密文分组 Ci 和密钥 K，通过解密函数的运算，可以产生一个中间结果 Ti，也就是解密函数运算的中间结果 Ti；然后，解密函数运算结果 Ti 和上一次的密文分组进行异或运算，获得明文分组 Pi，Pi = Ci-1 ^ Ti。

接下来，我们利用数据补齐方案的攻击，通过修改、构造最后一个密文分组 Cn 的前一个密文分组 Cn-1，也就是倒数第二个密文分组，然后让解密端解密构造后的密文数据，观察修改了密文数据的解密能不能成功。

那倒数第二个密文分组该怎么构造呢？首先，修改该密文分组的最后一个字节，该字节可以从零开始。毫无疑问，解密端并不能正确解密该密文分组，得到的明文分组也不是期望的明文数据。

但是，正像我们前面讨论的，解密端并没有办法知道这一个密文分组解密失败。要判断解密是否成功，还需要判断最后一个明文分组的补齐数据是不是符合规范。

如果我们修改倒数第二个密文分组的最后一个字节，那么解密后的最后一个明文分组应该只有最后一个字节受到影响。如果这个解密后的字节是零，其他的数据会被解读为有效数据，这样就会通过补齐数据的校验。如果这个解密后的字节是其他数值，补齐数据的校验通过的概率就很小。实际的攻击过程，这么一点小小的概率也可以优化过滤掉。

如果解密后的这个字节不是零，那么，补齐数据的校验就会失败，解密端就会报错。观察解密端的行为，攻击者就能了解到，这个构造的字节没有通过解密过程。如果观察到失败，攻击者就会调节最后一个字节的数值，比如数值加一，然后再次让解密端尝试解密。

一个字节只有 256 种可能性，最多尝试 255 次，一定会有一次尝试，补齐数据是零。

当攻击者观察到解密成功时，他能够掌握哪些数据呢？他能够直接得到的数据包括：

未修改密文分组的最后一个字节的数值（Cn-1[15]）；

修改后密文分组的最后一个字节的数值（Cn-1'[15]）；

解密端明文分组的最后一个字节的数值（Pn'[15]，也就是 0x00）。

通过计算，他还可以得到解密函数的运算结果的一个字节：

解密函数运算结果的最后一个字节的数值（Tn[15]）。

由于明文字节（Pn'[15]）是密文字节（Cn-1'[15]）和解密函数运算中间结果字节（Tn[15]）的异或运算结果，那么知道了明文字节（Pn'[15]）和密文字节（Cn-1'[15]），使用异或运算的归零律和恒等律，计算出解密函数运算中间结果字节（Tn[15]）就是一件简单的事情了。

> Pn'[15] = Tn[15] ^ Cn-1'[15]

> =>

> Pn'[15] ^ Cn-1'[15] = Tn[15] ^ Cn-1'[15] ^ Cn-1'[15] = Tn[15]

既然知道了解密函数运算中间结果字节 Tn[15]）和原始密文分组的字节，真正的明文字节也就被破解出来了。

> Pn[15] = Tn[15] ^ Cn-1[15]

你看，非常简单的运算，运算次数不多于 255 次，攻击者就破解了一位字节。接下来的目标，就是破解倒数第二位字节。

破解倒数第二位字节，要使用的补齐数据是一（0x01）。首先，既然攻击者已经知道了最后一个字节的明文、密文以及解密函数运算的中间结果，他就可以计算、修改上一次的密文分组的最后一个字节，使得解密后的明文分组的最后一个字节是一。

> Tn[15] ^ Cn-1'[15] = 0x01

> =>

> Tn[15] ^ Cn-1'[15] ^ Tn[15] = 0x01 ^ Tn[15]

> Cn-1'[15] = 0x01 ^ Tn[15]

这样，解密端就确认了最后一个字节是一，也就是补齐数据的标识字节是一。只要倒数第二个字节也是一，补齐数据的检验就通过了。倒数第二个字节破解方式和第一个字节的破解方式相同。

每个字节的破解，运算次数都不会多于 255 次。从倒数第一位开始，依次破解，直到第一个字节破解结束，总的计算量是 255 乘以字节数。对于 AES 而言，一个数据分组有 16 个字节，破解计算只需要 255 * 16 = 4080 次。

这么小的破解计算量，是没有什么安全性可说的。预测补齐数据的这一类破解方案，都可以叫做“补齐预言攻击”。2002 年，塞尔吉·沃德奈（Serge Vaudenay）公布了补齐预言攻击。

有一些 2002 年之前制定的安全协议，不能很好地应对这种攻击方案。相应地，也有一些成功的破解方案。那么，2002 年以后的年月呢？CBC 模式该怎么规避补齐预言攻击？

有效的方式有两个。第一个方式，也是最有效的方式，就是和数据完整性校验的算法结合起来，而不是依赖补齐数据是否合法，来判断解密数据是不是有效。这种方式，我们讨论数据完整性校验算法的时候，还会再讨论。另外一种方式，就是在初始化向量上想办法。

初始化向量就用一次吗？

初始化向量，现在你应该很熟悉了，就是加密或者解密运算开始的时候使用的向量。这个变量不是就使用一次、改变一次而已吗？怎么还使用它来阻断补齐预言攻击呢？

如果这个变量就使用一次、改变一次，它的确起不到作用。我们要做的，就是要改变这种一次性使用的方式，变成多次使用的方式。是不是听起来有点难度？是有难度的。

如果加密端和解密端每次运算（一次运算可以包含多个数据分组）都重置初始化向量，并且使用不一样的初始化向量。只要使用了不一样的初始化向量，每一次运算的最后一个密文分组都是不同的。这样，就不能重复地使用这个密文分组反复运算，从而切断了补齐预言攻击的攻击路径。

稍微有点麻烦的是，解密端怎么才能判断每一次运算都使用了不同的初始化向量。这不是一个容易的事情。所以，如果仅仅依靠一次运算一个初始化向量的办法，也很难有成熟的解决方案。

一次运算一个初始化向量的办法，除了可以用来阻断补齐预言攻击之外，它还有一个很有吸引力的特点。不过，让我们下一次再接着讨论。

Take Away（今日收获）

今天，通过讨论怎么在 CBC 模式的解密端，利用数据补齐方案，来展开补齐预言攻击，我们知道了数据补齐方案可能存在的缺陷。

另外，我们还讨论了阻断补齐预言攻击的办法，那就是，每一次加密 / 解密计算，都使用初始化向量，而不是仅仅就使用一次初始化向量。

通过今天的讨论，我们要：

知道 CBC 模式存在补齐预言攻击；

知道使用不同初始化向量的来阻断补齐预言攻击。

思考题

今天的思考题，我们去看一看协议的设计。

在 TLS 1.2 的设计中，使用 CBC 模式加密的数据包是由一个明文的初始化向量和密文的加密数据组成的。明文的初始化向量和密文的加密数据，可以通过不安全的网络，传递给信息接收方。

第一个问题是：为什么我们可以传递明文的初始化向量？

第二个问题是，为什么要传递初始化向量？

另外，如果你仔细看下面的数据结构，就会注意到，在 TLS 1.2 的设计里，待加密的数据块，已经补齐到算法要求的数据块的整数倍了。这似乎也就意味着，上一个数据包和这一个数据包，以及这一个数据包和下一个数据包，都可以无缝地衔接，而不需要使用数据补齐，也不需要每次都使用一个初始化向量。

既然已经补齐了，为什么还要每一次加密都使用一个初始化向量？这是第三个问题。

理解了这三个问题，你就算学会使用 CBC 算法了。CBC 算法要被扫进垃圾箱的主要原因，就是因为这个算法太难用对了，虽然它是用途最广泛的加密模式。如果你可以用对它，在很多场景，它还是能够再延续使用一段时间，等待你升级到更皮实的算法。

struct {
  opaque IV[SecurityParameters.record_iv_length];
  block-ciphered struct {
      opaque content[TLSCompressed.length];
      opaque MAC[SecurityParameters.mac_length];
      uint8 padding[GenericBlockCipher.padding_length];
      uint8 padding_length;
  };
} GenericBlockCipher;

欢迎在留言区留言，记录、讨论你的理解和看法。

好的，今天就这样，我们下次再聊。

12 | 怎么利用加密端攻击？

你好，我是范学雷。

上一讲，我们讨论了怎么利用解密端攻击 CBC 模式的对称密钥分组算法。那么，加密端的攻击方案是怎么进行的？有什么办法可以规避加密端的攻击？这是我们这一次要解决的问题。

虽然我们之前说，CBC 模式应该退休了，但是现实中，它还有广泛的应用。我们花了很多篇幅来讨论 CBC 模式，除了提醒你 CBC 模式的缺陷外，还希望你了解缺陷修补的办法，这样你就可以修复现有的应用了。

这一次的讨论，比起上一次，稍显烧脑。因为，这是一个相对较新的攻击技术。不过，我可以保证，如果你了解了这种攻击技术，将会有一个巨大的收获。

怎么利用加密端攻击？

我们先来看看是怎么利用加密端攻击的？其实，CBC 模式针对加密端的攻击，和针对解密端的攻击方案一样，最常见的攻击方式也是通过异或运算展开的。

攻击面应该怎么选？

要想了解针对加密端的攻击方案，我们先来回顾一下 CBC 模式的加密过程。在这个过程中，一个明文分组的加密，需要如下的输入数据：

上一次的密文分组 Ci-1；

这一次的明文分组 Pi；

加密和解密共享的密钥 K。

这一次的明文分组 Pi 和上一次的密文分组 Ci-1 进行异或运算，获得中间结果 Ti。

异或运算获得的中间结果 Ti，Ti = Ci-1 ^ Pi

然后，异或运算获得的中间结果 Ti 和密钥 K，通过加密函数的运算，产生密文分组：

密文分组 Ci

而加密端的异或运算，发生在明文分组和上一次的密文分组（对于第一次运算，上一次的密文分组就是初始化向量）之间。针对加密端的攻击，同样，我们也是在明文分组和上一次的密文分组或者初始化向量上想办法。

攻击理论上可行吗？

假设目标明文分组是 Pi，但是攻击者并不知道 Pi 的明文内容，而他攻击的目的就是解密出 Pi 的明文数据。由于密文数据不是保密的，所以攻击者可以知道所有的密文分组，包括参与 Pi 加密运算的上一次密文分组 Ci-1 和这一次的密文分组 Ci。

如果攻击者可以使用加密接口，攻击者就可以构造明文分组，然后调用加密运算，观测加密运算后的密文分组。如果攻击者观测到密文分组和目标明文分组对应的密文分组相同，那么他就可以确定目标明文分组的具体数据了。

那明文分组该怎么构造呢？像解密端的攻击方案一样，我们还是要利用异或运算的归零律和恒等律。假设已经知道了 Pi，当前的加密运算需要使用上一次的密文分组 Cj-1，如果攻击使用的明文可以构造成下面的形式，那么攻击就能够成功：

Pj = Pi ^ Ci-1 ^ Cj-1

加密运算后的结果，就是目标明文分组对应的密文分组。

Tj = Pj ^ Cj-1

= Pi ^ Ci-1 ^ Cj-1 ^ Cj-1

= Pi ^ Ci-1

= Ti

=>

Ek（Tj）= Ek（Ti）

=>

Cj = Ci

Tj ^ Ci-1 = Ti ^ Ci-1 = Pi ^ Ci-1 ^ Ci-1 = Pi

Pi = Tj ^ Ci-1 = Pj ^ Cj-1 ^ Ci-1

假设终归是假设，攻击者要是知道明文分组，也就不需要实施攻击了。似乎，这个美好的运算并不能够执行。对于 AES 算法来说，一个明文分组有 16 个字节，也就是 128 位。如果一位一位地尝试，需要 2^128 次运算。这种数量级的运算，现在的计算能力还是无能为力的。

攻击实践上可行吗？

那么，既然完整破解有 16 个字节的明文分组不可行，能不能只破解一个字节？

怎么破解一个字节呢？我们还要继续从假设开始。假设攻击者已经知道了明文分组里的 15 个字节，只剩下最后一个字节未知。攻击者能不能破解出这个字节？这样的破解太简单了。

攻击者只需要改动最后一个字节，按照上述的构造明文分组的方式，他最多尝试 255 次，最后一个字节就可以破解了。原则上，只要有一个字节能够被破解，这个算法就算是被破解了。

不过，我们还需要验证这个假设有没有实际的可能性，也就是说，一个明文分组里，攻击者有没有可能会知道 15 个字节，只有 1 个字节是未知的？

遗憾的是，网络里的数据，大部分都是格式化的，比如 HTTP 的数据。下面的数据是我在写这些文字的时候，从 geekbang.org 登录页面记录下来的 HTTP 请求的头部数据。

这里面的数据，除了 Cookie 之外，大部分都是重复的数据。

POST /account/sms/login HTTP/1.1
Host: account.geekbang.org
Connection: keep-alive
Content-Length: 371
sec-ch-ua: "\\Not;A\"Brand";v="99", "Google Chrome";v="85", "Chromium";v="85"
Accept: application/json, text/plain, */*
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36
Content-Type: application/json
Origin: https://account.geekbang.org
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://account.geekbang.org/signin
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: <deleted>

以及 HTTP 的请求内容。请求的内容，除了电话号码（cellphone）和手机验证码之外（code），其他的数据也都是重复的。

{"country":86,"cellphone":"12345678901","code":"123456","ucode":"","platform":3,"appid":1,"remember":1,"source":"","sc":{"uid":"","report_source":"Web","utm_identify":"","utm_source":"","utm_medium":"","utm_campaign":"","utm_content":"","utm_term":"","share_code":false,"original_id":"1746eb1327656f-05d10c17083f0f-f7b1332-2073600-1746eb132773ba","refer":"极客时间"}}

所以，攻击者知道 15 个字节，甚至整个明文分组，这种情况在实际中，是经常出现的。

我们真正关心的是敏感数据，比如手机验证码，是不是能够被破解。那么，首先，我们要了解的是，攻击者能不能破解手机验证码的第一个字节呢？

如果手机验证码的第一个字节刚好是一个明文数据分组的最后一个字节，前面我们已经讨论过了，这个字节就是可以破解的。由于手机验证码只能是数字，破解这个字节最多需要 9 次计算。比起 255 次，9 次计算就更简单了。

如果手机验证码的第一个字节不是明文数据分组的最后一个字节，该怎么办？思路也很简单粗暴，只要我们想办法把手机验证码的第一个字节挪到明文数据分组的最后一个字节就行。

该怎么操作呢？也很简单，通过更换顺序、删减或者增加手机验证码的前面的字节，就可以了。

既然可以想办法把手机验证码的第一个字节挪到明文数据分组的最后一个字节，当然也可以想办法把手机验证码的第二个字节、或者第三个字节，或者任意一个字节挪到明文数据分组的最后一个字节。

每当这个字节是明文数据分组的最后一个字节时，最多 9 次就可以破解这个字节了。一个 6 位数字的手机验证码，攻击者破解起来最多需要 6 * 9 = 54 次。

是的，如果加密使用的是 CBC 模式，54 次运算，6 位数的手机验证码就可以破解出来了，

2011 年 9 月，两位天才般的研究人员（Juliano Rizzo，Thai Duong）公开了上述的攻击方案。并且表示，只要给他们几分钟时间，他们就可以利用该漏洞入侵你的支付账户。他们给这个攻击技术取了一个超酷的名字，叫做 BEAST。

2011 年 9 月之前的岁月，CBC 模式一直是加密算法的主流模式，几乎所有的网络加密都选用 CBC 模式。2011 年以后，CBC 模式就开始正式办理退休手续了。新的协议或者应用，不应该继续使用 CBC 模式了；现有的协议或者应用，也需要采取措施防范 BEAST 攻击。

有什么防范措施？

那现有的协议或者应用，要怎样采取措施来防范攻击呢？

其实，防范措施还是要在初始化向量上想办法。BEAST 攻击起作用的关键，就是要使用上一次加密运算的最后一个密文分组。

如果这一次的加密运算不使用上一次加密运算的数据，BEAST 攻击就无法运算了。该用什么替换上一次加密运算的最后一个密文分组呢？你可以先思考一下。

和防范补齐预言攻击的办法一样，替换的办法就是每一次加密运算，都使用不同的初始化向量。

比较麻烦的是，对于每一次解密运算，解密端需要加密端使用的初始化向量，然后才能执行解密运算。可是，初始化向量的同步不是一件容易的事情，特别是在数据包可能被有意无意丢弃的环境下。

有没有改进的防范措施？

一个改进的办法，就是继续使用上一次加密运算的最后一个密文分组，同时把每一次运算的初始化向量当做第一个明文分组来处理。

这个办法的好处，就是解密端不需要知道加密端选择的初始化向量，就可以执行解密运算。这个办法的坏处，就是解密端需要丢掉初始化向量这一段数据，不能把它当做应用数据来处理。

如果新的应用不应该再使用 CBC 模式，那么现在推荐使用的加密模式是什么呢？在讨论这个新模式之前，我们还需要补充一块知识，那就是消息验证。下一次，我们讨论消息验证的机制；然后我们就有足够的知识去了解这个新的加密模式了。

Take Away（今日收获）

今天，我们讨论了怎么在 CBC 模式的加密端，通过挪移，重构明文分组，来展开攻击；以及针对攻击的防范措施。

除了知道加密端攻击手段和防范措施之外，我们还要了解这种挪移，重构明文分组的攻击办法。这种攻击办法自 BEAST 攻击发布以来，已经被成功运用于很多密码学算法的破解。如果你需要设计应用协议，要小心应对明文数据的挪移，以及重构会不会构成威胁的问题。

今天，我们应该：

知道 CBC 模式存在 BEAST 攻击；

知道使用初始化向量来阻断 BEAST 攻击。

思考题

今天的思考题，稍微有点和以前不同。这一次，我们去挑战一下怎么给标准协议打补丁。

标准协议一旦成为标准，也就意味着它的行为模式有了规范，不能随意更改。我们假设，一个标准协议已经使用了 CBC 模式加密它的通信数据，而且只有第一次加密运算使用了初始化向量。

后续的加密运算，都不再使用初始化向量。也就是说，这个协议存在安全漏洞，是 BEAST 攻击的目标。毫无疑问，由于是现成的标准，我们不能够修改这个协议，让每一次的加密运算都使用一个不同的初始化向量。

那么，还有没有办法修复这个协议的漏洞，使得修复后的协议实现，还能够和旧的协议实现通信，而没有明显的兼容性和互操作性问题？

这个问题稍微有点难度。在 BEAST 攻击发现之前，业界已经知道 CBC 加密端攻击存在理论上的可能性。BEAST 攻击的出现，简化了攻击方案，使得 CBC 加密端的攻击成为现实。

业界尝试了很多年，都没有找到理想的办法；BEAST 攻击发现之后，也经过很长一段时间，才找到了解决的办法。最后的解决方案，简简单单又出乎意料。

你来试一试，能不能找到这个简单的方案，或者有没有更好的解决方案？

欢迎在留言区留言，记录、讨论你的发现。

好的，今天就这样，我们下次再聊。

13 | 如何防止数据被调包？

你好，我是范学雷。

还记得我们在前面讨论过 CBC 模式补齐预言攻击吗？当时，我们谈到了一个问题，就是解密端是无法判断解密得到的数据是发送者发送的数据，还是被人掉了包的数据的。

这就好比，牛郎要送的信是，“七夕今宵看碧霄，牛郎织女渡鹊桥”。织女拆开一看，却是一堆乱码，或者是变成了“我断不思量，你莫思量我。将你从前与我心，付与他人可”。

如果这封信真的变成了一堆乱码，就意味着信息没有被有效地送达，这样会给织女愁上加愁；而如果内容是“我断不思量”，简直就是一支穿心箭，这可一点都不好玩。

那么，织女看到“我断不思量，你莫思量我。将你从前与我心，付与他人可”的信件，除了靠坚贞不渝的信念这个不靠谱的办法之外，她有没有其他的办法来判断这是一封伪造的信件？牛郎除了坚信织女有坚贞不渝的信念之外，他有没有其他的办法来预防信件的伪造？

其实，解决问题的思路也很直观，就是要能够验证发送的信息。牛郎发送信息时，心里要想着意外情况，要给织女提供验证信息的办法。织女收到信息时，要有办法验证信息的真伪，不能只依靠心里的信念。

那么，我们今天这一讲，就来分析一下如何防止数据被调包这个问题。

怎样有效地验证一段信息？

首先，我们来分析下，要想有效地验证一段信息，需要满足什么条件呢？

第一个条件就是，我们要有额外的信息。只有要验证的信息本身，是没有办法验证这个信息的。也就是说，信息本身不能自己验证自己。这个额外的信息，我们暂且称之为验证信息。

第二个条件就是，验证信息和待验证的消息之间要有关联。如果没有关联，也就意味着如果我们替换掉待验证的信息，验证信息并不受影响，这显然起不到验证的作用。

如果待验证的信息有变动，验证信息也要变动，而且验证信息的变动结果要不可预测。如果可以人为构造一段信息，它的验证信息和待验证的信息是一样的，也起不到验证的效果。

第三个条件，就是验证信息的计算要快，数据要小。不知道这一点，你能不能理解？这点说的就是，计算效率要高，要不然验证信息的实用和推广价值就会大打折扣。

那么，有同时满足上述三个条件的一个方案吗？消息验证码（Message Authentication Code, MAC）就是最常用的满足上述三个条件的一个方案。

消息验证码是怎么工作的？

既然消息验证码可以同时满足我们的三个条件，那么消息验证码是怎么工作的？

首先，我们来看使用消息验证码的前提，就是信息的发送方和接收方要持有相同的密钥，这和我们前面讨论的对称密钥的条件是一样的。能够使用对称密钥的场景，都能够满足这个前提。

另外，信息的发送方和接收方要使用相同的消息验证函数。这个函数的输入数据就是对称密钥和待验证信息。信息的发送方使用消息验证函数，可以生成消息验证码。

接下来，信息发送方把待验证信息和消息验证码都发送给信息接收方。信息接收方使用相同的消息验证函数和对称密钥，以及接收到的待验证信息，生成消息验证码。

然后，信息接收方对比接收到的消息验证码和自己生成的消息验证码。如果两个消息验证码是一样的，就表明待验证信息不是伪造的信息。否则，待验证信息就是被篡改过的信息。

这听起来是一个不错的方案。不过，消息验证函数需要使用对称密钥，输入任意大小的数据，输出为一小段数据。什么样的消息验证函数能够承担起这样的任务呢？

通常的对称密钥算法，密文数据不会小于明文数据，这样的话，就不能满足验证数据小的要求。因此，通常的对称密钥算法，我们并不能当做消息验证函数使用。

那么，我们到底该怎么选择消息验证函数？

该怎么选择消息验证函数？

我刚才提到，消息验证函数的输出应该是一小段数据，这一点有没有让你想起，我们前面提到的单向散列函数？我们再来回顾一下单向散列函数的三个特点：

正向计算容易，逆向运算困难；

运算结果均匀分布，构造碰撞困难；

给定数据的散列值是确定的，长度固定。

如果你再回头看看，我们上面讨论的消息验证的三个条件，单向散列函数就能够完美地满足这三个条件。那么，对称密钥怎么和单向散列函数结合起来，构造出消息验证函数呢？最常见的方案就是基于单向散列函数的消息验证码。

基于单向散列函数的消息验证码（Hash-based Message Authentication Code， HMAC）这个名字是不是听起来太长了，所以我们通常使用它的简称 HMAC。

在 HMAC 的算法里，单向散列函数的输入数据是由对称密钥和待验证消息构造出来的。到目前为止，这种构造方法还没有明显的安全问题，我们不再讨论构造细节。如果你感兴趣，可以查阅 1997 年发表的 RFC 2104。

一个密码学算法经历了二十多年还没有明显的安全漏洞，这真的是很难得！

不过，你需要注意的是，HMAC 算法并不使用我们前面讨论过的链接模式。所以，对称密钥链接模式的各种安全问题并不会影响 HMAC 算法的安全性。

为什么需要对称密钥？

到这里，你是不是早就有了一个疑问，消息验证函数为什么还需要对称密钥呢？我们前面提到，单向散列函数也可以验证数据的完整性。为什么它不直接使用单向散列函数呢？

还记得吗？我们前面在讨论单向散列函数解决数据完整性问题的时候，还有一个遗留的问题，就是怎么获得原始数据的散列值。对称密钥就是用来解决这个问题的。

我们先来看看，如果没有对称密钥的加入，消息验证码还能不能工作。

信息发送方把待验证信息和消息验证码都发送给信息接收方。假设存在一个中间攻击者，能够解开待验证信息和消息验证码。由于单向散列函数是公开的算法，中间攻击者就可以篡改待验证信息，重新生成消息验证码。

然后，中间攻击者把篡改的信息和篡改的验证码发给信息接收方。篡改的信息和篡改的验证码能够通过信息接收方的信息验证。也就是说，这样的话，信息接收方就没有办法识别出这个信息是不是原始的、没有篡改的信息。这样，信息验证就失效了。

可是，如果对称密钥参与了消息验证码的运算，由于中间攻击者并不知道对称密钥的数据，攻击者就很难伪造出一个能够通过验证的消息验证码。换一个说法，对称密钥的参与，是为了确保散列值来源于原始数据，而不是篡改的数据。

有了对称密钥这个私有数据的参与，消息验证码的算法是不是就没有安全漏洞了呢？

怎么计算 HMAC 算法的强度？

HMAC 算法与对称密钥和单向散列函数息息相关，所以，对称密钥的安全强度和单向散列函数的安全强度，都会影响 HMAC 算法的安全强度。该怎么计算 HMAC 算法的安全强度呢？

严格的来说，HMAC 算法的安全强度，是由对称密钥的安全强度和两倍的散列值长度之间较小的那个数值决定的。比如，如果我们选择 256 位的对称密钥，以及散列值长度是 160 位的 SHA-1。

两倍的散列值长度就是 320 位。那么，在 256 位和 320 位两个数值之间，256 位是较小的数值。那么，这个 HMAC 运算的安全强度就是 256 位。

一般来说，两倍的散列值长度通常大于流行对称密钥强度。所以，HMAC 算法的强度，通常也是由对称密钥决定。简单起见，对于流行的 HMAC 算法，我们只需要考虑对称密钥的安全强度。

有哪些常见的 HMAC 算法？

HMAC 算法是由单向散列函数的算法确定的。下面的表格，我列出了一些常见的算法。同样的，我们把 HMAC 算法也按照退役的、遗留的以及现行的算法来分类。

其中，HmacSHA256 和 HmacSHA384 是目前最流行的两个 HMAC 算法。和以前的讨论一样，为了最大限度的互操作性和兼容性，我们应该选择当前最流行的算法。

如果单独的加密并不能解决信息的有效传递问题，有没有加密算法，能够综合考虑信息的机密性和完整性？如果存在这样的算法，我们就不需要额外设计消息验证码了。下一次，我们来讨论这个问题。

Take Away（今日收获）

今天，我们讨论了防止数据被调包的技术，也就是消息验证码。我们讨论了消息验证码要解决的问题，以及消息验证码的工作原理。我们还谈到如何选择消息验证函数，最常见的方案就是选择基于单向散列函数的消息验证码，也就是 HMAC。

为什么我们需要对称密钥？其实是为了解决单向散列函数的遗留问题，因为对称密钥的参与，可以确保散列值来源于原始数据，而不是篡改的数据。

最后，我们还研究了怎么计算 HMAC 算法的强度，还列出了目前常见的 HMAC 算法。我们应该选择当前最流行的算法，而对于流行的 HMAC 算法，我们只需要考虑对称密钥的安全强度。

通过今天的讨论，我们要：

了解消息验证码要解决的问题；

尽量选用现行的、流行的算法：HmacSHA256 和 HmacSHA384。

思考题

今天的思考题，是一个复习题，也是一个改进的题目。

我们前面讨论过的牛郎织女的约会问题。我们再来看看现在，我们有没有更好的办法解决这个问题。如果牛郎要给织女发信息，七夕相约鹊桥会。

织女：

七月初七晚七点，鹊桥相会。不见不散。

牛郎

你能够帮助牛郎想想吗？该怎么使用单向散列函数和消息验证码，来防范约会信息被恶意修改？你建议的办法还有没有其他的问题？

欢迎在留言区留言，记录、讨论你的想法。

好的，今天就这样，我们下次再聊。

附：表格中算法参考文献链接

HmacMD5：RFC 1321

HmacSHA1：FIPS 180-1

HmacSHA224：FIPS 180-3

HmacSHA256、HmacSHA384、HmacSHA512： FIPS 180-2

HmacSHA512/224、HmacSHA512/256 ：FIPS 180-4

HmacSHA3-224、HmacSHA3-256、HmacSHA3-384、HmacSHA3-512：FIPAS 202

14 | 加密数据能够自我验证吗？

你好，我是范学雷。

还记得上一讲，我们讲的消息验证码吗？我们讨论过可以使用消息验证码来验证消息的真伪。但是，不知道你有没有注意，在上一次讨论中，我们并没有讨论该如何安全地传递待验证消息。

而且，待验证信息的传递还是通过明文的方式进行的，这种方式，信息的私密性会受到影响。我们前面讨论过，单独的加密并不能解决信息的有效传递问题，总是存在这样或者那样的问题。

那么，我们能不能把消息验证码和信息加密结合起来，既保持信息的私密性，也保持信息的完整性呢？这就是我们这一次要解决的问题。

先加密还是后加密？

想要保持信息的私密性，我们可以在信息传输之前，把明文数据加密成密文数据，然后传输密文数据。如果我们还想要保持信息的完整性，我们就要使用消息验证码。

第一个来到我们面前的问题是：消息验证码和信息加密该怎么结合起来？或者换一种说法就是，怎么构造可认证的加密（Authenticated Encryption (AE)）呢？

加密和验证组合起来的方式不外乎三种方案。

加密并验证

第一种方案，就是加密明文数据，计算明文数据的消息验证码，输出密文数据和验证码。这种方案，我们简称为加密并验证。安全外壳协议（SSH）就是采用加密并验证的方案。

这个方案的消息验证码，保护的是明文信息的完整性，而不是密文信息的完整性。如果明文信息相同，它的消息验证码也是相同的。从攻击者的角度看，如果发现两个相同的消息验证码，就可以猜测明文信息大概率是相同的。

我们前面反复讨论过，为什么要使用初始化向量来避免重复的明文生成重复的密文。这个方案，又一次把这个缺陷暴露了出来，只不过现在，我们是通过消息验证码来判断的。

加密后验证

第二种方案，加密明文数据，计算加密数据的消息验证码，输出密文数据和验证码。这种方案，我们简称为加密后验证。IPSec 协议采用的就是加密后验证的方案。

这个方案的消息验证码，保护的是密文信息的完整性，而不是明文信息的完整性。由于密文是从明文演算过来的，也就间接地保护了明文的完整性。

另外，只要加密算法不把相同的明文信息加密成相同的密文信息，它的消息验证码也就是不同的。所以，这个方案没有上面的加密并验证方案的安全问题。

验证后加密

第三种方案，则是计算明文数据的消息验证码，加密明文数据和验证码，输出密文数据。这种方案，我们简称为验证后加密。SSL 协议采用的就是验证后加密的方案。

这个方案的消息验证码，保护的是明文信息的完整性，而不是密文信息的完整性。如果我们把明文信息和消息验证码看作是一个数据，我们前面提到的 CBC 攻击方案是不是似乎又回来了？实际的攻击方案比我们前面讨论的复杂，如果你有兴趣，可以看看这篇论文。

每一种方案都有重量级的协议支持。重量级的协议都能使用这些方案，我们能不能任意选用哪一款方案呢？我觉得答案是很明显的。

该选用哪一个方案？

有了重量级协议的支持，给了我们一个很好的借口。似乎，我们可以选用这其中任何一个方案，把它用到我们的应用程序里。不过，重量级的协议也会有安全问题，有重量级协议撑腰，并不意味着它就是安全的。

由于新的密码分析技术的进展，尤其是前面我们讨论过的 BEAST 攻击这种新技术的出现，加密并验证以及验证后加密这两种方案都受到了很大的挑战和质疑。

接下来，在 2014 年以后，无论是 SSH 协议还是 TLS 协议，都提供了加密后验证的选项，用来提高协议的安全性。其中，2018 年新发布的 TLS 协议，甚至完全抛弃了 CBC 模式，也不再使用上述的任何一个方案。

所以，如果我们只能从上述三个方案中间选择，加密后验证这个方案目前来说，是最安全的方案。截止到我准备这一讲的时候，就我自己的知识范围里，只要算法选择得当，加密后验证方案还不会出现致命的安全漏洞。

答案似乎很清楚了，结论就是，在应用程序里，我们可以放心使用加密后验证方案。 在你决定要把这个结论记下来之前，你想不想知道新的 TLS 协议，采用了什么样新的加密方案？这个新的加密方案，应用程序也可以使用吗？

带关联数据的加密

到目前为止，我们都没有讨论，信息是怎么传递给对方的。无论是在互联网里，还是现实生活中，我们要传递给对方的信息，并不单纯只有信息本身。

生活中信件的传递，需要信封，信封上还要贴邮票、盖邮戳；快递的传递，要有包裹，包裹上要贴快递单。信封和包裹，虽然是用来携带具体信息的，但其实也是信息的一部分。信封里的信件是要保密的，但是信封上的信息是公开的。信封上的信息虽然是公开的，但是同样不可更改。

网络上的信息也是这样，而且更复杂。比如一段网络数据，除了要携带应用要传递的信息外，它的信封上一般还要有版本号，信息类型以及数据长度等信息。

如果一段信息的版本号或者数据类型被篡改，接收方就没有办法正确解读接收到的信息；如果信息的数据长度被修改，接收方就没有办法判断接收的数据是不是完整的，从而影响系统的读写效率，甚至进而存在拒绝服务攻击的风险。

那有没有办法保护信封上的信息，也就是公开部分信息的完整性？

带关联数据的加密，就是用来解决这个问题的。在解决公开信息的完整性问题的同时，一般的算法设计也会同时解决掉私密信息的完整性问题。所以，这一类算法，通常也叫做带关联的认证加密（Authenticated Encryption with Associated Data（AEAD））。

不同于我们前面讨论过的加密函数，带关联的认证加密的加密函数需要三个输入数据：

加密密钥；

明文信息；

关联信息。

输出结果包含两段信息：

密文信息；

验证标签。

一般来说，验证标签可以看做是密文信息的一部分，需要和密文信息一起传输给信息接收方。

如果改变明文信息，密文信息和验证标签都会变化，这一点，就解决了明文信息的验证问题。如果改变关联信息，至少验证标签会不一样，这一点，解决了关联信息的验证问题。

对应地，带关联的认证加密的解密函数需要四个输入数据：

解密密钥；

关联信息；

密文数据；

验证标签。

而输出的是明文信息。在解密过程中，如果密文信息或者关联信息验证失败，明文信息不会输出。换句话说，只有明文信息和关联信息的完整性都得到验证，才会有明文信息输出。

这么一看，带关联的认证加密既解决了需要保密信息的私密性和完整性，也解决了关联信息的完整性问题，这使得带关联的认证加密算法成为目前主流的加密模式。

带关联的认证加密算法的广泛使用，也使得曾经占据主导地位的 CBC 算法可以从容地退出历史舞台。因为，带关联的认证加密算法能够进行自我验证。

自我验证，就意味着解密的时候，还能够同时检验数据的完整性。这无疑减轻了应用程序的设计和实现压力。有了带关联的认证加密算法，应用程序再也不需要自行设计、解决数据的完整性问题了。

现在有哪些流行的带关联的认证加密算法呢？下一次，我们接着聊这个话题。

Take Away（今日收获）

今天，我们讨论了使用消息验证码构造可认证加密的三种方案。它们分别是加密并验证、加密后验证以及验证后加密。其中，加密并验证以及验证后加密这两种方案存在安全缺陷；加密后验证是一个更安全的方案。

另外，我们还讨论了带关联的认证加密的基本思路 _。_ 带关联的认证加密除了提供可认证的加密，保护私密数据的完整性之外，还通过关联数据保护公开数据的完整性。能同时保护私密数据的完整性和公开数据的完整性，这是带关联的认证加密算法要解决的主要问题。

通过今天的讨论，我们要：

知道加密并验证以及验证后加密这两种方案存在安全缺陷，尽量不要使用这两种方案；

了解带关联的认证加密，以及它要解决的问题。

思考题

今天的思考题，我们继续挖掘牛郎织女的约会问题。

这一次讨论完，我们再来看看现在有没有更好的办法解决这个问题。为了方便，我把描述部分又抄写一遍。如果牛郎要给织女发信息，七夕相约鹊桥会。

织女：

七月初七晚七点，鹊桥相会。不见不散。

牛郎

你能够帮助牛郎想想吗？该怎么来保证约会信息的私密性和完整性？传递的信息该怎么构造？当然，按照惯例，我们还是要想一想，你建议的办法还有没有其他的问题？

欢迎在留言区留言，记录、讨论你的想法。

好的，今天就这样，我们下次再聊。

祝你圣诞节快乐！

15 | AEAD有哪些安全陷阱？

你好，我是范学雷。

上一讲，我们讨论了加密数据如何才能够自我验证，自我验证就是指解密的时候，还能够同时检验数据的完整性。我们还谈到了带关联数据的认证加密（AEAD）是目前市场的主流思路。

我们有了带关联的认证加密算法，应用程序再也不需要自行设计、解决数据的完整性问题了。但问题是，如果我们要在应用程序中使用带关联数据的认证加密，有哪些算法可以使用？

带关联数据的认证加密算法，有没有需要小心的安全陷阱？这是我们这一次需要解决的问题。

有哪些常见的算法？

还是老规矩，我们先来看看有哪些常见的算法。现在，常见的 AEAD 模式有三种：

GCM；

CCM；

Poly1305。

一般地，我们可以把带关联数据的认证加密看做一个加密模式，就像 CBC 模式一样，我们可以和前面提到的 AES 等加密算法进行组合。但 ChaCha20 和 Poly1305 通常组合在一起；Camellia 与 AES 通常和 GCM 以及 CCM 组合在一起。

由于 AEAD 模式相对较新，而 3DES/DES 等遗留或者退役算法又存在明显的安全缺陷，所以，我们一般不会使用遗留或者退役算法的 AEAD 模式。

如果我们重新整理一下，综合考虑加密算法和加密模式，那么，当前推荐使用的、有广泛支持的、风险最小的算法是：

AES/GCM；

ChaCha20/Poly1305；

AES/CCM。

如果你的应用程序使用上述三个算法里的任何一个，就可以规避掉我们前面讨论的很多风险。经过了十多次的讨论，不知道你有没有变得小心谨慎？我们在使用任何算法之前，都要先存疑，后求证。这时候，你应该要问了，那这三个算法有没有风险？

AEAD 有风险吗？

还记得吗？我们上面提到的 AEAD 算法，都需要使用初始化向量。虽然 ChaCha20/Poly1305 不属于分组密码的分类，但是这个算法也需要使用初始化向量。

我们前面特意讨论过初始化向量选择的一个原则：在一个对称密钥的生命周期里，初始化向量不能重复。所以，对于 AES/GCM 算法，同样的对称密钥，一定不要使用重复的初始化向量。否则的话，就存在安全漏洞。

你发现了，初始化向量的重复问题，就是使用 AEAD 算法的最大风险，也是最难处理的风险。没有密码学经验的开发者可能会忽视这个风险，相应的应用程序也可能因此存在严重的安全问题。

在前面讨论分组对称密码的时候，我们讨论过两个初始化向量选择的方案：使用随机数以及使用序列数。然而，这两个方案也是各有各的缺陷。

单相思的随机数

我们先来看看，使用随机数作为初始化向量有什么问题呢？

谈到使用随机数作为初始化向量，我们指的是信息发送方的行为。既然信息的发送方使用的是随机数，信息的接收方怎么能知道这个随机数的数据呢？

不使用同样的初始化向量，信息接收方是没有办法解密加密数据的。该怎么解决初始化向量在信息发送方和接收方之间的同步问题？这是使用随机数作为初始化向量必须要考虑清楚的问题。

幸运的是，对于上述我们提到的 AEAD 算法，初始化向量并不需要保密。一个常用的方案，信息发送方发送加密信息的时候，可以把明文的初始化向量一起发送。信息接收方直接使用接收到的明文初始化向量，就可以解密加密数据了。

不过，随之而来的问题是，如果有攻击者修改了明文传递的初始化向量，信息接收方能够察觉吗？这个答案是肯定的。不同于没有验证的加密算法，AEAD 有自我验证的能力，如果初始化向量被篡改，解密过程中，数据验证这一关是通不过去的。

也就是说，解密不会成功，篡改可以被检测出来。

还有一个问题，我们需要注意随机数的大小。对于这个问题，我们要再次考虑初始化向量选择的原则，在一个对称密钥的生命周期里，初始化向量不能重复。也就是说，随机数必须有足够的容量，使得在对称密钥的生命周期里，都不太可能出现重复的随机数。

最常用的经验数据，是使用 64 位的随机数。64 位的初始化向量，支持 2^64 的加密运算。大部分的应用程序都难以达到这么大的运算量。

不过，需要注意的是，虽然随机数的文字里带有数字，我们也不能把随机数看做是整数。在密码学的范畴里，随机数指的是随机的数据，不是随机的数字。

所以，千万不要移除随机数头尾的零，要保持随机数完整的位数。64 位的随机数，就一定要有 64 位的数据。即使头尾可能是零，也不能掐头去尾变成 63 位或 62 位，或者画蛇添足变成 65 位。

使用随机数作为初始化向量，还有一个常被忽视的小缺陷。随机数的初始化通常是一个费时费力的过程。在应用程序里，大量的随机数实例的初始化可能会造成应用程序的性能问题，甚至包括应用程序的停顿。

不过，对于有经验的程序员来说，这是一个好解决的问题。只要选择好随机数发生器、减少随机数实例数就可以了。具体的情况，我们后面再讨论。

除此之外，使用随机数作为初始化向量，还有一个小小的缺陷，就是需要额外传递初始化向量。不过，对于大部分应用来说，这点小缺陷，并不是什么问题。

难断舍的序列数

在部分场景中，随机数的产生是一个费时费力的过程。但是，对于 AEAD 算法，只要初始化向量不重复就行，并不要求初始化向量不可预测。避免昂贵的随机数，是我们在使用密码学技术时，要经常考虑的问题。使用序列数作为初始化向量，就是一个最流行的方案。

如果使用序列数，你需要注意的是，使用静态的对称密钥，特别需要注意序列数的静态化。比如说，对称密钥存在磁盘上，每次启动程序，都加载该密钥。那么，如果序列数没有对应的保存下来，每次启动的应用程序就有可能使用重复的序列数，从而带来严重的安全问题。

如果序列数也保存在磁盘上，并且每次程序启动时，加载密钥的时候，也加载序列数，我们也需要注意多线程的同步问题，其实也不省心。

使用静态的对称密钥的问题很多，我们后面还要讨论其他的安全问题。根本的办法，就是避免使用静态的密码。对称密钥，最好使用的时候再生成它，使用完立即销毁，不保存，不长期使用（不超过 7 天）。

理想地，如果信息发送方的每一个信息，信息接收方都能够收到，中间没有攻击者的干扰，使用序列数的初始化向量就不需要传递。信息发送方和接收方各自独立维护一个序列数计数器，就可以保持初始化向量的同步问题了。

但是，这种状况太理想了，有很强的局限性，只能在严格的、没有序列数错配的场景使用。

在网络环境中，信息可能丢失，或者被丢失，或者被攻击者插入攻击信息，或者被攻击者丢弃部分数据，这些都会引起序列数的错配问题，打乱信息接收方的节奏，错乱信息接收方的计数器。

怎么能够获得序列数开销小的好处，同时避免序列数错配的问题呢？

序列数的开销小，直接受益方是信息发送方；

序列数错配，直接受害方是信息接受者。

如果信息接收方不需要保持序列数的状态，不需要匹配序列数，也就没有序列数错配的烦恼了。

其实，答案也很明显。信息发送方可以像使用随机数初始化向量一样，发送加密信息的时候，把明文的初始化向量（也就是序列数）一起发送。信息接收方直接使用接收到的明文初始化向量，而不用理会这个初始化向量是一个随机数，还是一个序列数，或者是其他的什么数据。

传送明文的序列数这一个方案，兼顾了信息发送方的效率，规避了信息接收方的顾虑，是目前最常用的初始化向量选择方案。

不过，还是要说一点注意的，既然信息接收方不再需要判别接收到初始化向量是一个随机数还是一个序列数，信息发送方就要兼顾到信息接收方的处理便利。

即使序列数是 1，也要按照固定的位数传输，比如我们前面讨论过的固定的 64 位数据。如果序列数较小，可以前面补零，直到满足 64 位的数据要求。

到目前为止，我们已经知道了，新的应用程序里，应该优先使用 AEAD 算法。AEAD 算法需要初始化向量，明文信息，关联信息，还有密钥。

你有没有想过这样的问题：密钥从哪里来的？密钥应该是什么样的？回答这些问题之前，我们要先讨论一个对称密钥依赖的技术，随机数。我们下次聊聊随机数以及随机数发生器。

Take Away（今日收获）

今天，我们讨论了常见的带关联数据的认证加密算法，以及使用带关联数据的认证加密算法的常见问题，也就是初始化向量的问题。

我们还列出了当前推荐使用的、有广泛支持的、风险最小的算法。它们是：

AES/GCM；

ChaCha20/Poly1305；

AES/CCM。

通过今天的讨论，我们要：

知道常用的三个 AEAD 算法；

知道 AEAD 算法初始化向量不能重复的要求；

知道 AEAD 算法常用的初始化向量选择方案。

思考题

今天的思考题，是一个需要动手的题。

在不同的章节，我们花了很大的精力来讨论初始化向量的问题。这是一个特别容易忽视的环节。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索、统计一下初始化向量的使用状况。加密端有没有使用重复的初始化向量？解密端有没有办法检验初始化向量的重复？初始化向量选择的是随机数还是序列数，或者随机数和序列数的组合？

你有没有发现不恰当的初始化向量使用方案？

欢迎在留言区留言，记录、讨论你的发现。

好的，今天就这样，我们下次再聊。

16 | 为什么说随机数都是骗人的？

你好，我是范学雷。

今天，我们来讲一个之前总提到的知识点：随机数。比如，我们在讲初始化向量时，提到可以使用随机数来避免重复。事实上，随机数还有更广泛的用途。可以这么说，只要使用密码学的技术，我们一定会用到随机数。

说起来也很巧，当我准备随机数这篇文章思路的时候，朋友圈里有一条感慨，说道：

“人能做出两种随机数。一种是欺骗自然的，算的很快，状态域超大，统计上随机；一种是欺骗人的，通常无法预测下一个是多少，通常算的慢”。

其实，欺骗自然的随机数，也是用来骗人的。这么说来，难道所有的随机数都是骗人的？为什么说随机数是骗人的？骗人的随机数还有现实的意义吗？这是这一次我们要讨论的问题。

今天的内容有点多，不过我给你多分了几个小模块，详细地分析了随机数的每个问题，这样会让你更清楚地、一次性地解决随机数的相关知识点。好，我们开始！

真的有随机数吗？

第一个问题就是，随机数真的存在吗？

数这个东西是人类的发明，不是自然界产物。所以，当我们说随机数的时候，一定是人造的东西。随机是自然世界的主旋律，你不可能找到相同的两片叶子，也不可能两次踏进同一条河流。

但是，在科学的世界里，随机从来就不存在，或者说随机都被当做噪音简化过滤掉了。因为科学研究的是确定性、可重复的科学。不确定的、不可重复的随机数，不属于科学的范畴，也走不进科学的领域。

冯·诺伊曼：若人们不相信数学简单，只因他们未意识到生命之复杂。

想一想计算机吧。计算机之所以存在，就是因为它的确定性。同一台计算机，相同的数据输入，一定要产生相同的结果。两台不同的计算机，相同的数据输入，也一定要产生相同的结果。

这是计算机存在的基础。怎么会有随机的数据和计算结果呢？所以说，在计算机的世界里，随机数的确是不存在的。

冯·诺伊曼：任何考虑使用数学产生随机数的方法都是不合理的。

这是不是在开玩笑？那我们前面多次提到过的随机数算是怎么回事？其实，我们能够创造出来的，都不是真的随机数。这些所谓的随机数，要么是骗机器的，要么是骗人的，或者两者都骗。严格一点，我们把它们叫做伪随机数。

既然没有真正的随机数，当然也就没有必要区分真的随机数和假的随机数。所以，我们通常把伪随机数简单地叫做随机数。只要我们明白，没有真正的随机数，一个更顺溜的名字节省了很多口水，却不会引起太多的麻烦。以后没有特别声明，我们讨论的随机数，都是伪随机数。

那么，既然没有真随机数，假随机数的边界在哪里呢？什么样子的伪随机数能够骗得过机器、骗得过人呢？要想回答这个问题，我们先要了解什么样的数才算是随机数。

随机数怎么来的？

先来问你一个问题，“123456”是一个随机数吗？那“123456”比“654321”更随机吗？我们很容易有这样的问题。遗憾的是，这样的问题在密码学里是没有实用意义的。一个数的随机性说的永远是还不存在的、未来的那个数的不可预测性。

即便我们知道了随机数产生的机制，以及所有的已经产生的随机数，我们也无法预测下一个随机数是什么。这就是随机数的第一个特点：不可预测。

那怎么检查未来的那个数是随机的呢？未来的那个数还不存在，当然也没有直接的方法。间接的办法，就是产生很多随机数，然后检测这些随机数能不能通过所有的随机性统计检验。在数学上，有很多种随机性统计检验的理论和方法，我们就不去讨论这么折磨人的数学问题了。

记住一点，只要有一个随机性统计检验方法没有通过，这个随机数产生的机制就是有问题的。换句话说，对于合格的随机数，我们没有办法验证下一个随机数不随机。这就是随机数的第二个特点：无法证伪。

到这里，不知道你有没有找到一对矛盾的地方。我们上面不是说过吗，“同一台计算机，相同的数据输入，一定要产生相同的结果”，那怎么才能够产生不可预测的随机数呢？

产生随机数的诀窍，就是把“相同的数据输入”拆成两部分。一部分是私密的数据，一部分是公开的数据。如果我们能保护住私密的数据，不让人知道，也不让机器拷贝，那么下一个数据是什么，对于别人或者别的机器来说，可能就是无法预测的。

当然，对于秘密的持有者，下一个数据是什么是确定的，不是不可预测的。那么，随机性统计检验呢，当然也得假装这个私密数据不存在。

这个私密数据的质量和计算输出的算法，就决定了随机数的质量。

幸运的是，现在几乎所有主流的计算机，都内嵌了随机数发生器；主流的操作系统或安全类库，都提供了随机数的接口。一般来说，我们不需要自行设计随机数算法，保守私密数据的秘密。

但是这并不意味着我们就可以高枕无忧了。随机数的应用，我们还需要注意两点：

第一点是，随机数的产生可能会阻塞；

第二点是，随机数的强度要匹配。

阻塞的随机数有什么麻烦？

随机数也是有质量要求的，为了保证随机数的质量，随机数发生器的设计需要收集随机信息，比如计算机的噪音，周围的温度，CPU 的状态，硬盘的状态，用户的行为等等。

这些信息收集，是需要时间的，所以有的时候，产生下一个随机数的时候，就会阻塞。阻塞的时间还不确定，有的可能是纳秒级别的，有的可能是秒甚至分钟级别的。而对于一个高吞吐量的系统，微秒级别的阻塞可能都是不能忍受的。

随机数阻塞会带来什么麻烦呢？一般来说，在一个应用程序里，密码学算法要保护的都是关键的信息或者流程，比如说身份认证或者数据加密。如果随机数有阻塞，这个应用程序就会停顿。应用程序的停顿，会使得占有的资源不能及时地释放，降低系统的效率，增加用户等待时间。

更要命的是，随机数阻塞的时间不确定，有时候时间长到无法忍受，有时候短到毫无影响。除了影响系统的效率和吞吐量之外，还会影响用户体验的一致性，影响程序运行的一致性，也使得出现的问题难以排查。所以，除非万不得已，我们尽量不要使用阻塞的随机数发生器。

非阻塞的随机数还能随机吗？

不使用阻塞的随机数发生器，也就意味着有非阻塞的随机数发生器。那么，难道非阻塞的随机数发生器就不需要收集随机信息了吗？不是的，非阻塞的随机数发生器也需要收集随机信息。区别在于随机信息怎么使用，以及使用的频率。

阻塞的随机数发生器的每一个随机数，都要损耗随机信息；而非阻塞的随机数发生器，可能仅仅在开始的时候就损耗随机信息（比如说一台计算机开机的时候），然后，随机数的发生就不再损耗随机信息了。只要不再损耗随机信息，就不会有收集随机信息带来的阻塞了。

不再损耗随机信息，还能保证随机数的随机性吗？

什么是确定性的随机数发生器？

有一类随机数的算法，叫做确定性的随机数发生器（Deterministic Random Bit Generator， DRBG），使用的就是单向散列函数。

怎么解释确定性？确定性说的就是相同的输入，有相同的运算结果。这也就意味着，对于确定性的随机数发生器来说，它的下一个随机数是确定的，是可以计算出来的，当然也是可以预测的。

计算结果可以预测，这还算什么随机数？

我们上面说过，产生随机数的诀窍，就是持有一部分私密的数据。随机数发生器持有私密的数据，所以计算结果对它来说，是确定的。如果我们不知道私密的数据，从随机数发生器外面看起来，下一个随机数还是能够做到貌似不可预测的。

怎么做呢？

怎么使用哈希函数实现随机？

你还记得我们前面讨论过的单向散列函数的性质和消息验证码的工作原理吗？使用单向散列函数实现非阻塞的随机数发生器的关键，就是把这部分私密的数据，当做单向散列函数输入的一部分，然后再加入不会重复的数据，比如序列数。

为什么这个办法是可行的呢？使用单向散列函数和有限的随机信息，随机性能够满足“不可预测”和“无法证伪”这两个检验指标吗？这需要我们回头再看看单向散列函数的特点。

第一个特点，单向散列函数正向计算容易，逆向运算困难。我们使用了私密数据作为单向散列函数输入的一部分，由于逆向运算困难，只要这部分私密数据保护得好，攻击者是没有办法通过运算结果计算出私密数据的。否则，也就意味着破解了单向散列函数。

第二个特点，单向散列函数运算结果均匀分布，构造碰撞困难。我们使用了不会重复的数据作为单向散列函数输入的另外一部分，由于运算结果均匀分布（也就是严格的雪崩效应），如果不知道私密数据，运算结果就是无法预测的。

同时，严格的雪崩效应，也就意味着每一次计算，输出数据的每一位都有 50% 的概率会发生变化。如果不知道私密数据，运算结果的随机性也是无法证伪的。

所以，你看，即使只损耗有限的随机信息，使用好单向散列函数，也能做出来合格的随机数发生器。

既然使用单向散列函数，也逃不掉单向散列函数的种种陷阱。所以，一般来说，我也不建议你自己设计、实现随机数发生器。现在大部分的密码学类库里，都应该支持了非阻塞的随机数发生器。我们要优先使用这些已经经过验证的、成熟的设计和实现。

使用这些类库时，有没有需要注意的事情呢？这是我们要重点关注的问题。其中，最重要的，就是安全强度的匹配。

随机数也有强度吗？

对于阻塞的随机数发生器来说。随机数每一位损耗的都是计算机收集的随机信息。随机数的位数也就是它的安全强度。所以，通常的，我们不担心它的安全强度。

但是对于非阻塞的随机数发生器，它只损耗有限的随机信息，通过单向散列函数来计算随机数。这也就意味着两个问题。

第一个问题是，单向散列函数本身是有安全强度的。相应的随机数发生器的安全强度，不会超过单向散列函数的安全强度。比如说，SHA-256 的安全强度是 128 位，那么使用它的随机数发生器的安全强度一般也不会高于 128 位。

第二个问题是，单向散列函数的输出长度是固定的，而随机数的长度可能并不匹配散列值的长度。SHA-256 的散列值长度是 256 位，如果要产生一个 512 位的随机数，就需要至少两个散列值的拼接。

这会带来什么问题呢？通常地，一个 256 位的随机数就应该有 256 位的安全强度，一个 512 位的随机数就应该有 512 位的安全强度。但是，由于单向散列函数自身的限制，基于 SHA-256 的随机数发生器，它的安全强度不会超过 128 位，无论这个随机数是 256 位还是 512 位。这就带来了很大的困扰。

需要多长的随机数，我们就使用随机数发生器生成多长的随机数，而不需要考虑随机数的强度够不够，这是人们的使用习惯。 由于这样的习惯，要想漂亮地解决上面的问题就变得很棘手。

甚至，很多密码学类库都没有准备好解决这样的问题的接口，其中包括 Java 语言。Java 语言默认使用很高的安全强度，这样应用程序就不用担心当前会有实质性的安全问题了。但是从长远看，这样的解决方法还不够灵活，没有给应用程序提供更高安全强度的选项。

有哪些常见的随机数算法？

目前，有两类常见的随机数算法，它们是基于单向散列函数的 Hash-DRBG 和基于 HMAC 的 HMAC-DRBG。一般来说，Hash-DRBG 算法初始化的时候需要使用有限的随机信息。而对于 HMAC-DRBG 算法来说，这个有限的随机信息被保密的对称密钥取代了。

无论是随机信息还是已知的对称密钥，都是需要保密的信息。所以，使用保密的对称密钥取代随机信息，并没有安全上的妥协。

不过，使用保密的对称密钥取代随机信息还有一个好处。一般来说，对于应用程序来说，随机信息是透明的，只有机器知道，应用程序并不知道随机信息是什么。

所以，这样的随机数是应用程序无法复制的。而使用对称密钥，只要知道了对称密钥，应用程序就有复制随机数的办法。这一点，对于很多应用场景，都有着重要的意义。可以说，HMAC-DRBG 给了应用程序更多的灵活性。

无论 Hash-DRBG 还是 HMAC-DRBG，它们的安全强度都是由所使用的单向散列函数的安全强度决定的。使用的时候，要注意安全强度的匹配问题，确保随机数能够提供足够的安全强度。

上一讲结尾的时候，我们说过随机数是对称密码依赖的技术。除了初始化向量以外，随机数也是生成对称密钥的基础。该怎么使用随机数生成对称密码？我们下一次讨论这个话题。

Take Away（今日收获）

今天，我们讨论了和随机数概念相关的话题，比如，真的随机数存不存在？伪随机数有什么特点？我们还讨论了阻塞的随机数发生器和非阻塞的随机数发生器，以及怎么使用单向散列函数来实现随机函数。

我们还讨论了随机数的强度，随机数的位数也就是它的安全强度。还有两类常见的随机数算法，也就是 Hash-DRBG 还是 HMAC-DRBG。我们使用的时候，要注意安全强度的匹配问题，确保随机数能够提供足够的安全强度。

通过今天的讨论，我们要：

知道随机数发生器可能会阻塞，我们应该尽量使用非阻塞的随机数发生器；

知道随机数的算法也有安全强度的限制，我们要小心安全强度错配的问题。

思考题

今天的思考题，是一个动手题。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索一下随机数的使用。重点关注如下的问题：

随机数发生器使用了什么算法？

随机数的安全强度足够吗？

随机数发生器会不会阻塞？

如果你发现了问题，有没有改进的建议？

欢迎在留言区留言，记录、讨论你的发现和建议。

好的，今天就这样，我们下次再聊。

17 | 加密密钥是怎么来的？

你好，我是范学雷。

到目前为止，你已经跟我一起走了很长的路了。不知道这一路上，你有了哪些心得和体会？对密码学是不是多了很多新的认知和想法？这一讲，我们继续上路，踏足密码学的世界。

前几讲，我们花了很长时间讨论了对称密钥的算法，以及使用对称密钥算法要注意哪些陷阱。但是，不知道你有没有注意到，一直有一个悬而未决的问题。

我们要使用对称密钥算法，总得有对称密钥吧。那么，对称密钥是从哪儿来的？这是我们这一次要讨论的问题。

合格的对称密钥什么样？

对称密钥从哪里来的？在讨论这个问题之前，我们先要弄清楚另外一个问题。一个合格的对称密钥，应该满足什么样的条件呢？只有知道了需求，我们才能有解决的方案。

对称密钥的长度

还记得我们之前提到过的 AES-128 和 AES-256 算法吗？

其中的 128 和 256，指的是密钥的长度。也就是说，AES-128 需要 128 位的密钥，AES-256 需要 256 位的密钥。一般来说，一个对称密钥算法的密钥长度是固定的。这就是对称密钥的第一个要求：对称密钥的长度是由对称密钥算法确定的。

当然，这并不意味着一个对称密钥只能用于一个加密算法。一个对称密钥，一般可以用于任意一个对称密钥算法，只要这个对称密钥满足算法要求的密钥长度。比如一个 256 位的对称密钥，既可以用于 AES-256，也可以用于 ChaCha20。

可是，一个对称密钥用于两个不同算法，这不是我们推荐的用法。因为，我们要考虑算法破解的风险。如果一个算法被破解了，那么它使用的对称密钥可能也就被破解了。我们不希望一个算法的失败连累另一个算法要保护的数据。

所以，大部分的应用程序接口，都不会限制一个对称密钥只能用于一个算法。但是，我们要有意识地避免这种情况。比如说，如果一个对称密钥已经用于 AES-256 的加密计算了，就不要再把它用于 ChaCha20 或者其他的加密算法了。

对称密钥的强度

说完了对称密钥的长度，我们来看对称密钥的强度。

有印象的话，你应该记得 AES-128 算法的安全强度是 128 位，AES-256 算法的安全强度是 256 位。可是，如果没有高质量的对称密钥，这样的安全强度就没有意义。

举个例子，如果密钥只能是阿拉伯数字，那么 128 位的密钥就只有 10^16 种可能性。也就是，如果使用蛮力攻击的话，最多需要 10^16 次尝试，加密密钥就能够找到，加密数据就能够被破解。

如果我们把 10^16 转换成按位表示的安全强度，也不过就是 53 位的安全强度，这离 128 位的安全强度可相差太远了。所以，对称密钥的强度一定要和加密算法的强度匹配。比如说吧，AES-128 算法需要 128 位的密钥，这个密钥就要有 128 位的强度。

对于任意给定的密钥，我们并不一定能够判断它的强度是不是足够。比如说，我们并不能判断“123456”是不是比“135246”强度更大。所以，当我们说密钥强度的时候，其实我们关注的还有产生密钥的机制。

首先，产生密钥的机制要有匹配的强度。如果产生密钥的机制只有 128 位的安全强度，它就不能提供 256 位安全强度的密钥。简单地说，攻击产生密钥的机制就可以了。

其次，密钥在它的长度上要均匀分布。也就是说，这个密钥的每一位是 0 还是 1 的概率都是 50%。如果不能做到均匀分布，就会降低密钥的安全性。比如说，我们前面提到的阿拉伯数字的密钥，就是密钥没有做到均匀分布，导致安全强度降低的例子。

还有，密钥生成机制产生的密钥要随机。也就是说，下一个密钥要均匀分布，而且不可预测。如果下一个密钥不是随机的，那么下一个密钥的安全性就没有保障。如果上一个密钥是“123456”，下一个密钥是“123457”，只是简单地递增，那么这两个密钥都是不合格的密钥。

总结起来就是，一个合格的对称密钥，它的长度和强度要与对称密钥算法相匹配。

对称密钥的秘密

在第 6 讲，我们提到，密钥的保密性和算法的安全性是对称密钥算法安全的两个关键因素。

既然密钥需要保密，那当然也就意味着密钥有秘可保。没有秘密的密钥当然谈不上保密，不能保密的密钥也没法保护数据的机密性。总之，对称密钥要有秘密。

不过，需要注意的是，秘密也是有安全强度的。比如说，很多地方的民俗，有“猜有无”的酒令。猜的人猜测对方握紧的手里有没有东西。

对于出酒令的人来说，手里有没有东西，当然是一个秘密。但是这个秘密被猜中的几率是 50%。如果换算成密码学的指标，也就是只有 1 位的安全强度。1 位的安全强度，当然简单好玩，适合于饮酒助兴。可是只有 1 位的安全性，并不适合在计算机系统中保护我们的机密数据。

一个合格的对称密钥，要有足够的秘密，并且它的长度和强度要与对称密钥算法相匹配。

这三个需求看起来简单、直观，但是用起来很容易就掉进强度错配、无秘可保的坑里。好了，有了这三个需求，对称密钥从哪里来这个问题，我们就可以来一起讨论它了。

对称密钥从哪里来？

那么，对称密钥是从哪里来的呢？你可能觉得这个问题有点怪怪的，其实这个问题，换个说法，就是我们去哪里才能够找到长度和强度都符合要求的秘密？

先看秘密的来源，来源主要有两类：

一类是计算机用户持有的秘密；

另一类是计算机持有的秘密。

对应地，也就是对称密钥的两种来源。

用户持有的秘密

计算机用户持有的秘密，主要表现为只有该用户知道的秘密和只有该用户拥有的秘密两种。比如，我们能够记住的用户口令，是只有我们知道的秘密；我们的指纹，是只有我们拥有的信息。

遗憾的是，我们能够记住的密码很短，一般来说，满足不了对称密钥强度的需求；我们拥有的指纹、面容、虹膜信息，都可以复制，保守这样的秘密是一个极具挑战的任务。陌生的场合，我们摸一摸杯子，睁一睁眼睛，露一下面颊，这些所谓的秘密就都不再是秘密了。

我们拥有的生物特征不可靠，我们能够记住的又太少，那为什么指纹识别和用户密码还这么流行呢？主要原因是还是没有更好的、更简单的办法。

从我接触密码学开始，就已经有人喊口令要消亡了。二十多年了，口令依然活得有模有样，指纹 / 面部识别也越来越流行，尤其是需要身份认证的时候。

使用口令生成对称密钥

既然口令的强度不够，那如果一段加密数据，只有用户参与才能解密，那该怎么办呢？解决的办法就是分级：使用弱的口令来保护强的密钥，然后使用强的密钥来保护私密数据。

如果加密数据泄漏了，由于保护它的对称密钥有足够的强度，我们不用担心破解的问题。

而口令，主要用于身份认证和衍生密钥。身份认证和密钥衍生，都不是高频次的运算。 从口令推导出来的对称密钥也不保存、不长留。这些措施，都降低了口令破解带来的数据泄漏风险。

使用口令生成对称密钥的办法，通常成为“基于口令的密钥推导”。现在常用的基于口令的密钥推导算法是 PBKDF2。我不在这里讲这个算法的细节了，你可以自己去找一找相关的规范。

不过，我们需要注意的是，由于口令的安全强度不够，很容易被破解，我们需要经常地变换口令，有些公司是强制性要求。如果口令变换了，从它衍生出来的对称密钥当然也就随着变换了。

如果我们直接使用衍生出来的对称密钥加密数据，每次的口令变化，都需要把已经加密的数据重新加密一遍。这可不是好事情！

所以，通常地，我们也不推荐使用口令推导出来的密钥直接加密需要留存的数据。有什么办法克服这个障碍呢？解决这个问题，我们还要再添加一个环节。也就是使用推导出的密钥保护一个使用时间更长的密钥；而使用时间更长的密钥用来保护私密数据。

多了一个环节以后，如果口令发生变更，我们只需要重新加密留存的对称密钥就行了，而不需要改动已经加密了的数据。那么，这个使用时间更长的密钥是怎么来的呢？

这就需要我们讨论下一个秘密的来源了：计算机持有的秘密。

使用随机数生成对称密钥

计算机持有的秘密，主要用的是我们上一节所讨论的随机数。

由于随机数是不可预测的，我们只要把随机数当做秘密信息来保护，就没有其他的人或者其他的计算机能够知道这个秘密。这样看起来，随机数能够完美地契合对称密钥的需求，无论是长度强度，还是保密要求。

需要一个对称密钥的时候，如果我们知道需要的长度、强度，找对强度匹配的随机数发生器，生成一个对应长度的随机数，就可以获得一个对称密钥了。这是一个看起来很简单、直观的办法。

但是，计算机持有的秘密也有一个缺陷，就是没法转换成我们人脑能够记住的东西。我们没有办法记住随机数，更没有办法记住从随机数衍生出来的对称密钥。既然我们没有办法，解密还需要对称密钥，那就需要计算机替我们记住它。

计算机该怎么管理对称密钥，怎么保守密钥秘密？这些问题也就一下子都冒出来了。

正像我们讨论过的一样，使用口令推导出的密钥可以保护使用时间更长的密钥，当然也包括使用随机数生成的密钥。不过这种保护方式，也有很大的局限性。这种局限性又在哪里呢？还有没有其他的方式？这些问题，我们下一次再讨论。

Take Away（今日收获）

今天，我们讨论了一个合格的对称密钥应该满足什么条件，以及对称密钥的两个主要来源。

通过今天的讨论，我们要：

了解对称密钥要满足的三个条件：长度、强度和秘密。

了解产生对称密钥的两个主要办法：使用随机数，或者是基于口令的密钥推导。

知道使用基于口令的密钥推导来保护数据的常用办法。

思考题

好的，又到了留思考题的时候了。

今天的思考题，我们稍微加大一点难度。不过也不用紧张，我相信只要你去反复撕扯这个问题，不管结论是什么，你都会有收获的。

我们反复强调过，我们能够记住的东西很少，我们能够记住的口令的安全强度也远远不够。那为什么从口令推导出来的密钥就能够更安全呢？ 如果口令只有 6 位数字，猜中口令的可能性是 10^6。

转换成按位表示的安全强度，也不过就是 20 位的强度。这样的安全强度我们应该担心吗？如果这样的担忧是合理的，我们应该怎么提高口令的安全性？如果使用从口令推导出来的密钥，需要注意哪些问题？

欢迎在留言区留言，分享你的经验。参与讨论的人越多，我们互相学习、互相启发，能够得到的就越多。

好的，今天就这样，我们下次再聊。

元旦快乐！

18 | 如何管理对称密钥？

你好，我是范学雷。

上一讲，我们讨论了合格的对称密钥需要具备什么条件，以及对称密钥是怎么产生的。但是，了解对称密钥是怎么产生的，是远远不够的，我们还要了解怎么管理这些对称密钥。

在我们讨论怎么管理之前，我们还要再给对称密钥分个类，划分的标准就是对称密钥要不要留存。那么，哪种密钥不需要留存呢？我们该怎么管理呢？这是我们这一次要讨论的问题。

对称密钥要不要留存？

你是不是有些迷惑，难道对称密钥不需要留存？如果不留存，为什么还要做密钥管理？

为什么我们要讨论对称密钥要不要留存？因为最好的管理，就是不管理或者少管理。按照这个思路，我们可以把对称密钥分成两类：即用即弃的对称密钥和需要留存的对称密钥。

即用即弃的对称密钥

即用即弃的对称密钥，就是用的时候才生成出来，不需要保存，用完了就扔掉的对称密钥。

那这种对称密钥的适用场景是什么呢？即用即弃的对称密钥可以用在加密数据不需要保存的场景。比如说， 像 HTTPS 这样的端到端传输协议，它的网络传输数据是加密的，而加密的网络传输数据是不需要保存到硬盘里的。

这种情况下，对称密钥可以使用计算机持有的秘密，也就是使用随机数来生成。还记得什么是计算机持有的秘密吗？你可以复习一下 17 讲，来回顾一下这个知识点。

当然，有的时候，加密数据需要保存的场景，也可能要使用即用即弃的对称密钥。但是，对称密钥不是被扔掉了吗？没有对称密钥，怎么解密加密数据呢？我知道你可能会有这样的疑问。

其实，思路很简单，我们在解密的时候，再生成一个完全相同的对称密钥就行了。这种情况下，对称密钥可以使用用户持有的、不需要存储的秘密，比如口令，在需要对称密钥的时候，即时地把它推导出来。

即用即弃的对称密钥，是我们推荐使用的对称密钥。下一节，我们再来聊推荐原因。

需要留存的对称密钥

和即用即弃的对称密钥相对的，就是用完了不能丢弃的对称密钥，也就是需要留存的对称密钥。

需要留存的对称密钥，大部分出现在用户无法参与的计算环境里，比如自动启动的服务器。因为，在用户能够参与的计算环境里，不应该使用需要留存的对称密钥。取而代之的，应该是由用户持有的、不需要存储的秘密推导出来的即用即弃的对称密钥。

要知道，对称密钥需要保密。毫无疑问地，留存的对称密钥需要得到额外的照顾，避免对称密钥的泄漏。比如说，保存对称密钥的文件，它的权限需要设置成只有它的拥有者才能阅读、修改。在高度保密的环境下，甚至，我们需要把对称密钥保存到专用的芯片里。

需要注意的是，在我们设计的软件架构里，应该尽量避免使用需要留存的对称密钥。无论对称密钥是保留在专用的芯片里，还是保密的文件里，随着时间的推移，留存的密钥都有泄漏的风险。

另外，我们前面已经讨论过，既然对称密钥需要保密，我们就要把对称密钥当做是超级敏感的信息来处理。这些处理方式，包括但不限于，要及时清理内存里的对称密钥，而不能依赖系统的内存回收机制；不要把对称密钥有意无意地泄漏出去，比如把对称密钥写到系统日志里。

对称密钥有什么麻烦？

接下来，我们来看看对称密钥会有什么麻烦？这会利于我们分析对称密钥的管理问题。

我们已经知道，所谓的对称密钥，就是加密和解密都使用相同的密钥。如果加密和解密都是同一个参与者，自己加密的数据自己解密，那么只要持有一份对称密钥就行了。

可是，如果牵涉到两个或者两个以上的参与者，那么，每一个解密的参与者就都要持有和加密的参与者相同的对称密钥，解密才能成功。这就带来了很多麻烦。

让我们来看一个例子，假设一个系统有三个参与者 A、B 和 C。

如果每两人之间的通信都使用不同的密钥，那么 A 和 B、B 和 C、A 和 B 之间，都需要不同的对称密钥。也就是说，三个参与者的系统，需要 3 对不同的对称密钥。类似地，我们可以计算出：

5 个参与者的系统，需要 10 对不同的对称密钥；

10 个参与者的系统，需要 45 对不同的对称密钥；

100 个参与者的系统，需要 4950 对不同的对称密钥。

随着参与者的增加，需要的对称密钥数量急剧地膨胀。这种膨胀的速度，就给密钥的管理带来了很多麻烦，也会使得系统的效率急剧地下降。显然，大量的这种一对一的密钥的设计，不适合有众多参与者的应用。

但是，如果无论参与者有多少，都使用一个相同的对称密钥呢？如果这些参与者之间，是可以信任的，这样做的问题不大。比如说，一个公司内部的远程视频会议，就可以使用同一个对称密钥加密视频数据，然后把加密后的视频数据分发给每一个参会者。

这样，每一份视频数据，就只需要一份加密，每一个参会的都能够解密，看到会议的内容。那要是每一个参会者之间都使用不同的对称密钥呢？那也就意味着，视频的发送端，需要给每一个参会者都发送不同的加密数据。

一个有 100 个人参与的视频会议，每一份视频，都需要有 99 份的加密。你可以想象一下，和只使用一份加密数据的方案相比，这么大的计算量会给这个视频会议系统带来多大的性能麻烦。

而且，也不是每一个场景里，它的参与者都是可以信任的。比如说，我们可以想一想即时通信系统里记录的联系人。这些人，有时候也被叫做朋友圈。可是，虽然叫做朋友圈，圈子里的不一定都是熟人，更不一定都是见过面的人。

当然，朋友圈有我们可以信任的朋友，也有我们不能信任的陌生人。如果我们和不同的联系人通信，都使用相同的对称密钥加密通信数据，那我们不信任的陌生人，就也知道了这个对称密钥。

他们就都能解密我们和每一个联系人的通信数据。即使我们可以信任的朋友，也不意味着他们之间就不能有两两之间的秘密。所以，每个人之间都使用相同的对称密钥是不行的。

如果每两个人之间的通信数据，都使用不同的、只有这两人才知道对称密钥加密，也就是所谓的端到端的加密技术了。想一想，如果有 2000 个联系人使用端到端的加密，也就意味着需要 2000 个对称密钥。

这是不是意味着，需要保存 2000 个对称密钥呢？通过前面的讨论，我想你已经有了答案。

在即时通信的场景里，还有一种用户不会喜欢，但是厂商会喜欢的加密方式。那就是每一个用户都把数据加密传递到通信的服务器，然后再由服务器分发给数据的接收方。

这种方式最大的优点，就是服务器知道用户发送的数据明文。服务器知道了数据的明文，就可以做很多事情了。有些事情，我们也许会喜欢；有些事情，我们可能不会喜欢。这种方式还有一个不太重要的优点，就是每一个用户只需要一个用于和服务器通信的对称密钥就够了。这无疑降低了系统设计的复杂度。

但是，我们不会喜欢服务器窥视我们的隐私，因为机器的背后站着不可预测的人。我们也不希望保存 2000 个对称密钥，毕竟密钥的管理不是一件轻松的事情。有没有办法，我们可以和成千上万的人通信，每一个通信都使用不同的对称密钥，但是又不需要在本地保存这些对称密钥呢？

当然是有答案的。Kerberos 就是一个仅仅使用对称密钥系统，就可以支撑这种通信方式的协议。更直观的方法，就是使用基于非对称密钥的密钥交换技术。

Kerberos 协议的使用场景，目前还在逐渐萎缩。除了单点登陆的系统之外，至少在我的认知范围内，使用 Kerberos 协议的新系统已经不太常见了。不过，Kerberos 协议是一个设计优雅的协议。在不使用非对称密钥技术的情况下，它依然可以做到支持大规模用户的端到端加密，这是一个很了不起的成就。

对称密钥的规模化是使用对称密钥的一个大麻烦，这也给对称密钥的管理带来了很多挑战，不过也催生了很多成熟的解决方案。下一次，我们讨论对称密钥的另外一个麻烦，尤其是量子计算时代来临的时候，这个麻烦可能更要命。

Take Away（今日收获）

今天，我们讨论了生成对称密钥的时机，介绍了两种不同生存周期的对称密钥，也就是，即用即弃的对称密钥和需要留存的对称密钥。即用即弃的对称密钥是我们推荐使用的方式。如果对称密钥需要留存，一定要把它当做超级敏感的信息来处理。

另外，我们还讨论了对称密钥在规模化通信场景下的麻烦。使用场景不同，解决这些麻烦的办法也是不同的。更通用的解决方案，需要了解更高级的协议，或者非对称密钥系统。如果还有机会，我们以后再来讨论这些解决方案。

通过今天的讨论，我们要：

有意识优先使用即用即弃的对称密钥；

有意识去保护好需要留存的对称密钥；

知道对称密钥在规模化通信场景下的麻烦，能够有意识地去寻找、学习相应的解决方案。

思考题

好的，又到了留思考题的时间了。

这一次的思考题，我们再加大一点难度，留一个延伸阅读题。我们前面说过，Kerberos 协议是一个设计优雅的协议，能够用来解决对称密钥在规模化通信场景下的问题。

今天的思考题，就是去阅读 Kerberos 协议，去了解这个协议是怎么工作的，是怎么解决对称密钥的规模化通信问题的。如果让你使用 Kerberos 协议去设计一个即时通信软件的数据加密框架，你觉得会有哪些优点，会有哪些缺点？

欢迎在留言区留言，分享你的阅读体验和见解。

好的，今天就这样，我们下次再聊。

19 | 量子时代，你准备好了吗？

你好，我是范学雷。

上一讲，我们讨论了怎么管理对称密钥，强调了要优先使用即用即弃的对称密钥。我们还一起分析了对称密钥的一些问题，相信你也感受到了，其规模化背后的麻烦。

不过，我们又留了一个小尾巴，就是对称密钥能不能应对未来量子计算时代的计算能力？

也就是说，现在使用对称密钥加密的数据，在量子计算时代能不能被破解？我们又该怎么保护我们的敏感数据，不受量子计算时代的算力影响？这是我们这一次要讨论的话题。

量子算力有多强？

要讨论量子计算的影响，我们首先要有一个概念，那就是量子算力有多强。

2020 年 12 月 4 日，中国量子计算原型机“九章”问世。理论上，这个计算机比目前最快的计算机还要快一百万亿倍。也比 2019 年谷歌发布的量子计算原型机“悬铃木”快一百亿倍。

你看看，现在量子计算的性能提升，是百亿倍这个数量级的。

还记得我们总提到的安全强度吗？那时候，我们有一个粗略的判断，使用 10 亿台 1 纳秒计算一次的计算机，破解 128 位的安全强度，需要一千万个十亿年。还有同学留言估算，使用目前最快的计算机 Fugaku，破解 128 位的安全强度，需要 2500 个十亿年。

而“九章”比目前最快的计算机还要快一百万亿倍，按照这个计算能力，如果我们对照 Fugaku 计算机，破解 128 位的安全强度，只需要 2.5% 的年，也就是大约 9 天。

量子计算机的研发还在原型阶段，成熟的量子计算机，性能可能还会有大幅度的提高。到了量子计算机成熟的时候，破解 128 位的安全强度，可能只是微秒或者纳秒级别的计算。

所以，量子计算时代，128 位的安全强度也就不再安全了。

我们再回顾一下第 3 讲，欧洲的 ECRYPT-CSA 的密码安全强度建议。ECRYPT-CSA 建议，128 位的安全强度的密码学算法可用于 2028 年之前，2028 年之后就要使用 256 位的安全强度了。这个建议，就是考虑到了量子计算的影响。

2028 年，离现在已经不是很远了。

那 256 位的算法安全吗？

是不是说，256 位安全强度的密码学算法，在量子计算时代都是安全的？

想要得出答案，我们还可以使用“九章”量子计算原型机的数据，来估算一下破解 256 位安全强度的密码学算法需要多长时间。为了简化计算，现在我们假设量子计算机一纳秒就可以破解 128 位的安全强度，我们有 10 亿台这样的量子计算机。

借用我们前面对 128 位安全强度算法的估算数据，破解 256 位的安全强度，要一千万个十亿年。

所以，我们可以有这样一个印象，在量子计算时代，256 位的安全强度，大致相当于今天 128 位的安全强度。今天 128 位的安全强度，是足够安全的。在量子计算时代，256 位的安全强度也是足够安全的。

我相信，你应该已经有这样的意识了：攻击者不会只按照算法设计者设计的路线攻击，他们有的是千奇百怪、出乎意料的办法。那么，对于 256 位的安全强度，有没有密钥算法承受不了量子计算时代的算力的？这个答案是肯定的。

尤其让人遗憾的是，所有现在流行的非对称密码算法，都不能抵御量子计算时代的算力。

不过，这和现在的非对称密码算法的设计思路有关系，业界现在也在紧锣密鼓地设计、遴选量子计算时代非对称密码算法。让人欣慰的是，256 位安全强度的对称密钥算法和单向散列函数，包括 AES 算法，在量子计算时代还是安全的。

非对称密码算法不能抵御量子计算时代的算力，那么由非对称密码推导出来的对称密钥，是不是也不能抵御量子计算时代的算力？回答这个问题之前，我们先来了解一个概念：前向保密性。

什么是前向保密性？

什么是前向保密性呢？

在密码学里，前向保密性指的是即使用来协商或者保护数据加密密钥的长期秘密泄漏，也不会导致数据加密密钥的泄漏。换个角度看，虽然数据加密密钥是由长期的秘密衍生出来的或者保护的，但是数据加密密钥不能再一次通过长期秘密推导出来。

是不是感觉有点绕口，不知道该怎么理解？

我们来看一看前面讨论过的，从用户口令推导出数据加密对称密钥的算法。每次需要使用这个对称密钥的时候，都可以通过用户口令再次推导出来，而不需要把这个对称密钥保存到硬盘上。

这个过程就不是前向保密的。如果用户口令泄漏了，这个用来加密数据的对称密钥能够再次被推导出来，对应的加密数据也能够被破解。

数据加密密钥可以从长期秘密衍生出来，但是又不能再一次推导出来，这听起来有点别扭。

关键就在于使用用完就丢、而且用完就不能再找回的秘密，比如说随机数。如果随机数也参与数据加密密钥的衍生，只要这个衍生算法是安全的，那这个数据加密密钥也就具有了随机性。

只要生成这个数据加密密钥的随机数被干干净净地丢弃了，别人就不太可能重新找回这个随机数，也就不能再次推导出同样的数据加密密钥了。

那单纯地使用随机数生成的数据加密密钥，具不具备前向保密性呢？比如说，如果我们就使用随机数生成的对称密钥，然后保存到读写权限受到限制的保密文件里。每次加密或者解密需要使用对称密钥的时候，我们就把它从文件里读取出来。那么，这样的密钥能不能前向保密呢？

能反复长时间使用的密钥，就是人们常说的静态密钥，也是我们前面提到过的需要留存的密钥。上面说的，就是一个静态密钥的例子。静态密钥不能前向保密，因为，静态的密钥一旦泄漏，数据保密性也就无从谈起了。

我们把需要留存的密钥排除出去后，那具备前向保密性的密钥一定只能是即用即弃的密钥了。而且，这个密钥的衍生，一定要有即用即弃的随机数的参与。

这样，我们就找到了具备前向保密性的对称密钥的两个特点：

密钥的产生需要有即用即弃的随机数的参与；

密钥的本身是即用即弃的密钥，而不能是静态的密钥。

具备了这两个特点以后，即使协商或者保护数据加密密钥的长期秘密泄漏，也不会导致数据加密密钥的泄漏。这就大大降低了长期秘密泄漏带来的数据泄密的风险。

这就是前向保密性要解决的问题。好了，我们讨论了前向保密性。接下来的问题是，前向保密性和抵御量子计算时代的算力，有什么关系呢？

怎么使用前向保密性？

要想现在就开始筹划抵御量子计算时代的算力的事情，有两件事是一定要考虑的。

第一件事，就是使用量子时代依旧安全的密码学算法。我们要使用 256 位安全强度的对称密钥算法和单向散列函数这样的密码学算法，来确保量子计算时代的算力无法蛮力地破解加密数据。

第二件事，就是使用具有前向保密性的对称密钥来保护数据，特别是密文能够泄漏的数据，来确保量子计算时代的算力无法获得加密使用的密钥。

我们提到，现在流行的非对称密码算法，不能抵御量子计算时代的算力。那基于非对称密码的密钥交换或者密钥协商算法，当然也不能抵御量子计算时代的算力。那么，是不是基于非对称密码的密钥交换或者密钥协商算法衍生出来的对称密钥，也不能抵御量子计算时代的算力呢？

这要看对称密钥衍生算法的细节。如果对称密钥的衍生算法里，有即用即弃的随机数的参与，或者用来衍生对称密钥的非对称密钥也是即用即弃的，而且这个对称密钥还是即用即弃的密钥，那使用它加密的数据，也能够抵御量子计算时代的算力。

具体的算法细节，依赖于我们对非对称密码算法的理解，我们以后有机会再聊。

到目前为止，这个专栏就接近尾声了。我们从专栏之初就开始操心的牛郎织女约会问题，下一讲来看一看，我们能不能通过在这个专栏里讨论的算法，来解决这个约会难题。

Take Away（今日收获）

今天，我们讨论了量子计算时代的算力，相信你对量子计算时代的算力有了一个大致的印象。然后，我们还提到了，256 位安全强度的对称密钥算法和单向散列函数，包括 AES 算法，在量子计算时代还是安全的；而非对称密码算法，不能抵御量子计算时代的算力。

量子计算时代的非对称密码算法目前还在遴选中，而主流的密钥交换算法一般都是建立在非对称密码技术之上的。在后量子时代的非对称密码算法普及之前，我们有没有办法抵御量子计算时代的算力呢？这个问题的答案，就是要使用具备前向保密性的对称密钥。

当然，前向保密性不仅仅是用来应对量子计算时代的算力的。它也是我们现在协议设计和实现里，实现深度防御的一个重要考虑因素。不具备前向保密性的对称密钥，不能使用在加密数据有可能外泄的场景里，这几乎会断送加密的意义。

所以，对于现在的安全协议来说，要求具备前向保密性是一个硬指标。

通过今天的讨论，我们要：

了解量子计算时代的算力；

知道 256 位安全强度的对称密钥算法和单向散列函数在量子计算时代还是安全的；

知道现有的非对称密码算法，不能抵御量子计算时代的算力；

了解前向保密性要解决的问题和它的特点。

思考题

今天的思考题，是一个动手题。

在你正在开发的项目中，或者你关注的开放源代码项目中，试着搜索一下其中的对称密钥是如何产生的、有没有留存、具不具备前向保密性？对称密钥的强度能不能应对量子计算时代的算力？你有什么改进的建议？

这是一个能够帮助你提前评估量子计算影响的好办法，也能够帮助你为量子时代的计算能力做好准备。欢迎在留言区留言，记录、讨论你的发现和建议。

好的，今天就这样，我们下次再聊。

加餐 | 密码学，心底的冷暖

你好，我是范学雷。

今天是我们的一次加餐，我们不讲知识，我要跟你分享一点关于密码学的小感慨。

2011 年的时候，因为要参与解决 BEAST 安全漏洞的国际合作，我得以和一些顶尖的密码学家一块儿工作。当时，幸运的是，我提议的解决方案被广泛采纳，成为了事实上的标准，给 TLS 1.0 续了十年的命。

不幸的是，十多年来，我对密码学一直都很崇拜，但是，它的很完美的印象在我的心底被彻底击碎了，这也彻底改变了我对密码学应用市场的认识。

为了更好地帮助我找到解决方案，当时的一位密码学家分享给我一些他的调查数据。这些数据研究了全球最知名的几十万家公司的公开网站，帮助他们发现了很多和密码学相关的安全问题。

问题很严重，国内公司的问题尤其严重。按理说，大公司有钱有人有技术，能够养得起、请得动密码学专业领域的工程师，信息系统应该没有突出的密码学问题。

很遗憾的是，2011 年的数据表明，那时候的国内大公司，虽然有钱有人有技术，却依然霸占了问题最突出、威胁最严峻的榜单前列。

现实和理想

这是我第一次被密码学现实和理想之间的差距震撼到了。因为这些问题，其实都不是什么技术门槛的问题。技术都是公开的，也都是随手可取的。大部分的安全问题，都来源于这些应用的设计者和实现者没有意识到这些麻烦的存在。

他们没有意识到问题的存在，当然也就不可能解决掉这些问题。在密码学业者眼里的常识，也许是普通软件工程师意识之外的存在。更严重的现实是，由于意识不到这些安全问题，这个公司当然也就不知道这些安全漏洞。

什么时候，他们能知道这些问题呢？要么有善意的研究者告诉他们，要么有恶意的攻击者警告他们。更多的时候，恶意的攻击者已经击破了系统，盗取了数据，也没有留下痕迹，当然也没有敲响警钟。这些被盗取的数据，随时都会是威力巨大的暗雷，不知道什么时候就会引爆！

从 BEAST 安全漏洞开始，随后的几年里，我的工作时间几乎就被不断颠覆的密码算法霸占了。三五年的时间里，几乎所有的主流密码学算法都爆出了或多或少的漏洞。昨天还占据主流地位的算法，今天就被宣布有破解办法，明天就要被扫进历史的垃圾箱了。

大家都忙着给算法打补丁，找替代品，更新产品。这种连环式的暴雷，过了五六年才算消停。 但是，能够消停，也是因为业界几乎把 2010 年之前主流的密码学算法都换了一个遍。

隐忧依然存在

2020 年，一切似乎算是可以喘口气了。可是，隐忧依然存在。

第一个隐忧就是，大家都知道老算法有问题，那使用新算法了吗？答案是令人难堪的：并没有。比如，2020 年，Zoom 就被研究者披露使用了二三十年前就已经不安全的加密算法。

第二个隐忧是，如果使用新算法，数据就安全了吗？ 答案还是令人难堪的：也不一定安全。如果有心者记录了历史数据，如果加密历史数据的算法有一天被破解，历史数据还是有可能被破解。历史数据里有价值的信息，比如用户名和密码，再比如知名人士的行程。尴尬的是，有人认为，不仅存在这样的有心者，而且还有钱有权有势。

第三个隐忧是，如果是新系统、新算法，数据就安全了吗？答案稍微让人欣慰：还有一点风险。这点需要防范的风险就是，如果新算法未来被破解，加密数据能不能被解密？ 我们要确保即使未来算法被破解，特别是量子时代到来后，数据也没有办法解密。要做到这一点，还是需要有点密码学领域的专业知识积累的。

看看这些隐忧，我心里只有凉凉两个字。因为，大部分的软件工程师还没有掌握密码学的基础知识，当然也不会担心这些问题。不担心这些问题，当然就更不会有人去想解决问题的方法。

不过，我还是听说，有很多学校，在大学一二年级，开设了类似于“密码学 101”这样的基础课，即便学生的专业并不是计算机。但令人欣慰的是，即使不是软件工程师，像密码学 101 这样的基础课程，也可以帮助我们更好地保护自己的隐私，保护自己。

十年后，也许有一个新模样。

极客时间的《实用密码学》这个专栏，就是我为十年后的新模样，添的一块砖。

20 | 综合案例：如何解决约会难题？

你好，我是范学雷。

今天是我们正式篇的最后一讲了，我们这场密码学之旅就要暂时结束了。还记得我们第一次的讨论吗？那时候，我们讨论了牛郎织女约会会碰到的困难，详细地分析了每一个障碍。

当时，我们说密码学可以解决牛郎织女的约会难题。这一路走来，每多学习一点知识，我都会再提一次这个难题，不知道你对这个问题，是不是有了很多新的认识和想法？

现在，我们已经学习了对称密码系统的基本框架了，一切有终有始，我们是时候回头看看，能不能使用我们学到的知识，来帮助牛郎织女解决好他们的约会问题了。

回顾一下约会难题

我们先来看一看第一次讨论时，我们罗列出来的问题。牛郎和织女需要考虑的问题主要有五点：

问题一：怎么证明双方的身份？

问题二：怎么能使消息不泄露？

问题三：怎么防止内容被篡改？

问题四：怎么确保信件能收到？

问题五：怎么防止翻脸不认账？

然后，我们通过这五个问题，分析出来了信息安全的六个需求：

需求一：识别身份，确定牛郎就是牛郎；

需求二：认证身份，验证牛郎就是牛郎；

需求三：管理特权，授予织女看信权利；

需求四：信息保密，没有权限不能看信；

需求五：信息完整，保护内容不被篡改；

需求六：信息可用，保持信息获取能力。

我们还提到，一般地，对称密码技术可以通过加密、解密，解决“机密性”的问题；单向散列函数可以解决“完整性”问题；非对称密码技术可以解决授权和认证的问题。

我们通过对这三项基础技术的综合运用，就可以提高系统的“可用性”。虽然，非对称密码技术不在这个专栏的讨论范围，不过，这并不妨碍我们在对称密码技术里寻找替代方案。

下面，我们就分别来看看这些问题可以怎么解决。

怎么解决机密性问题？

既然，对称密码技术可以解决“机密性”的问题，那么，想要使用对称密码技术，首先就要有对称密钥，而且牛郎和织女都要持有相同的密钥。接下来的问题是，对称密钥怎么来的呢？

对称密钥怎么来的？

对称密钥的两个来源，一个是用户持有的秘密，另一个是计算机持有的秘密。

对于计算机持有的秘密来说，每一个计算机持有的秘密都是不同的。即使牛郎和织女都有计算机，独自使用各自的计算机持有的秘密也不能直接演化出两个相同的对称密钥。

所以，我们能想到的，就是从用户持有的秘密着手，看看有没有两个人都知道的秘密，然后各自演化出两个相同的对称密钥。

牛郎和织女有没有共同的秘密呢？这个我们当然无法知晓，但是，我们就假设他们有共同的秘密，比如两个孩子的生辰八字。一般来说，一个人的生辰八字是敏感信息，是需要保密的。

牛郎和织女有一个男孩一个女孩，我们假设男孩的生辰八字是“庚子辛巳乙亥丙子”（庚子年，辛巳月，乙亥日，丙子时），女孩的生辰八字是“戊戌庚申甲申甲子”（戊戌年，庚申月，甲申日，甲子时）。

这样，这个共同的秘密就可以用来推演对称密钥了。怎么推演这个对称密钥呢？

还记得我们说过的，怎么使用口令生成对称密钥吗？在这里，生辰八字就可以当作口令用，基于口令的密钥推导算法可以使用 PBKDF2。下一步面临的问题是，两个人共有的小秘密可能很多，织女怎么才能知道牛郎选择使用孩子的生辰八字这个两人共有的秘密？

还有，使用的是哪一个孩子的生辰八字？生辰八字又是怎么推演出密钥的？

其实，问题很好解决，牛郎在信件里告诉织女这些信息就行了。这些信息并不需要加密，使用人人都能看的明文就行，这就是所谓的算法公开。我们讲过，算法公开并不会影响数据的安全性。

好，现在，让我们把基本思路整理一下：

牛郎要给织女发送约会信息，“织女，七月初七晚七点，鹊桥相会。不见不散。牛郎字”。这部分是私密信息，需要保密。

牛郎要告诉织女怎么解密信息，这一部分是公开信息。这部分信息包括：

1. 使用两个孩子的生辰八字作为推导对称密钥的秘密；

2. 使用什么密钥推导算法来从共有的秘密推导对称密钥；

3. 使用什么对称密钥算法来加密、解密约会信息。

现在有了推演对称密钥的基本思路了，接下来，我们要考虑算法的细节问题了。那么，牛郎该选用哪些基本的密码学算法？

怎么选择密码学算法？

这个问题，其实就是说，在算法的选择上，我们要考虑什么问题。

首先，我们要确定所需的安全强度。如果牛郎希望约会信息的秘密维持得越长越好，256 位的安全强度是首选。

然后，我们来确定所需的密码算法。牛郎要发送的信息，既包含私密信息，又包含公开信息。私密信息和公开信息，都不能被篡改。这种情况下，我们可以使用带关联数据的认证加密算法，也就是 AEAD 算法。AEAD 算法可以从现在流行的三种算法里面挑选一个。

结合安全强度的要求，我们可以选择 ChaCha20/Poly1305 算法。

然后，我们去查查 ChaCha20/Poly1305 算法需要的条件。这个算法需要四个输入数据：

一个 256 位的对称密钥；

一个 96 位的随机数；

代加密数据明文，也就是私密信息；

关联数据，也就是公开信息。

怎么推导出这个 256 位的对称密钥呢？也就是使用我们上面说到的基于口令的密钥推导算法。

这个推导算法，我们可以挑选 PBKDF2，接着，我们要再去查查 PBKDF2 算法需要的条件。这个算法需要五个输入数据：

用户的口令；

HMAC 算法；

盐值，类似于我们讨论过的初始化向量；

迭代次数 ;

导出密钥长度。

接下来，我们还要选择 PBKDF2 使用的 HMAC 算法。先看看安全强度，我们需要选择至少 256 位安全强度的算法。HMAC 算法的强度，通常是由对称密钥决定。所以，我们可以选用最流行的 HmacSHA256 算法，推导出 256 位的密钥。

PBKDF2 算法需要的盐值，我们随机选取一个数值就可以了，迭代次数不妨就选用一次。这样，我们就可以使用 PBKDF2 算法了。它的输入数据看起来像下面的样子：

用户的口令：“庚子辛巳乙亥丙子戊戌庚申甲申甲子”；

HMAC 算法：“HmacSHA256”；

盐值：“3B 07 A6 CB CF 98 48 F0 68 11 28 40 E7 6F 98 66”

迭代次数: 1；

导出密钥长度：256 位。

有了这些输入数据，我们就可以根据 PBKDF2 算法推导出 256 位的对称密钥了。

37 af 4f bd dd 22 7c f3 bc 66 d4 c0 2c 3d e4 5a
e4 b0 da f4 58 0f 37 19 b2 31 93 63 fc 61 61 9d

有了对称密钥，我们再去看 ChaCha20/Poly1305 的运算。在 ChaCha20/Poly1305 算法里，96 位的随机数，还是随机生成一个就可以了。它的输入数据看起来像下面的样子：

一个 256 位的对称密钥：使用上面 PBKDF2 算法推导的对称密钥；

一个 96 位的随机数：“0A 00 00 00 00 00 00 4B 00 00 00 ED”；

数据明文：“织女，七月初七晚七点，鹊桥相会。不见不散。牛郎字”；

关联数据：未知。

除了关联数据外，我们其他的数据都已经就绪了。那么，关联数据该怎么定义呢？

怎么定义通信协议？

其实，这就需要自己定义关联数据，也就意味着我们要定义自己的通信协议。通信协议里，我们一定要定义清楚通信双方都要遵循的格式和规范。在这个案例里，关键就是要告诉织女这封信件的格式，以及怎么解密这封信件。

其中，需要包含我们上述提到的算法的选择，以及对应的参数。

大致地，我们可以这么写这封信：

这封信使用 ChaCha20/Poly1305 算法加密，随机数选取的是“0A 00 00 00 00 00 00 4B 00 00 00 ED”(十六进制表示)，关联数据是本信件内容除去加密数据之外的所有数据，加密密钥是使用 PBKDF2 算法推导出 256 位的对称密钥。PBKDF2 算法的用户口令，使用的男孩和女孩的生辰八字，HMAC 算法使用的是 HmacSHA256 算法，盐值选取的是“3B 07 A6 CB CF 98 48 F0 68 11 28 40 E7 6F 98 66”(十六进制表示)，迭代一次。此后为加密数据。6E 2E 35 9A 25 68 F9 80 41 BA 07 28 DD 0D 69 81 …… 5A F9 0B BF 74 A3 5B E6 B4 0B 8E ED

有了这封信，ChaCha20/Poly1305 算法要使用的关联数据也就确定下来了。就是信件里，除了加密数据之外所有的数据。有了关联数据，我们就可以执行 ChaCha20/Poly1305 的运算了。

织女收到这封信后，就可以按照信件明文部分的描述，结合自己掌握的两个孩子的生辰八字，解密加密信息，获取私密的约会信息了。

很明显，私密的约会信息被隐藏了起来，即使有人截获了这封信，也不会知道私密信息的内容。这个协议很好地解决了第二个问题，也就是怎么能使消息不泄露的问题。

约会问题都解决了吗？

那么，其他的问题呢？我们依次来看一看其他的四个问题。

第一个问题，怎么证明双方的身份？

由于孩子的生辰八字只有孩子的父母知道，牛郎知道，只有织女才能读到这封信的约会信息；而织女也知道，只有牛郎才能这样加密约会信息。身份证明的问题，我们就解决了。

第三个问题，怎么防止内容被篡改？

由于这封信采用的是带关联数据的认证加密算法，无论是信的私密内容，还是公开内容，都没有办法被篡改而不被察觉。信息完整的问题，我们也解决了。

第四个问题，怎么确保信件能收到？

遗憾的是，这个问题并没有彻底地得到解决。由于发信人、收信人以及信件的真实内容都没有泄露，别人截留这封信件的动机可能就没有那么强烈，传递信件的喜鹊被请去吃酒喝茶的概率也会急剧减少。

即便喜鹊被请去了，也不能解密私密信息的内容，无论是利诱还是威逼，都没有用。这样，喜鹊的人身安全风险急剧降低。从这个方面说，这个方案提高了信件送达的概率。剩下的，就要依靠喜鹊飞越银河的能力了。

第五个问题，怎么防止翻脸不认账？

由于孩子的生辰八字只有孩子父母知道，织女收到信后，她就知道，只有牛郎才能这样加密约会信息。因为使用的是只有这两个人才知道的共同秘密，牛郎就不能抵赖说不是自己发送的信件。

可是，牛郎怎么能够确认，织女收到了信，还不能抵赖说没收到呢？我们把这个悬念留作今天的思考题。你可以好好地思考一下。

Take Away（今日收获）

今天，我们回顾了一下牛郎织女约会这个案例，讨论了怎么使用我们前面学过的密码学知识来解决具体的问题。

为什么我们讨论的方案能解决牛郎织女约会的问题呢？最基本的原因主要有两点：

使用共有的秘密，这样就解决了身份认证的问题；

使用 AEAD 算法，这样就同时解决了数据完整性问题和数据机密性问题，同时提高了信息的可用性。

其实，更流行的方案是使用基于非对称密钥的密钥交换技术，或者类似于 Kerberos 的基于对称密钥的密钥交换技术，不过，这两个方案都不在我们的讨论范围里。

有兴趣的话，你可以找一下相关的资料看一看，想一想基本的思路。今天的讨论，主要是让你感受一下密码学解决问题的思路，尤其是怎么把多种算法结合起来解决具体的问题。

思考题

正如我们前面提到的，今天我们讨论的方案，只能解决牛郎不能翻脸不认账的问题，还没有解决织女翻脸不认账的问题。织女翻脸不认账的问题，该怎么解决呢？

也就是说，牛郎怎么能够确认，织女收到了信，还不能抵赖说没收到呢？我们上述的方案该怎么改进？或者你有没有其他的方案？

欢迎在留言区留言，记录、讨论你的想法。

好了，课程到这里，就基本结束了，下一次我们结束语见。

结束语 | 深挖坑、广积粮

你好，我是范学雷。

我觉得自己是一个写字还算快的程序员。

从第一段文字开始，到着手写这一篇结束语，也有八九个月的时间了，还包括利用了近两个月的休假。如果当初我能够想象得到，每一讲的文章，都要写一两个星期的话，我是断断不敢有写这个专栏的一星一点的想法的。2020 年初，我一不小心给自己挖了一个大坑。

不过，现在回头看看，所有的付出时间都是有收获的。2020 年初挖的坑，我在 2020 年底还是给填上了。

珍惜踩过的坑

其实，我们踩过的每一个坑，最后都会变成我们脚底的一块砖。在我想着怎么把我记忆里的东西表达成文字的时候，在我想着怎么把复杂的东西用简单的形式叙述出来的时候，在我想着怎么呈现技术背后的基本原理和基础逻辑的时候，我都在积攒着对密码学更深的理解和认识。

对于我自己而言，密码学具有神奇的魔力。

我在这个领域已经工作了二十多年，一点儿也没有感到厌倦。密码学领域的工程实践，依然对我有巨大的诱惑力；其中的很多问题，也一直吸引着我继续在这个领域里去摸索。

我每一年都要看很多论文，每一年都要解决很多新问题，每一年又会发现很多新问题，几乎每一年都有完全不一样的挑战。也许，这就是密码学和密码学工程实践的魔力所在吧。

其实，别人踩过的每一个坑，也都可以变成我们脚底的一块砖。

不过，这需要我们去寻找，去分析，去汲取。相比较而言，密码学本身并不难学，困难在于追踪密码学的最新进展，了解密码分析的最新动向，知道什么时候该抛弃，知道什么问题该避免。

信息安全危机和数据泄漏的代价是巨大的，我们不能等着掉进坑里后，再去想怎么从坑里爬出来。所以，我们要关注、珍惜别人踩过的坑。如果说学习密码学有捷径的话，就是去深挖别人踩过的坑，让每一次公开的信息安全危机，都成为你脚底的一块砖。

积累学过的粮

珍惜自己和别人踩过的坑，还不够，我们还要给自己积累更多的粮。

比如说，看到了 Zoom 安全问题的报道，就不能仅仅把它当作一个故事，我们要去寻找更多的材料，去了解这个安全问题的技术细节，以及解决这个问题的具体方案。

我们可以创建自己的“禁止清单”和“爬坑清单”，在我们要去检查我们自己的产品有没有类似的问题时，给自己的“禁止清单”添加新的条目，给自己的“爬坑清单”添加新的场景和解决办法。

如果我们每一年都读很多论文，每一年都分析很多安全事故，每一年都解决很多新问题，每一年都遇到新挑战，毫无疑问地，我们的“禁止清单”就会越来越厚，“爬坑清单”也会越来越实用。

只要我们没有丧失兴趣，在这个领域的积累就会越来越多。我们站的越高，我们就有越多的机会看到更多的风景，包括很少人才能看到的风景。

深挖坑，广积粮，这是我送给你的学好密码学的一个小建议。

送君千里，终须一别

终于还是到了要说再见的时候了，但是这只是这个专栏的结束，却是你新的开始。希望这个专栏里提出的问题，给你思路和启发，让你在之后的密码学学习中，发现新的问题。

我说过，密码学就是在一个又一个的问题里蓬勃发展的，不管你以后遇到多么艰深的困难，我都希望你可以停下来思考一小会儿，看看自己有没有什么想法，有没有更深入的见解。

送君千里，终须一别。希望这一路走来，曾有文字和观点可以让你开心和愉悦，也许只是一个小思路的点拨，也许只是一个新的知识点，还有可能是那只喝茶吃酒的喜鹊。

无论怎样，都是你与我的宝贵回忆，有幸与你相遇在这个专栏，走过这样一段路。写的文字有人读，然后才能有想法的传递。谢谢你阅读我写的文字！

如果他日有缘相遇，我们再叙衷肠。

《实用密码学》课程结束了，这里有一份毕业问卷，题目不多，希望你能花两分钟填一下。十分期待能听到你说一说，你对这个课程的想法和建议。

结课测试 | 这些密码学的知识，你都掌握了吗？

你好，我是范学雷。

到这里，《实用密码学》这门课程已经全部结束了。我给你准备了一个结课小测试，来帮助你检验自己的学习效果。

这套测试题共有 20 道题目，都是单选题，满分 100 分，系统自动评分。

还等什么，点击下面按钮开始测试吧！