密码学之分组密码

1.1.1 从古典密码到分组密码

现代分组密码的奠基人是德裔美国密码学家Horst Feistel 。20世纪60年代末,IBM为英国的劳埃德银行(Lloyds Bank)开发一套现金自动存取系统,为了保护敏感的金融数据,IBM需要一种可靠的加密方法。为此,Feistel领导的团队开发出了一款名为 "路西法"(Lucifer) 的加密算法,该算法通常被认为是第一个现代意义上的商用分组密码,其设计已具备现代分组密码的雏形。

1973年,美国国家标准局(NBS,即后来的NIST)公开征集国家标准加密算法。IBM提交了改进后的"路西法"算法,经过评估和采纳,该算法在1977年被正式采纳为数据加密标准(DES),成为历史上第一个被官方认可的公开分组密码标准,对后续密码学发展产生了深远影响。

Feistel设计的DES算法具体又是什么样?在正式讨论分组密码的设计之前,让我们先回到密码学最古老的形式------替换密码

前文的密码学历史中,我们知道古典密码代表凯撒密码 ,是把每个字母向后移动三位:A变成D,B变成E,C变成F。后来,阿拉伯密码学家发明了更通用的单表替换密码 ------A可能变成X,B可能变成C,C可能变成Q......只要保证每个字母唯一对应一个替换字母,接收者就能反向查表还原出原文。

这种替换的本质是映射:把26个字母的集合,一对一地映射到另一个26个字母的集合上。每一个替换表,就是一个映射。

这个简单的模型,其实已经包含了分组密码的全部数学本质。现代分组密码,只是做一个 "放大"操作 :把字母换成比特串。把单个字母换成 nn n位的二进制分组------比如64位或128位。于是,替换表就不再是26个字母之间的映射,而是 2n2^n 2n个可能明文分组与 2n2^n 2n个可能密文分组之间的一一映射。

现在,请你把自己想象成一位密码设计师。你的任务是:构造一个从 nn n位明文到 nn n位密文的可逆映射,保证每个明文唯一对应一个密文。

你会怎么做?

最直接的想法是这样的:按照古典替换密码的思路,列一张大表,把每一个输入的替换结果都写清楚------只是不再是26个字母,而是输入有 2n2^n 2n个比特,表更大一些而已。把所有可能的映射关系全部列出来,加密时根据密钥从中选择一种来用。每一个明文都随机对应一个独一无二的密文,没有任何规律可循。攻击者即使拿到了海量的明文-密文对,也无法推测出其他明文的密文。

这个方案叫做理想分组密码(Ideal Block Cipher, IBC)

1.1.2 理想分组密码:完美但不可行

理想分组密码具有五个令人向往的特性:

第一,随机置换。 每个密钥对应一个从所有明文到所有密文的一一映射,这个映射对于不知道密钥的人来说完全不可预测。就算攻击者手里有一百万个明文-密文对,也无法预测下一个明文的密文是什么------因为剩下的映射方式仍然多得数不清。

第二,确定性与雪崩效应。 同一个密钥和同一个明文,永远产生同一个密文------这是可重复性的基本要求。但是,只要密钥或者明文哪怕只改变一个比特,密文就应当面目全非、毫无相似之处。这种"微小输入变化导致巨大输出变化"的性质被称为雪崩效应,它让攻击者无法通过观察输入输出的相似性来寻找规律。

第三,可逆性。 加密后再解密,必须还原出原始明文。这是所有实用密码系统的底线------如果连这都做不到,加密就失去了意义。

第四,足够大的密钥空间。 密钥空间要大到让暴力破解------也就是把所有可能的密钥都试一遍------成为不可能完成的任务。

第五,伪随机性。 密文看起来必须像完全的随机数,没有任何模式或规律可循。不管攻击者做多少次统计分析,都找不到明文的任何痕迹。

而最"完美"的地方在于:对于 nn n位的分组密码,所有可能的置换共有 (2n)!(2^n)! (2n)!种。这个数字有多大?我们来算一下: nn n位二进制数可以表示 2n2^n 2n个不同的输入。第一个明文有 2n2^n 2n个密文可选,第一个选其中做映射之后,第二个有 2n−12^n-1 2n−1个可选......依此类推,总共就是:

(2n)×(2n−1)×⋯×1=(2n)!(2^n) \times (2^n-1) \times \cdots \times 1 = (2^n)! (2n)×(2n−1)×⋯×1=(2n)!

种置换。理想分组密码的每一个密钥,就是在这些置换中随机抽取一个来使用。如果这个方案可行,密码学问题就一劳永逸地解决了。我们只需要随机挑一个置换,把所有的明文-密文对应关系写在一本 "大账本" 里,加密时查表即可。

映射本身不是问题,问题在于怎么记住这个映射?

对于古典替换密码,替换表只有26项,记住一张表并不难,甚至可以编成口诀。

但对于一个 n=64n=64 n=64的理想分组密码,这本"账本"里需要记录 2642^{64} 264对映射关系。每对映射需要记录一个64位的密文,所以账本的总大小是:

64×264 位64 \times 2^{64} \text{ 位} 64×264 位

这个量级有多大? 2642^{64} 264约等于 1.84×10191.84 \times 10^{19} 1.84×1019,也就是1.84亿亿。银河系中大约有1000亿到4000亿颗恒星, 2642^{64} 264是银河系恒星数量的几百倍。这还只是64位------如果分组长度是128位,账本的大小将是 128×2128128 \times 2^{128} 128×2128位,这个数字大到连宇宙中所有原子加起来都不够用。

所以说,理想分组密码的困境并非来自"如何构造映射" ------真正的困境是:如何把选中的这个映射"告诉"加密者和解密者? 换句话说,密钥就是对这个映射的描述。如果描述这个映射需要海量的存储空间,那这个"密钥"本身就大得无法传输、无法存储、无法使用。

这就引出了一个深刻的矛盾:

  • 如果 nn n太小(比如 n=4n=4 n=4或 n=8n=8 n=8),账本很小,密钥很短------但密文空间太小,攻击者用频率分析就能轻松破解,就像破解一个字母替换表那样简单。
  • 如果 nn n足够大(比如 n=64n=64 n=64或 n=128n=128 n=128),统计特征被掩盖了------但账本大到根本存不下,密钥长到根本没法用。

理想分组密码就像物理学中的"理想气体"或"无摩擦平面":理论上完美无缺,现实中却无法实现。它注定是一个理论上的标杆,而不是工程上的方案。

但话又说回来------理想虽然达不到,但确实是实现的方向。 密码学家要做的事情,不是在现实中强行实现那个"完美"的全映射,而是换了一个思路:

不再用"账本"来记住映射,而是用"算法"来生成映射。

也就是说,加密者和解密者只需要共享一个短小的密钥(比如128位),然后双方运行同一个算法,动态地计算出每个明文对应的密文。这样一来,记住一个128位的密钥很容易,而算法本身------经过精心设计------可以模拟出(至少是逼近)一个随机置换的效果。

如何做到这一点?这就要说到香农给出的答案了------混淆与扩散。这是所有现代分组密码真正的灵魂。

1.1.3 混淆与扩散

香农在1949年的那篇奠基性论文中提出了一个关键洞察:密码系统的致命弱点,是明文的统计特征。

我们在频率分析攻击的介绍中已经知道,任何有意义的信息------无论是英文、中文还是二进制数据------都不是均匀分布的。英文中字母"e"出现的概率远高于"z",中文里"之乎者也"这类虚词在某些文体中反复出现,就连一段程序代码中某些指令字节也会比其他字节更频繁。如果一个加密系统只是简单地对数据进行替换或变换,而没有刻意去抹掉这些指纹,那么攻击者就可以通过统计分析来破解它。他不需要知道密钥,只需要观察密文中某些字符或比特出现的频率,就能推断出明文的模样------甚至直接猜出密钥。

香农意识到,要对抗频率分析攻击,必须做两件事:

  1. 消除明文与密文之间的任何统计关联
  2. 把明文的每一个细节扩散到密文的各个角落去

这两件事,就是他所说的混淆扩散

混淆(Confusion)

混淆要解决的问题很直接:让攻击者无法从密文中推导出明文和密钥之间的任何关系。

具体来说,就是让密文的统计结构和明文的统计结构尽可能独立------明文中某个字符出现的频率,不在密文中留下任何可识别的痕迹。同样,密钥中的任何一个比特,也不应该单独决定密文中的某一位。即使攻击者知道了密钥的某一部分,也无法据此推断出明文的任何信息。

其实,这个思想在古典密码时期就已经有了朴素的实践。还记得我们在第一章中提到的维吉尼亚密码吗?它就是古典密码学家对"混淆"的一次早期尝试。

在单表替换密码中,明文字母和密文字母之间是一一对应的关系------"a"永远变成"x","e"永远变成"b"。这种一对一的固定映射,使得明文中字母出现的频率特征完整地传递到了密文中。攻击者只需要统计密文中哪个符号出现次数最多,就能大致猜出它对应的是哪个字母。

维吉尼亚密码则换了一个思路:不再使用一个固定的替换表,而是同时使用多张替换表,轮换着用。 明文中第一个字母用第1张表替换,第二个字母用第2张表替换,第三个字母用第3张表替换......到了第 nn n个字母后再回到第1张表,如此循环。至于具体使用哪一张表、如何轮换,则由密钥来决定。

这样一来,同一个明文字母在密文中就不再是同一个符号了。在英文文本中出现频率最高的字母"e"------虽然在明文中反复出现,但每一次被替换时,因为轮到了不同的替换表,而被映射成完全不同的密文字母。第一次是"x",第二次是"m",第三次是"q"......"e"的频率特征被分散到了多个不同的密文字母中,攻击者再数频率时,已经找不到统计规律。

这就是古典密码对"混淆"的朴素理解:用多表替换来抹平明文的统计指纹。

当然,维吉尼亚密码的混淆能力并不完美。它的替换表数量比较有限,而且替换表之间的轮换是有规律的(周期性的)。只要攻击者猜出了密钥长度,就可以把密文按周期分组------每一组实质上又变回了单表替换------频率分析仍然可以派上用场。但即便如此,维吉尼亚密码在其时代仍被称为"不可破解的密码",这足以说明混淆思想的价值------它第一次让密码学家意识到:仅仅把字母"换名字"是不够的,必须把频率特征真正"搅散"才行。

在现代密码中,"多表替换"的思路被继承并进化到了一个全新的高度。它的现代版本就是S盒(Substitution Box) ------同样是一张精心设计的替换表,输入和输出之间不再是简单的线性对应关系,而是极其复杂的非线性 映射。S盒的设计目标就是确保输入数据的任何微小变化,都会引起输出数据不可预测的巨大变化,从而彻底抹去明文的统计痕迹。不同之处在于,古典维吉尼亚密码的替换表是线性轮换的(表1→表2→表3......),而S盒的替换规则是非线性的------因为没有简单的规律可循,攻击者无法通过观察少量输入输出对来推测S盒的内部结构。

而在实际的分组密码算法(如DES、SM4、AES)中,S盒往往还要与密钥混合配合使用------数据先与子密钥异或,再送入S盒进行替换。这样一来,即使S盒本身是公开的、固定的,但由于每一轮进入S盒的数据都混入了不同的子密钥,实际的替换效果也随着轮次而变化------相当于每一轮都在使用一张"不同的"替换表,而且这个"不同"是由密钥驱动的、非线性的、无规律的。

关于S盒的具体原理,我们在后面的章节中会进一步讲述。

扩散(Diffusion)

扩散要解决的问题,则是另一个方面:让明文的任何微小变化,都能波及到密文的大片区域。

换句话说,明文的每一位都应该尽可能多地影响密文的每一位------反过来,密文的每一位也应该尽可能多地受到明文中多个位的影响。这样做的效果是:哪怕明文只改动了一个比特,密文也会发生天翻地覆的变化,像推倒了多米诺骨牌一样牵一发而动全身。这个现象有一个形象的名字------雪崩效应

在算法层面,扩散通常通过置换(Permutation)、**移位(Shift)**这样的操作来实现,把数据的位置打乱、重新排列,让信息从一个点传播到多个点。反复进行多轮操作,扩散效应会不断叠加,最终让每一个明文字节都影响到密文中的所有字节。

如果只有混淆而没有扩散,那么明文的统计特征虽然被非线性变换打乱了,但可能还残留在数据的局部结构中,攻击者仍然可能找到规律。而如果只有扩散而没有混淆,虽然明文的变化会波及整个密文,但由于变换本身可能是线性的,攻击者可以用线性代数的方法轻松反推出变换规则。只有把两者结合起来,才能彻底抹去明文的统计指纹,达到安全加密的目的。

混淆与扩散如何逼近理想分组密码?

现在我们来理解:怎么通过混淆和扩散来靠近理想分组密码?

理想分组密码用一张大到无法想象的"账本"记录了所有可能的映射,每一个密钥就是一个随机置换------那是混淆的极致,但工程上完全不可行。

现实中的分组密码没有这张账本,而是用一个算法来模拟那个"账本"的效果。这个算法的核心,就是混淆与扩散的反复迭代:每一轮加密,数据先经过非线性变换(S盒),再经过位置重排(P盒),然后与子密钥混合。如此重复若干轮,每一轮都在进一步"抹去"明文的统计特征,进一步"扩散"每一个比特的变化。

一轮操作可能不够,两轮也许仍然不够,但经过十几轮、几十轮的反复"搅拌"和"扩散"之后,明文的每一比特都渗透到了密文的每一个角落,且与密钥之间形成了极度复杂的依赖关系。最终的效果,虽然从数学上不等于真正的随机置换,但在实际的计算能力下,已经足以逼近理想分组密码的安全性了。

这就是现代分组密码的全部秘密:用有限的计算,模拟无限的随机。 而这个模拟的工具箱里最核心的两样工具,就是香农在七十多年前提出的混淆与扩散。

当然,混淆和扩散只是解决了"怎么靠近理想的随机置换",但没有解决可逆性。所以,除了混淆和扩散,密码学家还需要一样东西:

一种结构------让加密过程无论多么复杂、轮函数无论是否可逆,都能保证整个密码系统是可逆的。

这就是Feistel网络要做的事。

1.1.4 Feistel密码结构

Feistel借鉴香农的混淆与扩散理念,借助乘积密码 的思想,设计出一种通过多轮迭代轮函数 来同时实现混淆、扩散和可逆性的密码结构,这种结构被称为Feistel密码结构

Feistel首次将其应用于IBM的Lucifer 密码中。1976年,美国联邦政府采纳了基于Lucifer修改而来的DES算法 ,Feistel结构由此获得了广泛的关注与认可,深刻影响了此后几十年的密码学发展。这一结构不仅被DES采用,还被RC5、FEAL、GOST、LOKI 等多种算法沿用。我们国家的SM4算法 也采用了Feistel结构的变体------非平衡Feistel结构(左右两半长度不等),我们将在后续相关章节中详细阐释。

下面,我们来逐层拆解Feistel网络的工作原理。

一、Feistel一轮加密:半数参与,半数传递

Feistel网络的输入是 2w2w 2w位的明文分组(图中以64位为例,左右各32位)。首先将其等分为左右两半: L0 L_0 L0和 R0 R_0 R0,分别从左侧和右侧进入网络。一轮加密(round)包含以下步骤:

  1. 右半部分直接传递 :将输入数据的右半部分 Ri−1 R_{i-1} Ri−1直接复制到输出的右侧,记为 Ri R_i Ri,内容保持不变。
  2. 轮函数运算 :将 Ri−1 R_{i-1} Ri−1送入轮函数 ff f ,轮函数结合当前轮的子密钥 Ki K_i Ki ,经过一系列运算后,输出一串比特序列 f( Ri−1 ,Ki) f(R_{i-1}, K_i) f(Ri−1,Ki)。
  3. 异或运算 :将轮函数的输出与输入的左半部分 Li−1 L_{i-1} Li−1执行异或(XOR) 操作。
  4. 左半部分更新 :将异或的结果作为输出的左半部分 Li L_i Li。

用数学公式表示,一轮运算就是:

Li= Ri−1 L_i = R_{i-1} Li=Ri−1 Ri= Li−1 ⊕f( Ri−1 ,Ki) R_i = L_{i-1} \oplus f(R_{i-1}, K_i) Ri=Li−1⊕f(Ri−1,Ki)

其中 Ki K_i Ki是第 ii i轮使用的子密钥(subkey) 。每一轮都需要使用一个不同的子密钥,它们由主密钥通过特定的密钥扩展算法生成。由于每个子密钥只在其所属的那一轮中使用,作用范围局限于单轮,因此被称为"子"密钥------它们只是全局密钥的"局部代理人"。

整个加密过程就是上述一轮运算的多次循环迭代。例如,DES执行16轮,SM4执行32轮。

我们注意到:每一轮中,只有一半的数据位(左半部分)参与了轮函数的计算,另一半(右半部分)原封不动地直接传递到下一轮。 初次接触的读者可能会产生疑问:这样的设计不是在浪费计算资源吗?对全部数据位都进行加密处理,难道不会更安全吗?

对于这个问题,我们将在讲解解密原理时给出答案。到时候你会发现,正是这个看似"浪费"的半数传递设计,才成就了Feistel结构加解密高度对称的精妙特性。

二、Feistel解密:同样的流程,相反的顺序

Feistel结构最精妙之处在于解密方式:解密过程与加密过程完全一样,唯一不同的是子密钥的调用顺序相反。

具体来说:

  • 加密时各轮使用的子密钥依次为: K0,K1,K2,..., Kn−1 K_0, K_1, K_2, \ldots, K_{n-1} K0,K1,K2,...,Kn−1
  • 解密时各轮使用的子密钥依次为: Kn−1 , Kn−2 ,...,K1,K0 K_{n-1}, K_{n-2}, \ldots, K_1, K_0 Kn−1,Kn−2,...,K1,K0

顺序倒过来,除此以外,所有运算步骤完全相同。另外,输入从明文换成密文,按照同样的流程走一遍,这样输出就是原始明文。

这听起来有些不可思议------我们用一个具体的数值例子来验证。

假设明文长度为8位(左右各4位),子密钥 K=0101K = 0101 K=0101。为了便于手算演示,假设轮函数 ff f仅为异或运算。在实际的密码算法(如DES、SM4)中,轮函数要复杂得多------通常包含S盒非线性替换、P盒位置置换、密钥混合等多种运算的组合------但无论轮函数多复杂,Feistel网络的可逆性原理与这里的演示完全一致。

加密过程:

  1. 输入明文 11001010 ,等分为左右两部分,左边 L0=1100 L_0 = 1100 L0=1100,右边 R0=1010 R_0 = 1010 R0=1010
  2. 右半部分直接输出: R1=1010 R_1 = 1010 R1=1010
  3. 轮函数运算: f(R0,K)=1010⊕0101=1111 f(R_0, K) = 1010 \oplus 0101 = 1111 f(R0,K)=1010⊕0101=1111
  4. 更新左半部分: L1=L0⊕f(R0,K)=1100⊕1111=0011 L_1 = L_0 \oplus f(R_0, K) = 1100 \oplus 1111 = 0011 L1=L0⊕f(R0,K)=1100⊕1111=0011
  5. 这一轮加密结果: L1∥R1=00111010 L_1 \parallel R_1 = 00111010 L1∥R1=00111010

解密过程:

  1. 输入密文 00111010 ,同样等分为左边 L0′=0011 L_0' = 0011 L0′=0011,右边 R0′=1010 R_0' = 1010 R0′=1010
  2. 右半部分直接输出: R1′′ =1010 R_1'' = 1010 R1′′=1010
  3. 轮函数运算(使用相同子密钥): f(R0′,K)=1010⊕0101=1111 f(R_0', K) = 1010 \oplus 0101 = 1111 f(R0′,K)=1010⊕0101=1111
  4. 更新左半部分: L1′′ =L0′⊕f(R0′,K)=0011⊕1111=1100 L_1'' = L_0' \oplus f(R_0', K) = 0011 \oplus 1111 = 1100 L1′′=L0′⊕f(R0′,K)=0011⊕1111=1100
  5. 解密结果: L1′′ ∥ R1′′ =11001010 L_1'' \parallel R_1'' = 11001010 L1′′∥R1′′=11001010,与原始明文完全一致。

为什么能成功还原?我们来做一个符号推导。

解密第四步有:

L1′′ =L0′⊕f(R0′,K) L_1'' = L_0' \oplus f(R_0', K) L1′′=L0′⊕f(R0′,K)

注意, L0′ L_0' L0′实际上等于加密阶段输出的 L1=L0⊕f(R0,K) L_1 = L_0 \oplus f(R_0, K) L1=L0⊕f(R0,K)(因为把加密阶段的输出作为输入),而 R0′ R_0' R0′等于加密阶段的 R0 R_0 R0(因为右半部分在加密时直接被传递了下来)。代入得:

L1′′ =L0⊕f(R0,K)⊕f(R0,K) L_1'' = \big L_0 \\oplus f(R_0, K) \\big \oplus f(R_0, K) L1′′=L0⊕f(R0,K)⊕f(R0,K)

根据异或运算的性质 A⊕B⊕B=AA \oplus B \oplus B = A A⊕B⊕B=A,上式化简为:

L1′′ =L0 L_1'' = L_0 L1′′=L0

解密成功恢复了原始的左半部分,而右半部分自始至终都原样传递------因此整个明文被完整还原。

这个推导揭示了一个关键洞察:Feistel网络的可逆性不依赖轮函数 ff f是否可逆,而是完全依赖于异或运算的性质。 无论 ff f多么复杂、甚至不可逆,只要加密时做了"左半部分 ⊕\oplus ⊕某值",解密时再做一次同样的异或,就能抵消掉------因为 A⊕B⊕B=AA \oplus B \oplus B = A A⊕B⊕B=A。而右半部分因为从未被修改过,可以直接作为解密的右半部分输出。

这,就是为什么每一轮只有一半数据参与运算。 这不是设计的缺陷,而是刻意为之的设计。那一半不参与运算的数据,充当了"信使"的角色------它原封不动地穿越一轮,在解密时帮助另一半恢复原状。如果让全部数据都参与轮函数运算,反而会破坏这种可逆对称性,导致解密无法用同一套结构完成。

换言之,Feistel用"一半运算、一半传递"的模式,把"可逆性"这个难题从轮函数内部 转移到了结构层面------轮函数可以任意复杂,结构保证可逆。

三、混淆与扩散在Feistel结构中的分工

现在,让我们回到本节开头的问题:Feistel结构是如何实现混淆与扩散的?

混淆的实现 ------主要依赖轮函数 ff f的内部构造 。一般的轮函数都包含复杂的非线性变换 (如S盒操作),能够将输入数据以高度非线性的方式映射为输出,使输入的微小变化引发输出的剧烈且不可预测的改变。同时,轮函数在运算过程中会将子密钥与数据混合,且各轮使用不同的子密钥------即使攻击者掌握了某些明文-密文对应关系,也难以逆向推导加密过程中的具体变换细节。非线性S盒与密钥混合,共同构成了混淆的基石。

扩散的实现 ------依赖Feistel结构左右交替更新的迭代机制。每一轮中,右半部分经轮函数变换后与左半部分异或,产生新的左半部分;而原来的左半部分则成为下一轮的右半部分。这种左右互换的模式,使得明文中任何一位的信息,都会在多轮迭代中逐步"蔓延"到整个密文。这就像搅拌机,反复对数据进行搅拌------经过多轮(如DES的16轮)加密后,明文的微小改变最终会使密文的大部分乃至全部位都产生变化。

所以,Feistel结构将三项任务清晰地划分开来:

任务 实现方式
混淆 轮函数内部的非线性变换(S盒)+ 密钥混合
扩散 左右交替传递 + 多轮迭代
可逆性 半数直接传递 + 异或运算

四、Feistel密码结构的特点总结

我们来总结一下Feistel密码结构的特点:

  • Feistel是设计框架,不是具体算法。 Feistel结构通过"半数传递 + 异或"的设计,在保证可逆性的前提下,将混淆和扩散的任务完全交由轮函数承担,为设计者提供了一个高度灵活的蓝图。

  • 轮函数可以任意复杂。 由于Feistel的可逆性完全不依赖轮函数的性质,而仅依赖异或运算的自反性( A⊕B⊕B=AA \oplus B \oplus B = A A⊕B⊕B=A),因此无论轮函数的具体形式如何------即使它本身不可逆------Feistel网络都能保证正确解密。设计者无需考虑解密约束,可以专注于把混淆和扩散做到极致。

  • 子密钥生成算法独立可设计。 密钥长度是算法设计时可以灵活选定的参数,较长的密钥提供更高的安全性,但会增加时间开销。设计者可以根据实际需求,在安全性与性能之间做出权衡。此外,子密钥生成算法的复杂度也直接影响密码分析的难度,设计足够复杂的密钥扩展算法是提升整体安全性的重要手段。

  • 轮数可以任意增加。 无论执行多少轮,Feistel网络都能正确解密。增加轮数能显著提高抗分析能力,但时间开销也随之增大------同样需要设计者根据安全需求与性能要求进行权衡。

  • 分组长度可调。 分组长度越长,安全性越高,但加解密的时间开销也越大。设计者需要在安全性与效率之间选择合适的平衡点。

  • 加解密结构完全对称。 加密和解密使用完全相同的硬件电路或软件模块,只需反转子密钥的输入顺序。这极大地简化了硬件实现,降低了芯片设计的成本。

总而言之,无论轮数如何设定、密钥与数据长度为多少,也不管轮函数具体为何种形式------Feistel网络都能用同一套结构同时完成加密与解密,并确保加密结果必然可以被正确还原。

正是这种特质,使得Feistel网络获得了众多分组密码算法的青睐。理解了Feistel网络之后,再去学习基于它的具体算法(如DES、SM4)就会轻松许多------因为它们本质上都是在同一个骨架上填充不同的肌肉(轮函数设计、密钥长度、子密钥生成方式等)。

1.1.5 DES:数据加密标准算法

上一节我们拆解了Feistel网络的工作原理。现在,我们来看第一个基于Feistel网络实现的分组加密算法:DES算法

它的前身是IBM在20世纪70年代初设计的Lucifer密码,经过修改后提交给美国国家标准局,1977年正式成为联邦信息处理标准。此后二十多年里,DES在金融、通信、政府等领域被广泛使用。

1.1.5.1 参数设定

Feistel网络允许设计者自由设定五个参数。DES的选择如下:

  • 分组长度:64位。
  • 密钥长度:64位输入,其中8位是奇偶校验位,不参与加密。有效密钥56位。
  • 轮数:16轮。
  • 子密钥生成算法:从56位主密钥生成16个48位子密钥。
  • 轮函数:包含扩展置换、异或、S盒替换、P盒置换四个步骤。

除此之外,DES在Feistel网络的前后各加了一次置换:初始置换IP最终置换IP⁻¹ 。这两个操作互为逆运算,不改变数据的值,只改变位的位置。它们不贡献安全性,主要目的是方便硬件实现------让数据在进入芯片时按特定顺序排列。

1.1.5.2 IP置换(初始置换)

初始置换IP是一张8×8的表,规定了64个输入位如何重新排列。例如,第一行第一列是58,表示把明文的第58位 移到第1位 ;第一行第二列是50,表示把第50位 移到第2位......依次类推。

58 50 42 34 26 18 10 2
60 52 44 36 28 20 12 4
62 54 46 38 30 22 14 6
64 56 48 40 32 24 16 8
57 49 41 33 25 17 9 1
59 51 43 35 27 19 11 3
61 53 45 37 29 21 13 5
63 55 47 39 31 23 15 7

最终置换IP⁻¹是初始置换的逆操作。如果初始置换把第58位移到了第1位,最终置换就把第1位移回第58位。两张表互为逆运算。

我们注意到,这张表有一个规律:上半部分全是偶数位(58、50、42......),下半部分全是奇数位(57、49、41......)。经过IP置换后,64位数据中原来的所有偶数位全部被挪到了前32位,所有奇数位全部被挪到了后32位。

为什么这样设计?这与DES诞生的时代背景有关。1970年代,加密运算主要由专用硬件芯片 完成,数据通过物理引脚进入芯片------Pin 1、Pin 2、......一直到Pin 64。如果芯片要求的数据输入顺序和系统总线的传输顺序不一致,就需要在芯片外部额外增加交叉连线来调整,这会增加电路板的复杂度和成本。IP置换的规律性------把偶数位和奇数位分开------可以让芯片内部的布线更加规整,偶数位走一路、奇数位走另一路,两条路径并行处理,减少了交叉走线的数量。

初始置换和最终置换是两个固定位置重排 操作,它们不改变数据的值,只改变比特的位置。它们不贡献任何安全性(规则是公开的,攻击者随时可以还原),主要目的是让数据在进入芯片时按照硬件设计者期望的位序排列,以简化芯片内部的布线结构

1.1.5.3 子密钥生成过程

DES从56位有效密钥生成16个48位子密钥,每个子密钥用于一轮。生成过程分三步:

第一步:初始密钥的置换选择(PC-1)

首先,对64位的初始密钥进行奇偶校验位的剔除。这64位密钥中,第8、16、24、32、40、48、56、64位是奇偶校验位,将它们舍弃后得到56位的有效密钥。

然后,对这56位有效密钥进行置换选择操作(Permuted Choice 1,简称PC-1 )。PC-1置换表同前述的初始置换一样,也是一个固定的置换表,规定了如何从56位密钥中选取和重新排列位,之后将其分为左右两个28位的数据块,分别记为 C0 C_0 C0和 D0 D_0 D0。

PC-1置换表如下:

57 49 41 33 25 17 9 1 58 50 42 34 26 18
10 2 59 51 43 35 27 19 11 3 60 52 44 36
63 55 47 39 31 23 15 7 62 54 46 38 30 22
11 3 61 53 45 37 29 21 13 5 28 20 12 4

注意到一个规律:每一行内部,相邻数字之间都相差8。 也就是说,PC-1表其实就是把56个位置按间隔8 的方式重新排列。这样排列的目的是对密钥进行扩散,让 C0 C_0 C0和 D0 D_0 D0各包含来自原始密钥各个位置的比特,且尽可能均衡。

如果不是按间隔8排列,而是简单地"前28位给 C0 C_0 C0、后28位给 D0 D_0 D0",那 C0 C_0 C0就只包含原始密钥的低28位, D0 D_0 D0只包含高28位------这会降低安全性。攻击者如果知道了 C0 C_0 C0,就等于知道了原始密钥的一半。

第二步:循环左移操作

循环左移的任务是:让每一轮使用的子密钥都不同。

对于每一轮迭代 ii i( i=1,...,16i=1, \ldots, 16 i=1,...,16), Ci−1 C_{i-1} Ci−1和 Di−1 D_{i-1} Di−1分别进行循环左移操作。循环左移的位数取决于轮数 ii i,在不同的轮数中,循环左移的位数有所不同。例如,在第1、2、9、16轮中,循环左移1位 ;在其他轮中,循环左移2位。由此派生出的16个子密钥各不相同。

如果去掉循环左移,那每一轮的 CC C和 DD D都是同一个值,16轮的子密钥完全一样------加密强度大大降低。循环左移让子密钥在每一轮都有变化。

第三步:置换选择(PC-2)生成子密钥

经过循环左移操作后得到 Ci C_i Ci和 Di D_i Di,将它们合并(共56位),然后进行置换选择操作(Permuted Choice 2,简称PC-2 )。PC-2置换表从这56位中选取48位,生成该轮所需的48位子密钥。

PC-2置换表如下:

14 17 11 24 1 5
3 28 15 6 21 10
23 19 12 4 26 8
16 7 27 20 13 2
41 52 31 37 47 55
30 40 51 45 33 48
44 49 39 56 34 53
46 42 50 36 29 32

例如,第一行表示选择了输入密钥中的第14、17、11、24、1和5位,并将它们作为输出子密钥的前6位。以此类推。这48位子密钥将用于该轮的加密操作,整个DES加密过程中会生成16个48位子密钥,分别用于DES算法中的16轮加密过程。不同的轮加密所使用的密钥都不相同,增加了攻击者破解难度。

这一步置换,仍然是为了把顺序再次打散 。PC-2从 CC C和 DD D的56位中挑选48位,而且这些位的排列顺序也被重新安排了。即使攻击者知道了某个子密钥的某一位,也不知道这个位在 C+DC+D C+D中对应的具体位置,更不用说在原始密钥中的位置了。

子密钥生成的流程总结:

步骤 做什么 为什么
PC-1置换 打散56位,平分为两半 C0 C_0 C0和 D0 D_0 D0都混合了原始密钥各处的比特
循环左移 每轮移动不同位数 让每一轮子密钥都不同
PC-2置换 56位中选48位,再打散 压缩长度,同时增加位置的不确定性

每一轮都重复这个流程,得到 K1,K2,...,K16 K_1, K_2, \ldots, K_{16} K1,K2,...,K16。

这样做的最终结果是:16个子密钥互不相同,且每个子密钥的每一位都与原始密钥的多个位相关。攻击者即使拿到了一个子密钥,也无法反推出主密钥;即使拿到了多个子密钥,也无法通过比较它们之间的差异来推断主密钥的结构。

1.1.5.4 轮函数

DES的轮函数接收32位数据和48位子密钥,输出32位。它包含四个步骤:扩展置换(E盒)→ 与子密钥异或 → S盒替换 → P盒置换 。下面逐一拆解。

第一步:数据扩展置换(E盒)

轮函数的输入是32位,但子密钥是48位------32位和48位没法做异或。所以第一步是把32位扩展到48位。

扩展的方法很简单:把32位中的某些位复制一份,然后重新排列,凑成48位。扩展置换表如下:

32 1 2 3 4 5
4 5 6 7 8 9
8 9 10 11 12 13
12 13 14 15 16 17
16 17 18 19 20 21
20 21 22 23 24 25
24 25 26 27 28 29
28 29 30 31 32 1

置换表的读法是:第一行第一列是32,表示把输入数据的第32位 复制到输出数据的第1位 ;第一行第二列是1,表示把第1位 复制到第2位......依此类推。

观察这张表可以发现:每个输出位来自输入位本身或其相邻位。比如第1位输出来自输入第32位(左邻),第2位输出来自输入第1位(自身),第3位输出来自输入第2位(右邻)。扩展置换实际上是把32位的数据按照分块的方式扩展------分成8组,每组4位,然后在每组的两侧各补上相邻组的一位,形成8组6位的数据,正好48位。

扩展置换完成了三件事:

  1. 32位变成48位,与子密钥长度对齐
  2. 某些位被复制了一份,使得它们在后续S盒中会进入不同的S盒,加速扩散
  3. 输入数据中某一位的变化,在扩展后能影响到两个S盒的输出

第二步:子密钥异或操作

得到扩展后的48位数据后,将其与该轮对应的48位子密钥进行异或操作。

第三步:S盒替换(S-boxes)

异或后的48位数据分成8组 ,每组6位,分别送入8个不同的S盒。每个S盒输入6位、输出4位。

S盒是一种非线性的替换操作。实际上,每个S盒是一个4行×16列的查找表。6位输入记作 b1b2b3b4b5b6 b_1 b_2 b_3 b_4 b_5 b_6 b1b2b3b4b5b6,其中 b1b6 b_1 b_6 b1b6组成行号(03), b2b3b4b5 b_2 b_3 b_4 b_5 b2b3b4b5组成列号(015),从S盒表中取出对应数值作为4位输出。

以S1盒为例:

行\列 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 14 4 13 1 2 15 11 8 3 10 6 12 5 9 0 7
1 0 15 7 4 14 2 13 1 10 6 12 11 9 5 3 8
2 4 1 14 8 13 6 2 11 15 12 9 7 3 10 5 0
3 15 12 8 2 4 9 1 7 5 11 3 14 10 0 6 13

输入101011 b1b6=11 b_1 b_6 = 11 b1b6=11(行号3), b2b3b4b5=0101 b_2 b_3 b_4 b_5 = 0101 b2b3b4b5=0101(列号5),查表得第3行第5列的值为9 (二进制1001)。

8个S盒各输出4位,合并为32位。

S盒有两个关键性质:

  • 非线性:输入和输出之间没有线性关系,无法用线性方程表达
  • 不可逆:6位输入压缩到4位输出,信息丢失了

这两点使得S盒成为DES实现混淆的核心工具。初始置换只是重新排列位置,不改变值;S盒则改变了值本身,而且是不可逆的改变。

第四步:P盒置换(P-box)

最后,对32位的数据进行P盒置换操作。P盒置换根据一个固定的置换表对32位数据中的位进行重新排列,得到最终轮函数的输出结果。

16 7 20 21 29 12 28 17
1 15 23 26 5 18 31 10
2 8 24 14 32 27 3 9
19 13 30 6 22 11 4 25

P盒置换的本质也是位置重排------和IP、PC-1、PC-2是同一种操作。但它出现的位置决定了它的作用不同:P盒在每一轮中执行,目的是把S盒的输出位打散后重新分配,使得本轮产生的变化在下一轮能影响到不同的S盒。如果去掉P盒,S盒的输出位会在原地不动,下一轮进入同一个S盒------变化传播不出去,扩散效果就大打折扣。

轮函数的完整流程是:

32位 →扩展置换 48位 → ⊕Ki 48位 →S盒(8×6→4) 32位 →P盒 32位 32\text{位} \xrightarrow{\text{扩展置换}} 48\text{位} \xrightarrow{\oplus K_i} 48\text{位} \xrightarrow{\text{S盒(8×6→4)}} 32\text{位} \xrightarrow{\text{P盒}} 32\text{位} 32位扩展置换 48位⊕Ki 48位S盒(8×6→4) 32位P盒 32位

1.1.5.5 DES的特点与退役

最后,我们总结一下DES的特点:

  • 结构清晰:基于Feistel网络,加密和解密使用相同的流程,只需反转子密钥顺序。硬件实现时,加密和解密可以用同一套电路。
  • 轮函数设计:核心操作为扩展置换、与子密钥异或、S盒替换、P盒置换。其中S盒提供了非线性混淆,扩展置换和P盒配合16轮迭代提供了扩散。
  • 参数固定:分组长度64位,有效密钥56位,轮数16轮,子密钥长度48位。
  • 速度优势:在硬件实现中,DES的运算速度很快------这是因为移位和置换都是位操作,S盒是查表,这些都是硬件擅长的事情。

DES的主要缺点是密钥太短 。56位密钥的搜索空间是 2562^{56} 256,约 7.2×10167.2 \times 10^{16} 7.2×1016种。1977年这个数量是安全的,但计算能力的增长遵循摩尔定律。到1999年,电子前沿基金会制造的专用机器Deep Crack ,联合分布式计算平台上的数千名志愿者,22小时15分钟就破解了一个56位DES加密的消息。

此后,破解时间还在不断缩短。56位密钥已经不再提供任何实质性的安全保护。

1.1.5.6 DES的后续:3DES

DES的56位密钥被证明不够用之后,一个自然的想法是:把DES多做几次 ,用多个密钥加密多次,有效密钥长度就变长了。这就是3DES的基本思路。

但3DES并不是简单地"加密→加密→加密"。它的标准做法是:加密→解密→加密,如下图所示:

为什么中间要插一次解密?理论上三次都加密更安全,但实际上这么设计是为了兼容普通的DES 。如果三个密钥相同( K1=K2=K3 K_1 = K_2 = K_3 K1=K2=K3),那么加密→解密→加密的结果就等于一次普通DES加密:

  • 第一次加密: EK(P) E_K(P) EK(P)
  • 第二次解密: DK(EK(P))=P D_K(E_K(P)) = P DK(EK(P))=P(解密抵消了加密)
  • 第三次加密: EK(P) E_K(P) EK(P)

结果是 EK(P) E_K(P) EK(P),正好等于普通DES。这个设计的巧妙之处在于:使用3DES的硬件或软件,可以向下兼容只支持DES的老系统

3DES有两种密钥模式:

  • 双密钥3DES K1=K3 K_1 = K_3 K1=K3, K2 K_2 K2独立,有效密钥长度112位
  • 三密钥3DES K1,K2,K3 K_1, K_2, K_3 K1,K2,K3相互独立,有效密钥长度168位

三密钥3DES的有效密钥长度是 56×3=16856 \times 3 = 168 56×3=168位,大幅增加了暴力破解的难度。但代价也很明显:运算速度只有DES的三分之一左右(因为做了三次DES操作)。

3DES在20世纪90年代到21世纪初被广泛使用,尤其是在金融领域------许多银行和支付系统长期使用3DES加密ATM交易和信用卡数据。但它的局限也很明显:速度慢、分组只有64位(在现代标准下偏小)。随着计算能力的提升,112位甚至168位的安全性也在逐渐受到质疑。

2018年,NIST宣布从2023年起禁止3DES用于所有新开发的应用。3DES作为过渡性算法的使命已经结束。

1.1.6 SM4算法

DES和3DES都是国外的标准。在很长一段时间里,中国在商用密码领域没有自己的标准算法,只能使用国外的算法。

2012年,国家密码管理局发布了GM/T 0002-2012《SM4分组密码算法》,将SM4纳入中国商用密码标准体系。"SM"是"商密"的简称。 这是中国第一个自主设计的公开分组密码算法。

SM4的分组长度是128位,密钥长度也是128位。它采用非平衡Feistel结构,进行32轮迭代加密。加密和解密的结构相同,只是轮密钥的使用顺序相反。

1.1.6.1 平衡Feistel的局限

在介绍非平衡Feistel之前,先回顾一下我们之前讲的传统Feistel结构。

传统Feistel把明文平分为左右两半。如果分组长度是64位,左右各32位。如果要把分组长度提高到128位呢?按照平衡结构,左右各64位------轮函数的输入变成了64位。

问题在于:轮函数要处理的数据变大了,它的复杂度必须相应增加。 但构造更长输入、安全又高效的轮函数比较复杂------因为扩大轮函数的规模,会导致S盒更大、运算更复杂、速度明显下降。

另一种思路是:不把左右分得一样大。 让一半大一点、一半小一点,轮函数仍只需要处理较小的一半,剩下的那一半通过异或来更新。这样分组的整体长度可以做大,但轮函数的输入规模不需要等比例放大。

这就是非平衡Feistel的设计动机。

1.1.6.2 SM4的非平衡结构

SM4的128位明文被分成四个32位的字: X0,X1,X2,X3 X_0, X_1, X_2, X_3 X0,X1,X2,X3。在每一轮中, Xi+1 , Xi+2 , Xi+3 X_{i+1}, X_{i+2}, X_{i+3} Xi+1,Xi+2,Xi+3这三个字(共96位)构成了"右半部分",而 Xi X_i Xi单独作为"左半部分"(32位)。轮函数的输入规模只需要处理32位------这和64位分组的传统Feistel一样------但分组总长度扩大到了128位。

这个设计把 "分组长度扩大"和"轮函数复杂度增加"解耦了,避免了传统平衡Feistel在分组长度扩大时面临的工程难题。

SM4的一轮运算可以用一个公式表示:

Xi+4 =Xi⊕T( Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕rki) X_{i+4} = X_i \oplus T(X_{i+1} \oplus X_{i+2} \oplus X_{i+3} \oplus rk_i) Xi+4=Xi⊕T(Xi+1⊕Xi+2⊕Xi+3⊕rki)

其中 TT T是合成置换, rki rk_i rki是第 ii i轮的32位子密钥。整个加密过程执行32轮。

SM4整个加密过程分为三个步骤:

第一步:初始输入

把128位明文分成四个32位字: X0,X1,X2,X3 X_0, X_1, X_2, X_3 X0,X1,X2,X3。

第二步:32轮迭代

从第0轮到第31轮,每一轮更新一个32位字。更新规则是:

Xi+4 =Xi⊕T( Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕rki) X_{i+4} = X_i \oplus T(X_{i+1} \oplus X_{i+2} \oplus X_{i+3} \oplus rk_i) Xi+4=Xi⊕T(Xi+1⊕Xi+2⊕Xi+3⊕rki)

其中 rki rk_i rki是第 ii i轮的子密钥, TT T是合成置换(非线性S盒替换 + 线性 LL L变换)。

Xi+4 X_{i+4} Xi+4的计算依赖于前四个状态 Xi, Xi+1 , Xi+2 , Xi+3 X_i, X_{i+1}, X_{i+2}, X_{i+3} Xi,Xi+1,Xi+2,Xi+3和当前轮的子密钥 rki rk_i rki。计算完成后, Xi+4 X_{i+4} Xi+4作为新的状态加入到序列中,序列向前推进一位: Xi+1 , Xi+2 , Xi+3 , Xi+4 X_{i+1}, X_{i+2}, X_{i+3}, X_{i+4} Xi+1,Xi+2,Xi+3,Xi+4进入下一轮。

每一轮产生一个新的32位字,32轮后总共产生了32个新的字,最终得到 X32,X33,X34,X35 X_{32}, X_{33}, X_{34}, X_{35} X32,X33,X34,X35。

第三步:反序输出

32轮迭代结束后,得到四个字: X32,X33,X34,X35 X_{32}, X_{33}, X_{34}, X_{35} X32,X33,X34,X35,再进行一次反序置换得到最终密文:

(Y0,Y1,Y2,Y3)=(X35,X34,X33,X32) (Y_0, Y_1, Y_2, Y_3) = (X_{35}, X_{34}, X_{33}, X_{32}) (Y0,Y1,Y2,Y3)=(X35,X34,X33,X32)

这个反序置换是Feistel结构的标准操作------因为最后一轮的左右两半在输出时需要交换位置,才能让加密和解密使用相同的流程。和DES中的"交换左右两半"是同一个道理。

1.1.6.3 轮函数与T置换

SM4的轮函数接收四个32位字 Xi, Xi+1 , Xi+2 , Xi+3 X_i, X_{i+1}, X_{i+2}, X_{i+3} Xi,Xi+1,Xi+2,Xi+3和一个32位子密钥 rki rk_i rki,输出一个32位字 Xi+4 X_{i+4} Xi+4:

Xi+4 =Xi⊕T( Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕rki) X_{i+4} = X_i \oplus T(X_{i+1} \oplus X_{i+2} \oplus X_{i+3} \oplus rk_i) Xi+4=Xi⊕T(Xi+1⊕Xi+2⊕Xi+3⊕rki)

其中 TT T是整个轮函数的核心。它是一个合成置换,由两部分组成:非线性变换 τ\tau τ线性变换 LL L

T(⋅)=L(τ(⋅))T(\cdot) = L(\tau(\cdot)) T(⋅)=L(τ(⋅))

非线性变换 τ\tau τ:SM4使用一个8位输入、8位输出的S盒。输入32位数据被分成4个字节,每个字节分别查表替换,输出4个新字节。S盒是固定的,查找规则为:输入字节的高4位确定行号、低4位确定列号,从16×16的S盒表中取出对应值。

SM4的S盒表如下:

0 1 2 3 4 5 6 7 8 9 a b c d e f
0 D6 90 E9 FE CC E1 3D B7 16 B6 14 C2 28 FB 2C 05
1 2B 67 9A 76 2A BE 04 C3 AA 44 13 26 49 86 06 99
2 9C 42 50 F4 91 EF 98 7A 33 54 0B 43 ED CF AC 62
3 E4 B3 1C A9 C9 08 E8 95 80 DF 94 FA 75 8F 3F A6
4 47 07 A7 FC F3 73 17 BA 83 59 3C 19 E6 85 4F A8
5 68 6B 81 B2 71 64 DA 8B F8 EB 0F 4B 70 56 9D 35
6 1E 24 0E 5E 63 58 D1 A2 25 22 7C 3B 01 21 78 87
7 D4 00 46 57 9F D3 27 52 4C 36 02 E7 A0 C4 C8 9E
8 EA BF 8A D2 40 C7 38 B5 A3 F7 F2 CE F9 61 15 A1
9 E0 AE 5D A4 9B 34 1A 55 AD 93 32 30 F5 8C B1 E3
a 1D F6 E2 2E 82 66 CA 60 C0 29 23 AB 0D 53 4E 6F
b D5 DB 37 45 DE FD 8E 2F 03 FF 6A 72 6D 6C 5B 51
c 8D 1B AF 92 BB DD BC 7F 11 D9 5C 41 1F 10 5A D8
d 0A C1 31 88 A5 CD 7B BD 2D 74 D0 12 B8 E5 B4 B0
e 89 69 97 4A 0C 96 77 7E 65 B9 F1 09 C5 6E C6 84
f 18 F0 7D EC 3A DC 4D 20 79 EE 5F 3E D7 CB 39 48

例如,输入字节EF,查表取第E行第F列,得到84

线性变换 LL L τ\tau τ输出32位数据后,再经过线性变换 LL L:

L(B)=B⊕(B⋘2)⊕(B⋘10)⊕(B⋘18)⊕(B⋘24)L(B) = B \oplus (B \lll 2) \oplus (B \lll 10) \oplus (B \lll 18) \oplus (B \lll 24) L(B)=B⊕(B⋘2)⊕(B⋘10)⊕(B⋘18)⊕(B⋘24)

其中 ⊕\oplus ⊕表示异或操作, ⋘\lll ⋘表示循环左移操作。例如, B⋘2B \lll 2 B⋘2表示将 BB B的32位二进制表示循环左移2位。

τ\tau τ负责混淆(S盒非线性替换), LL L负责扩散(循环左移+异或)。两者合在一起,就是 TT T置换。

1.1.6.4 SM4密钥扩展

SM4的密钥扩展负责从128位主密钥生成32个32位子密钥,每轮使用一个。

(1)输入与初始化

主密钥 MKMK MK是128位,分成四个32位字,表示为 MK=(MK0,MK1,MK2,MK3)MK = (MK_0, MK_1, MK_2, MK_3) MK=(MK0,MK1,MK2,MK3)。

SM4还定义了两组固定参数:

  • 系统参数 FKFK FK FK=(FK0,FK1,FK2,FK3)FK = (FK_0, FK_1, FK_2, FK_3) FK=(FK0,FK1,FK2,FK3),四个32位常量
  • 轮常数 CKCK CK CK=(CK0,CK1,CK2,...,CK31)CK = (CK_0, CK_1, CK_2, \ldots, CK_{31}) CK=(CK0,CK1,CK2,...,CK31),32个32位常量,每轮使用一个

密钥扩展的第一步是把主密钥和系统参数 FKFK FK做异或:

(K0,K1,K2,K3)=(MK0⊕FK0,MK1⊕FK1,MK2⊕FK2,MK3⊕FK3) (K_0, K_1, K_2, K_3) = (MK_0 \oplus FK_0, MK_1 \oplus FK_1, MK_2 \oplus FK_2, MK_3 \oplus FK_3) (K0,K1,K2,K3)=(MK0⊕FK0,MK1⊕FK1,MK2⊕FK2,MK3⊕FK3)

这一步的作用是把主密钥"搅"一下------让密钥扩展的输入状态不依赖于主密钥的结构。不管主密钥长什么样(全0、全1、有规律、无规律),进入扩展算法时都是"被搅散"的状态。这样可以避免某些特殊主密钥产生弱子密钥序列,也能加快主密钥中每个比特对子密钥的扩散速度。

(2)迭代生成子密钥

i=0i=0 i=0开始,每一轮根据当前的 Ki, Ki+1 , Ki+2 , Ki+3 K_i, K_{i+1}, K_{i+2}, K_{i+3} Ki,Ki+1,Ki+2,Ki+3计算出 Ki+4 K_{i+4} Ki+4:

rki= Ki+4 =Ki⊕T′( Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕CKi) rk_i = K_{i+4} = K_i \oplus T'(K_{i+1} \oplus K_{i+2} \oplus K_{i+3} \oplus CK_i) rki=Ki+4=Ki⊕T′(Ki+1⊕Ki+2⊕Ki+3⊕CKi)

Ki+4 K_{i+4} Ki+4就是第 ii i轮使用的子密钥 rki rk_i rki。

T′T' T′是一个合成置换,和轮函数中的 TT T结构相同------都包含非线性替换 τ\tau τ和线性变换 LL L。其中,非线性替换 τ\tau τ是相同的(使用同样的S盒),区别是线性变换部分不同:

  • 轮函数中的 TT T: L(B)=B⊕(B⋘2)⊕(B⋘10)⊕(B⋘18)⊕(B⋘24)L(B) = B \oplus (B \lll 2) \oplus (B \lll 10) \oplus (B \lll 18) \oplus (B \lll 24) L(B)=B⊕(B⋘2)⊕(B⋘10)⊕(B⋘18)⊕(B⋘24)
  • 密钥扩展中的 T′T' T′: L′(B)=B⊕(B⋘13)⊕(B⋘23)L'(B) = B \oplus (B \lll 13) \oplus (B \lll 23) L′(B)=B⊕(B⋘13)⊕(B⋘23)

T′T' T′的线性变换比 TT T简单------只做了两次循环左移异或,而不是四次。

1.1.6.5 SM4算法总结

相较于DES,SM4在几个关键维度上做了升级。

首先是密钥长度。DES是56位,SM4是128位------密钥空间大幅扩大,安全性显著增强。

其次是结构 。DES用平衡Feistel,分组64位,轮函数处理32位。SM4采用非平衡结构------轮函数仍只处理32位,另外96位通过异或来更新。这个设计把分组长度和轮函数复杂度解耦了,避免了DES在扩容时遇到的工程难题。SM4的32轮也比DES的16轮多一倍,混淆和扩散更充分。

此外,DES有8个不同的S盒,每个6入4出。SM4只有一个S盒,8入8出------设计更规整,硬件实现更简单。

目前,SM4已纳入ISO/IEC国际标准,在国内政府、金融、通信等领域逐步替代国外算法,是中国商用密码体系的核心。

1.1.7 AES:高级加密标准

DES用了二十多年,到20世纪90年代末,已经明显跟不上了。

问题出在两个地方。第一是密钥太短。 56位密钥在1977年还算安全,到1999年已经被22小时攻破。第二是Feistel结构本身的局限。 DES基于Feistel网络,每轮只有一半数据参与运算,信息扩散速度慢。如果把密钥加长到128位,Feistel的轮函数也要跟着扩大,导致效率下降得厉害------增加的密钥长度带来的安全性提升,远远抵不上速度的损失。

所以,DES修修补补无法继续,而是需要一个全新的算法

1997年,美国国家标准与技术研究院(NIST)发起了一场公开竞赛,征集新的加密标准。这是密码学历史上第一次面向全球的公开算法竞赛。NIST提出了明确要求:新算法的分组长度必须是128位,密钥长度要支持128、192、256位三种,并且算法必须公开------安全性只能依赖密钥,不能依赖算法的保密

这个做法和DES完全不同。DES的S盒设计细节直到很多年后才完全公开,而AES从一开始就要求所有设计完全透明。

来自全球的15个算法提交了申请。NIST组织了多轮评审,考察每个算法的安全性、速度、内存占用、代码大小、抗攻击能力等各项指标。经过三年的筛选和评估,2000年,两个比利时密码学家------Joan DaemenVincent Rijmen 提交的Rijndael算法最终胜出,成为AES。

Rijndael算法胜出的理由很简单:它同时满足了三件事------安全性足够好、在各种平台上都快、代码量小容易实现。

在Rijndael的设计中,没有任何多余的步骤,每个操作都有明确的目的。字节代换负责混淆,行移位和列混淆配合完成扩散,轮密钥加负责注入密钥。整个算法的数学结构只用几页纸描述清楚。

Daemen和Rijmen在提交文档中写道:Rijndael的设计理念是 "宽轨迹策略"(Wide Trail Strategy) ------这个策略的核心思想,就是让扩散路径足够宽,使得攻击者无法利用局部的不规则性来构造有效的攻击路径。这不是靠堆砌复杂的操作来实现的,而是靠对数学结构的精妙安排。

很多失败的算法,死因往往不是不够复杂,而是太复杂了------堆砌了大量的操作,设计者自己都说不清为什么每一层是必要的。有些算法甚至不敢公开全部设计细节,因为担心一旦公开就会被攻破。

后来,NIST在最终的评估报告中写道:Rijndael的设计是 "简洁而优雅"(elegantly simple) 的。Rijndael在硬件和软件上的实现效率都非常高,而且在所有候选算法中,它最符合"简单而优雅"的设计标准。

值得注意的是,比利时是欧洲小国,在密码学领域并不显赫。Daemen和Rijmen当时都在鲁汶大学工作。他们没有任何特别的背景优势,提交的算法在所有平台上都表现出色,能击败来自美国和欧洲其他大国的团队,靠的是纯粹的技术实力。这件事本身就说明了AES竞赛的公正性:算法好坏,不看国籍,不看背景,只看设计本身。

和DES一样,AES也严格遵循香农的混淆与扩散理念。但实现方式完全不同:DES基于Feistel网络,按位 操作;AES基于SPN结构,按字节 操作。AES的核心操作有四种:字节代换(SubBytes)行移位(ShiftRows)列混淆(MixColumns)轮密钥加(AddRoundKey)。前三种分别负责混淆或扩散,第四种负责把密钥混合进来。

AES的分组长度固定为128位,密钥长度有三种:128位(10轮)、192位(12轮)、256位(14轮)。密钥每增加64位,轮数增加2轮。128位分组的版本最常用,后面的介绍都以这个版本为准。

与DES不同,AES不按位处理数据,而是按字节处理。128位数据分成16个字节,排成一个4×4的矩阵,称为状态矩阵 。排法是按列填充 :第0列放 P0,P1,P2,P3 P_0, P_1, P_2, P_3 P0,P1,P2,P3,第1列放 P4,P5,P6,P7 P_4, P_5, P_6, P_7 P4,P5,P6,P7,依此类推。也就是说,处理AES的数据不是按行填满再换行,而是先把第一列的4个字节填满,再填第二列、第三列、第四列。这个"列优先"的排列方式是AES所有操作的基础。

状态矩阵的排列方式如下:

列0 列1 列2 列3
P0 P4 P8 P12
P1 P5 P9 P13
P2 P6 P10 P14
P3 P7 P11 P15

举个例子,假设128位明文是:

00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF

按列填充到状态矩阵中:

列0 列1 列2 列3
00 44 88 CC
11 55 99 DD
22 66 AA EE
33 77 BB FF

1.1.7.1 轮密钥加(AddRoundKey)

轮密钥加是四步中最简单的一步:把状态矩阵和本轮的子密钥矩阵逐字节异或。

AES的密钥扩展从128位主密钥生成11个子密钥(10轮每轮一个,外加第0轮一个),每个子密钥也是128位,同样排成4×4矩阵。轮密钥加就是把状态矩阵的每个字节和子密钥矩阵的对应字节做异或。

这一步本身不产生混淆或扩散------因为异或是线性的,而且可逆。但它是密钥进入加密过程的入口,没有这一步,密钥就影响不到数据。

AES的并行处理能力在此体现出来:16个字节可以同时做异或,不需要像Feistel那样分左右两半依次处理。这是AES比DES快的原因之一。

1.1.7.2 字节代换(SubBytes)

字节代换是AES中实现混淆的核心步骤。

和DES、SM4一样,AES也用S盒。但AES的S盒和DES有两个重要区别:

第一,AES的S盒是数学构造的,不是随意生成的。 DES的S盒设计准则从未完全公开,设计者当时说"我们做了大量测试选了最好的",但没有说明为什么好、好在哪里。AES的S盒基于有限域上的乘法逆元运算,配合一个仿射变换------这个构造方法完全公开,任何人都可以验证它的非线性、差分均匀性等指标。这不是说每次使用时生成一次,而是说生成规则是公开的、确定的,所有实现都使用同一个表。

第二,AES只有一个S盒,8位输入8位输出------这和SM4的S盒一样。而DES有8个不同的S盒,每个6入4出。因此AES的S盒更规整,硬件实现更简单。

查找方法和SM4一样:输入字节的高4位确定行号,低4位确定列号,从16×16的表中取出对应值。例如输入53,行5列3,查得ED。解密时使用逆S盒,把ED还原成53

AES的S盒表如下:

0 1 2 3 4 5 6 7 8 9 a b c d e f
0 63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76
1 ca 82 c9 7d fa 59 47 f0 ad d4 a2 af 9c a4 72 c0
2 b7 fd 93 26 36 3f f7 cc 34 a5 e5 f1 71 d8 31 15
3 04 c7 23 c3 18 96 05 9a 07 12 80 e2 eb 27 b2 75
4 09 83 2c 1a 1b 6e 5a a0 52 3b d6 b3 29 e3 2f 84
5 53 d1 00 ed 20 fc b1 5b 6a cb be 39 4a 4c 58 cf
6 d0 ef aa fb 43 4d 33 85 45 f9 02 7f 50 3c 9f a8
7 51 a3 40 8f 92 9d 38 f5 bc b6 da 21 10 ff f3 d2
8 cd 0c 13 ec 5f 97 44 17 c4 a7 7e 3d 64 5d 19 73
9 60 81 4f dc 22 2a 90 88 46 ee b8 14 de 5e 0b db
a e0 32 3a 0a 49 06 24 5c c2 d3 ac 62 91 95 e4 79
b e7 c8 37 6d 8d d5 4e a9 6c 56 f4 ea 65 7a ae 08
c ba 78 25 2e 1c a6 b4 c6 e8 dd 74 1f 4b bd 8b 8a
d 70 3e b5 66 48 03 f6 0e 61 35 57 b9 86 c1 1d 9e
e e1 f8 98 11 69 d9 8e 94 9b 1e 87 e9 ce 55 28 df
f 8c a1 89 0d bf e6 42 68 41 99 2d 0f b0 54 bb 16

解密时使用的逆S盒表如下:

0 1 2 3 4 5 6 7 8 9 a b c d e f
0 52 09 6A D5 30 36 A5 38 BF 40 A3 9E 81 F3 D7 FB
1 7C E3 39 82 9B 2F FF 87 34 8E 43 44 C4 DE E9 CB
2 54 7B 94 32 A6 C2 23 3D EE 4C 95 0B 42 FA C3 4E
3 08 2E A1 66 28 D9 24 B2 76 5B A2 49 6D 8B D1 25
4 72 F8 F6 64 86 68 98 16 D4 A4 5C CC 5D 65 B6 92
5 6C 70 48 50 FD ED B9 DA 5E 15 46 57 A7 8D 9D 84
6 90 D8 AB 00 8C BC D3 0A F7 E4 58 05 B8 B3 45 06
7 D0 2C 1E 8F CA 3F 0F 02 C1 AF BD 03 01 13 8A 6B
8 3A 91 11 41 4F 67 DC EA 97 F2 CF CE F0 B4 E6 73
9 96 AC 74 22 E7 AD 35 85 E2 F9 37 E8 1C 75 DF 6E
a 47 F1 1A 71 1D 29 C5 89 6F B7 62 0E AA 18 BE 1B
b FC 56 3E 4B C6 D2 79 20 9A DB C0 FE 78 CD 5A F4
c 1F DD A8 33 88 07 C7 31 B1 12 10 59 27 80 EC 5F
d 60 51 7F A9 19 B5 4A 0D 2D E5 7A 9F 93 C9 9C EF
e A0 E0 3B 4D AE 2A F5 B0 C8 EB BB 3C 83 53 99 61
f 17 2B 04 7E BA 77 D6 26 E1 69 14 63 55 21 0C 7D

1.1.7.3 行移位(ShiftRows)

行移位是AES中实现扩散的第一步。

操作很简单:状态矩阵的每一行循环左移不同字节数。第0行不移,第1行移1位,第2行移2位,第3行移3位。

这个操作的作用是把各列的数据打散。行移位把第1行第0列的字节移到了第1列,把第2行第0列的字节移到了第2列......这样下一轮列混淆时,不同列的数据就会混合在一起。

解密时反向移动------第1行右移1位,第2行右移2位,第3行右移3位。

1.1.7.4 列混淆(MixColumns)

列混淆是AES中最复杂的操作,也是扩散的主要来源。

它与行移位配合工作。行移位把每一行的字节分散到不同列,列混淆则把每一列的4个字节混合成4个新的字节------混合方式是让每个输出字节都依赖于该列的所有4个输入字节。

列混淆的数学本质是:把状态矩阵的每一列,左乘一个固定的4×4矩阵。这里的加法和乘法不是普通的整数运算,而是在伽罗瓦域(Galois Field,也叫有限域) 上定义的运算。

什么是域?简单说,域是一种支持加法、减法、乘法、除法四则运算的数学结构------有理数、实数都是域。伽罗瓦域是元素个数有限的域。AES用的是GF(2⁸),即由0到255这256个元素构成的域,加法对应异或,乘法对应多项式乘法后取模。

为什么要在GF(2⁸)上算,而不是直接用整数运算?因为普通整数运算的结果会超出256的范围,而GF(2⁸)上的乘法结果始终落在0~255之间。这是计算机处理字节数据的基本要求------输入一个字节,输出也得是一个字节。

对于不关心数学细节的读者,只需要知道结论:列混淆让一个字节的变化能扩散到一整个列。 加上行移位的配合,经过几轮迭代后,任何一个明文字节的微小变化都会影响到密文的全部16个字节。

正向列混淆的矩阵是固定的:

02 03 01 01
01 02 03 01
01 01 02 03
03 01 01 02

每个输出字节是该列的4个输入字节的线性组合,系数就是上面这个矩阵里的值。解密时使用另一个矩阵------它是上面这个矩阵在GF(2⁸)中的逆矩阵。

1.1.7.5 密钥扩展

AES的密钥扩展从128位主密钥生成11个子密钥。为什么是11个?因为128位版本共10轮,每轮需要一个子密钥,加上第0轮(轮密钥加)也需要一个,总共11个。

子密钥存储在扩展密钥数组 W0W0 W0 W43W43 W43中。每4个 WW W组成一个子密钥------例如 W0∼W3W0 \sim W3 W0∼W3是第0轮子密钥, W4∼W7W4 \sim W7 W4∼W7是第1轮子密钥,依此类推。每个 WW W是32位(4个字节)。

密钥扩展算法如下:

  • 前4个 WW W,即 W0∼W3W0 \sim W3 W0∼W3直接从主密钥复制
  • W4W4 W4开始:
    • 如果下标 ii i是4的倍数 (即每轮的第一个 WW W: W4,W8,W12,...W4, W8, W12, \ldots W4,W8,W12,...),则先对前一个 WW W(即 Wi−1Wi-1 Wi−1)做G函数 处理,再与 Wi−4Wi-4 Wi−4异或: Wi=Wi−4⊕G(Wi−1)Wi = Wi-4 \oplus G(Wi-1) Wi=Wi−4⊕G(Wi−1) 例如: W4=W0⊕G(W3)W4 = W0 \oplus G(W3) W4=W0⊕G(W3)
    • 如果下标 ii i不是 4的倍数(即每轮的其余三个 WW W: W5,W6,W7,W9,...W5, W6, W7, W9, \ldots W5,W6,W7,W9,...),则直接与前第4个 WW W异或: Wi=Wi−4⊕Wi−1Wi = Wi-4 \oplus Wi-1 Wi=Wi−4⊕Wi−1 例如: W5=W1⊕W4W5 = W1 \oplus W4 W5=W1⊕W4

G函数包含三个步骤:

  1. 循环左移1个字节
  2. 经过S盒替换
  3. 第一个字节与轮常数 RconRcon Rcon异或

G函数的作用有两个:

  • 增加非线性:如果没有这个处理,密钥扩展就是一系列异或操作,完全是线性的,安全性会大打折扣
  • 消除对称性:AES的轮函数本身是对称的,如果不加入非对称因素,某些攻击方法可以利用这种对称性。G函数中的轮常数确保每轮子密钥都不同

轮常数 RconRcon Rcon 是一个有10个元素的一维数组,每个元素1个字节。第 ii i轮的轮常数是 2i−12^{i-1} 2i−1在GF(2⁸)上的值:01, 02, 04, 08, 10, 20, 40, 80, 1B, 36

1.1.7.6 加密流程

现在把前面讲的四个操作和密钥扩展串起来,看AES完整的加密流程。假设用128位密钥加密128位数据,整个过程如下:

第一步:初始轮(第0轮)

把明文排成4×4状态矩阵,直接与第0轮子密钥( W0∼W3W0 \sim W3 W0∼W3)做一次轮密钥加

这一步在正式轮次开始之前执行,相当于把密钥"注入"数据。

第二步:10轮迭代

从第1轮到第10轮,每轮依次执行四个操作:

字节代换 → 行移位 → 列混淆 → 轮密钥加

其中最后一轮(第10轮)略有不同------不做列混淆。

也就是说:

  • 第1轮~第9轮:字节代换 → 行移位 → 列混淆 → 轮密钥加
  • 第10轮:字节代换 → 行移位 → 轮密钥加(跳过列混淆)

最后一轮跳过列混淆是为了加解密结构一致。加密最后一轮不做列混淆,解密第一轮就不需要做逆列混淆------两边对称,硬件实现更简单。

第三步:输出

第10轮结束后,状态矩阵中的16个字节就是密文。按列读出得到128位密文。

1.1.7.7 解密流程

AES的解密和加密使用相同的子密钥,但顺序相反,并且每个操作都换成对应的逆操作

  • 加密的子密钥顺序是: K0,K1,K2,...,K10 K_0, K_1, K_2, \ldots, K_{10} K0,K1,K2,...,K10
  • 解密的子密钥顺序是: K10,K9,K8,...,K0 K_{10}, K_9, K_8, \ldots, K_0 K10,K9,K8,...,K0

AES的四个操作中,除了轮密钥加是自逆的(异或两次回到原值),其他三个操作都需要逆操作来"抵消"加密时的效果:

  • 字节代换 → 用逆S盒
  • 行移位 → 反向移动
  • 列混淆 → 用逆矩阵

解密完整流程:

第一步:初始轮(用 K10 K_{10} K10)

密文状态矩阵与 K10 K_{10} K10做逆轮密钥加。

第二步:10轮迭代

  • 第1轮(对应加密第10轮):逆行移位 → 逆字节代换 → 逆列混淆 → 逆轮密钥加( K9 K_9 K9)
  • 第2轮~第9轮:逆行移位 → 逆字节代换 → 逆列混淆 → 逆轮密钥加(对应)
  • 第10轮(对应加密第0轮,解密最后一轮):逆行移位 → 逆字节代换 → 逆轮密钥加( K0 K_0 K0)------最后一轮不做逆列混淆

第三步:输出

得到128位原始明文。

1.1.8 AES算法总结

AES自2001年正式成为标准,至今已使用超过二十年,是目前使用最广泛的对称加密算法。AES的设计有几个值得注意的地方。

第一,全部公开。 从S盒的构造方法到每一轮操作的设计理由,全部公开。安全性不依赖任何秘密------唯一需要保密的是密钥。这一点和DES的S盒"我们做了大量测试但细节不公布"的做法完全不同。

第二,数学驱动。 每个操作背后都有明确的数学依据。S盒基于有限域上的乘法逆元,列混淆在GF(2⁸)上定义,轮常数由GF(2⁸)上的乘法生成。这意味着AES的安全性可以用严格的数学工具来分析,而不是依赖大量的随机测试。

第三,设计理念可证明。 AES的设计者Daemen和Rijmen提出了**"宽轨迹策略"(Wide Trail Strategy)**。这个策略的核心是:把密码分析中复杂的攻击概率问题,转化为对"活跃S盒数量"的数学计数问题。AES的设计保证了任何4轮加密至少激活25个S盒,这意味着即使攻击者想利用数据的某种规律来构造攻击路径,也会被大量活跃S盒的非线性变换彻底打乱。攻击成功的概率下降到 2−1502^{-150} 2−150以下,远低于穷举 21282^{128} 2128的难度。设计者不需要靠运气去猜测哪个参数安全,而是可以直接算出安全边界在哪里。

第四,结构规整。 四个操作各司其职:字节代换负责混淆,行移位和列混淆配合完成扩散,轮密钥加负责注入密钥。没有多余的步骤,每一层都有清晰的职责。

第五,实现友好。 按字节处理,状态矩阵整齐排列,16个字节可以并行操作。在硬件和软件上都有很高的效率。

AES自发布以来,经历了全球密码学家二十多年的分析尝试。目前对完整轮数的AES没有比穷举更有效的攻击方法。128位密钥空间 21282^{128} 2128,在可预见的未来仍然是安全的。

1.1.9 一点延伸:AES背后的数学原理

AES的四个操作看起来都很简单------查个表、移个位、做个异或、做个乘法------但每一个操作背后都有深刻的数学原理在支撑。

  • S盒的构造涉及有限域上的乘法逆元和仿射变换
  • 列混淆的运算建立在伽罗瓦域GF(2⁸)之上
  • 轮常数的生成依赖于有限域上的多项式乘法
  • 行移位的偏移量选择与抗线性密码分析有关

这些数学概念本身构成了一个庞大的知识体系,有兴趣的读者可以进一步探索。不理解这些概念并不影响使用AES------就像开车不需要懂内燃机的工作原理一样。但如果你想知道 "为什么AES这样设计是安全的" ,答案就藏在这些数学原理之中。

AES的设计告诉我们一件事:好的密码算法不在于把操作做得多么复杂,而在于用简单的操作组合出足够的安全性。 所谓"简洁而优雅",就是这个意思。

相关推荐
MrZhao4001 小时前
Agent 长上下文处理机制:Context Compact 与 Memory 的协同
算法
h_a_o777oah1 小时前
【动态规划】区间 DP :三层循环逻辑与模板实现细节(洛谷 P1880)
c++·算法·动态规划·acm·区间dp·化环为链·石子合并
从此以后自律1 小时前
迪杰斯特拉
算法
QiLinkOS2 小时前
第三视觉理解徐玉生与他的商业活动(32)
大数据·c++·人工智能·算法·开源协议
KaMeidebaby2 小时前
卡梅德生物技术快报|实操手册:CXCL4 蛋白原核表达全套工艺,两步层析去除蛋白多聚体附完整电泳数据
人工智能·算法·机器学习·架构·spark
AIGCmagic社区2 小时前
Unlimited OCR 论文精读:R-SWA 如何实现一次性长文档解析
人工智能·算法·aigc
鹏易灵2 小时前
C++——7.类与对象,掌握封装、继承、多态.详解
开发语言·c++·算法
Mortalbreeze2 小时前
深入 Linux 线程机制(三):线程互斥——竞争条件与互斥锁的本质
linux·运维·服务器·c++·算法
Mr_Controll2 小时前
【运动控制——电子凸轮曲线设计(多项式)】
算法·自动化