上个星期的的 Web 安全技术课上讲了 DES 算法的具体算法,本文来说明一下 DES 算法的具体流程,以及使用 C 语言和 Go 语言实现 DES 算法。🎉
🚀 代码: Github
算法原理概述
DES 是一种对称加密算法,加密和解密使用同样的密钥。
DES 使用 64 位的密钥对明文进行块加密,将明文划分为 64 位一个块,通过换位和置换,对其进行加密混淆,输出同样的 64 位长度的密文,但是由于采用了 ECB 模式,在块与块之间没有扩散,同样的明文块会被加密成相同的密文块,因此并不能很好地隐藏数据模式。
64 位密钥除去 8 位奇偶验证位,实际上起作用的只有 56 位,但是我们可以通过多次 DES 加密来加强加密强度,延长密钥长度。
总体结构
DES 的加密和解密的过程除了引用子密钥的顺序相反,其他的过程都是一样的。
总体结构如图所示:

其具体的过程如下:
(注意:下面所有下标都是从 1 开始而不是从 0 开始)
首先,对原始明文消息按 PKCS#5 规范进行字节填充:
- 原始明文消息最后的分组不够 8 个字节(64 位) 时,在末尾以 字节填满,填入的字节取值相同,都是填充的字节数目
- 原始明文消息刚好分组完全时,在末尾填充 8 个字节(即增 加一个完整分组),字节取值都是 08。
举个例子:
如果原文为
xxxxxxxx xxx,末尾需要填充 5 位,因此填充xxxxxxxx xxx,05,05,05,05,05如果原文为
xxxxxxxx, 末尾就需要填充 8 个08,这样解密出来就可以识别出那些是填充的。
然后对其的每 64 位进行如下处理:
- 初始置换$IP$ - 根据IP置换表(各种表的数据在下文的代码中可以查看)将 64 位明文进行初始置换得到$L_0R_0$
 
- 根据
- 16 轮迭代$T$ - 将$L_0R_0$按照下述规则进行 16 次迭代:  
- 其中$K_i$为第 - i个子密钥, f 是 Feistel 轮函数:- 输入:32 位串$R_{i-1}$、48 位的子密钥(子密钥的生成方式稍后再说明)
- 将 32 位串$R_{i-1}$根据扩展置换表进行E扩展,得到 48 位串$E(R_{i-1})$
- 将 48 位串$E(R_{i-1})$和 48 位子密钥$K_i$进行异或操作
- 将 48 位串分成 8 个分组,每个分组 6 位,分别经过 8 个 S 盒的6-4转换,得到 8*4=32 位的串- S-盒转换输入 6 位$b_1b_2b_3b_4b_5b_6$串
- 取$b_1b_6$作为行、$b_2b_3b_4b_5$作为列,从 4 行 16 列的 S 盒中取出 4 位结果输出
 
- 最后将结果进行P置换,得到 Feistel 轮函数的 32 位输出
 
- 将 Feistel 轮函数的结果与上一轮的左边 32 位串进行异或操作,作为这一轮的右边 32 位串 
- 将上一轮的右边 32 位串作为这一轮的左边 32 位串 
 
- 交换置换$W$ - 将第十六轮迭代生成的串$L_{16}R_{16}$交换,输出$R_{16}L_{16}$
 
- 逆置换$IP^{-1}$ - 根据IP逆置换表将 64 位串置换得到结果
 
- 根据
然后就得到 64 位的密文块。
最后把所有密文块合在一起就是密文。
其中16 个子密钥的生成过程如下:
- 首先对 64 位密钥串进行PC-1置换,得到 56 位串(去掉了 8 位检验位),分为前 28 位和后 28 位$C_0D_0$
- 然后分别对$C_i$和$D_i$进行循环左移,如果$i = 1,2,9,16$,则循环左移一位,否则循环左移两位,得到$C_{i+1}$和$D_{i+1}$
- 对 56 位的$C_iD_i$进行PC-2置换压缩,得到 48 位的子密钥$K_i$
- 返回第二步,直到生成第 16 个子密钥$K_i$

其中子密钥的生成过程可以和 T 迭代置换同时进行,也可以一开始先生成所有的子密钥,只是安全性不高。密钥停留在内存中事件越久就越危险,尤其是没有内核保护的情况下。在
Linux下,加密算法可以将密钥生成在内核的保护区域内以确保安全。
而解密过程和加密过程大致相同,在迭代变换过程中则从 T16-T1 反过来进行迭代,最后就可以得到明文,最后记得根据最后一位的内容删除掉之前根据PKCS#5填充的内容。
模块分解
由于 DES 的加密和解密只有 T 迭代的顺序有所不同,因此可以共用一个主体模块 DES。
而 DES 的算法的主体模块DES又可以分解为以下的模块:
- 生成子密钥 :makeKey- 根据 64 位密钥生成 16 个 48 位的子密钥,用于 16 轮 T 迭代变换
 
- T 迭代:tIteration- 按照一定规则,使用子密钥对 IP 置换后的信息执行迭代变换 16 轮,最后进行一次 W 置换
 
其中需要到一些辅助模块:
- PKCS5 填充: - PKCS5Padding- 对明文信息补全,使明文可以划分为一定数量的 64 位块
 
- 置换: - transform- 进行各种置换:IP 置换、IP 逆置换、PC1 置换、PC2 置换、E 扩展置换、P 置换
 
- Feistel: - feistel- T 迭代中比较重要的函数,将 32 位输入和 48 位子密钥进行 PC 置换和 S-BOX 置换,得到 32 位输出
 
- S-Box 置换: - sBoxTransform- Feistel 轮函数中的一个重要步骤,将 6 位输入根据S-Box生成 4 位输出
 
- Feistel 轮函数中的一个重要步骤,将 6 位输入根据
- 循环左移: - leftShift- 生成子密钥时每次迭代时对于CD的处理
 
- 生成子密钥时每次迭代时对于
数据结构
这里使用了unsigned char作为主要的数据存储单位,虽然一个unsigned char为 1 个字节,即 8 位,但是为了运算的方便起见,某些数据结构还是使用 1 个字节来存储 1 位数据。
为了运算速度,需要预先存储一些置换运算将会使用到的表:
- PC1-Table:生成密钥时对原密钥的 PC1 置换
- PC2-Table:生成密钥时生成子密钥的 PC2 置换
- IP-Table:IP 置换
- IPInverse-Table:IP 逆置换
- sBox(1-8):feistel 轮函数中的 S 盒选择函数
- P-Table:feistel 轮函数中的 P 置换
- E-Table:feistel 轮函数中的 E 扩展
为了节省空间,以上表都使用unsigned char数组存储
对于明文数据和密文数据,同样是使用unsigned char数组存储。
源代码
根据上面的过程,分别使用 C 语言的 Go 语言实现了 DES 算法,对于同一明文使用同一密钥加密的结果都是一样的。
下面是 Github 地址:
由于 Go 语言的版本划分了多个文件,这里不便贴出来,因此只把 C 语言的版本展示出来,详情可以点击上面的连接查看:
C 语言版本:
| 1 | 
 | 
编译运行结果
测试对于一个字符串使用密钥对其加密,然后对密文用同一密钥进行解密,如果解密出来的明文和原来的一样,那么这个加密算法就算成功了。
使用gcc对上面的代码进行编译并运行,得到下面的结果:
| 1 | $ gcc ./DES.c | 

可以看到,解密出来的结果和之前输入的明文是一样的,因此这个算法是成功的。
 
        