上个星期的的 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 |
可以看到,解密出来的结果和之前输入的明文是一样的,因此这个算法是成功的。