一、DES加密过程
- 64位秘钥经子秘钥产生算法产生16个子秘钥:K1、K2……、K16, 分别供第一次到第16次加密迭代使用。
- 64位明文首先经过初始置换IP,将数据打乱重新排列并分成左右两半。左边32位构成L0,右边32位构成R0.
- 由加密函数f实现子秘钥K1对R0的加密,结果为32位的数据组f(R0, K1) 。f(R0, K1)再与L0模2相加(即异或),又得到一个32位的数据组L0&f(R0, K1). 以L0&f(R0, K1)作为第二次加密迭代的R1,
以R0作为第二次加密迭代的L1.至此,第一次加密迭代结束。- 第二次到第16次加密迭代分别用子秘钥K2到K16进行,其过程如上。
- 第16次迭代结束,产生一个64的数据组。以其左边32位作为R16, 以其右边作为L16,两者合并再经过逆初始置换IP-1,将数据重新排列,便得到64位密文。加密结束。
注:下面提到的二进制位数都是从左往右数的。
二、DES的算法细节
<1> 子秘钥的产生
64位秘钥经过置换选择1、循环左移、置换选择2等变换,产生16个48位长的子秘钥。
如图:
1. 64位秘钥中的每个字节的最后一位为奇偶校验位,所以秘钥的真实长度为56位,将这56位经置换选择1分为左右各28位,置换选择1的矩阵如下:
左部分C0 右部分D0
57 49 41 33 25 17 9 63 55 47 39 31 23 15
1 58 50 42 34 26 18 7 62 54 46 38 30 22
10 2 59 51 43 35 27 14 6 61 53 45 37 29
19 11 3 60 52 44 36 21 13 5 28 20 12 4
C0的各位依次为原始秘钥的57, 49, ……..,36位;
D0的各位依次为原始秘钥的63 ,55, ……..,4位。
2. 将得到的C0,D0各做循环左移,每一轮的循环左移如下表:
迭代次数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
左移位数 1 1 2 2 2 2 2 2 1 2 2 2 2 2 2 1
注:左移为循环左移
3. 右移后得到C1,D1,合并后经置换选择2得到子秘钥K1,置换选择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
C1、D1合并为56位数,从中选取第14位,17位……32位作为子秘钥K1的第1-32位。
4. 迭代上面2-3步,分别得到K2-K16.
<2> 初始置换IP
将64位明文打乱重排并分为左右两半。左32位为L0, 右32位为R0, 供后面的加密迭代使用。置换IP的矩阵如下
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
置换后64位数据的第1-64位依次为原明文的第58,50,… ,7位。
<3>加密函数
上面得到的L0,R0分别作为加密函数的输入,先经过选择运算将32位输入扩展为48位,以便与48的子秘钥进行异或,还有后续的S盒替换。
1. 选择运算
选择运算通过重复选择某些数据位来达到数据扩展的目的,选择运算的矩阵如下:
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
end
2. 模2加(异或) 。将选择运算得到的48位中间值与48位子秘钥异或。
3. S盒替换。将上面得到的48位数,分为8部分,1-6,7-12,…,43-48.分别经S1,S2,…,S8盒各转换为4位二进制数,合为32位数。
S盒内容看S盒等数据,太多不写了。。。
4. 置换运算P。置换运算把输出的32位数据打乱重排,得到32位的加密函数输出。用P置换来提供扩散,把S盒的混淆作用扩散开来。正是置换P和S盒的互相配合提高了DES的安全性。
三、DES算法实现
我使用的是位运算的方法来重排各个元素的,所以上面的各种位置矩阵我都转换为掩码的形式,方便位运算,为了方便转换为掩码,使用下面的转换程序:
#include <stdio.h>
#define ROW 8 // 矩阵行数
#define COL 8 // 矩阵列数
#define MAX (ROW*COL) // 最大位数
static int table[ROW][COL] = {...}; // 存放相应的位置矩阵
for(i = 0; i < ROW; i++)
{
printf("{");
for(j = 0; j < COL; j++)
{
printf("0x%lx", n << (MAX-table[i][j])); // 因为是从左往右数的位置,而这里的运算是从右往左的,要用数的最大位数去减
if(j != COL-1)
printf(", ");
}
if(i != ROW-1)
printf("}, \n");
else
printf("}\n");
}
类型定义
typedef short bool; // 布尔类型
enum{false, true};
typedef unsigned int bit_32; // 32位类型
typedef long long bit_64; // 64位类型,64位机
typedef long bit_48; // 48位类型
typedef int bit_28; // 28位类型
子秘钥产生函数:
static bit_64 frlTable[4][7]; // 置换选择1的左边矩阵
static bit_64 frrTable[4][7]; // 置换选择1的右边矩阵
static bit_64 srTbale[8][6]; // 置换选择2的矩阵
static int bitTable[16]; // 循环左移的矩阵
void firstReplace(bit_64 key, bit_28 *left, bit_28 *right ) // 置换选择1,key为64位的原始秘钥, left, right分别指向存放左右部分秘钥
void secondReplace(bit_28 l, bit_28 r, bit_48 *subkey) // 置换选择2, l, r为打乱后的左右部分秘钥, subkey为子秘钥存放位置
void leftmove(bit_28 *value, int bits) // 循环左移,value指向要处理的值,bits为左移位数
void geneSubKeys(bit_64 key, bit_48 *subkeys) // 产生子秘钥,key为原始秘钥,subkeys为子秘钥数组
初始置换函数:
static bit_64 ipTable[8][8]; // 初始置换IP表
void initReplaceIp(bit_64 text, bit_32 *left, bit_32 *right); // 初始置换IP函数, text为64位明文,left指向置换后的左部,right指向置换后的右部
加密函数:
static bit_32 selectTable[8][6]; // 选择运算表
static int Sbox1[4][16]; // 八个S盒
static int Sbox2[4][16];
static int Sbox3[4][16];
static int Sbox4[4][16];
...
static int Sbox8[4][16];
static int** Sbox[8]; // 存放八个S盒的首地址指针
static bit_32 replTable[8][4]; // 置换运算的矩阵
static bit_64 ripTable[8][8]; // 逆IP置换矩阵
void selectOperator(bit_32 value, bit_48 *result);// 选择运算函数,value为32位输入值,result指向运算结果值
void SboxOperator(bit_48 value, bit_48 subkey, bit_32 *result); // S盒运算函数,value为48位输入,subkey为子秘钥,result指向运算结果值
void ReplaceOperator(bit_32 *value)// 置换运算函数,将S盒输出的32位数据打乱重排,再存回value指向的值
void reverseInitReplace(bit_32 left, bit_32 right, bit_64 *ciphertext) // 逆初始置换函数,left为16次迭代后的左部,right为16次迭代后的右部,ciphertext指向打乱后的密文
bit_64 encrypt(bit_32 left, bit_32 right, bit_48 *subkeys) // p输入处理后明文的左右部分left为左部,right为右部,subkeys指向子秘钥数组,返回密文
遇到的问题:
- 书中说的第几位,是从左往右数的位数,我搞错了,以为是二进制数的实际位数。。。
很久没用移位了,这次遇到了几个小问题。
<1> 移位的优先级低于四则运算,高于逻辑运算,所以bit_64 value = (uleft << 32) + right;
要加括号,否则会出错。。。再比如:
row = ((sixbits&0x20) >> 4) + (sixbits&0x1);
<2>右移时尽量使用无符号数。因为右移分逻辑右移和算术右移,逻辑右移直接在左端补0,算术右移在左端补最高位有效位的值。而无符号数只有逻辑右移,有符号数两者都可以,C语言没有明确说明。。。
<3>左移时要注意被左移数类型是否适合,会不会有溢出的可能,例如
bit_64 key; key = (l << 28) + r;// error: 这里的l变量为bit_28类型,所以肯定会溢出 bit_64 ul = l; key = (ul << 28) + r; // OK: 这样就不会溢出了。。
<4> 循环左移时要把用掩码限定在要求的位数内(例如这里的
0xFFFFFFF
,限定在28位),不然如果类型的长度比你要设定的循环长度长的话,就会出错:inline void leftmove(bit_28 *value, int bits) // 循环左移 { // 因为bit_28其实是int类型,存储位数为32位 int left = *value; int right = *value; left = left << bits; left = left & 0xFFFFFFF; // 没有这步的话,超过28位部分会被保留 right = right >> (28-bits); *value = left | right; }
注:使用inline函数,gcc编译时要加-O2选项,即
gcc -O2 -o des main.c des.c
不然会出现错误:$ gcc -o des main.c des.c /tmp/ccyn4UUe.o:在函数‘geneSubKeys’中: des.c:(.text+0x209):对‘leftmove’未定义的引用 des.c:(.text+0x223):对‘leftmove’未定义的引用 collect2: error: ld returned 1 exit status
原因是:
C语言原本是不支持inline的,但C++中原生对inline的支持让很多C编译器也为C语言实现了一些支持inline语义的扩展。C99将inline正式放入到标准C语言中,并提供了inline关键字。和C++中的inline一样,C99的inline也是对编译器的一个提示,提示编译器尽量使用函数的内联定义,去除函数调用带来的开销。inline只有在开启编译器优化选项时才会生效。引用自:查看原文
二维数组的首地址不能直接作为两重指针的值。
static int Sbox1[4][16] = {...}; static int Sbox2[4][16] = {...}; static int **Sbox[2] = {Sbox1, Sbox2}; // warning: 类型不匹配 typedef int (*p)[16]; // p 为指向长度为16的一维数组的指针类型 static p Sbox[2] = {Sbox1, Sbox2}; // OK: Sbox1, Sbox2其实就是这样的类型,要指明第二维度的长度,这样Sbox1+1等操作才有明确的意义