cplus-DES对称加密实现

DES(Data Encryption Standard)对称加密方法

DES算法全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法。DES算法是一种对称算法,即可以使用同一个密钥进行加密和解密。DES的具体原理解析,本人推荐J. Orlin Grabbe的名作《DES Algorithm Illustrated》 以及一篇非常优秀的中文介绍:DES算法示例讲解。本文的重点是用C++代码 逐步实现DES算法的具体过程

0.前提说明

本文实现的是基于ECB的Zeropadding的DES加密算法。ECB指的是将加密的数据分成若干组,每组的大小跟加密密钥长度相同;然后每组都用相同的密钥加密, 如果最后一个分组长度不够64位,要补齐64位。Zeropadding指的是补齐着这64位的方法是用0填充。这种模式是实现DES的基本模式。 DES的输入有三个部分,第一个部分是明文,就是需要加密内容;第二个部分是密钥,用来加密数据;第三个部分是工作模式,有两种,加密和解密。明文的长度不固定,但是都被分为固定的64位“数据块”,以一个块为单位进行加密,输出的密文也是一个64位的“块”;密钥的长度是64位,但是实际用到的只有56位,第8、16、24、32、40、48、56、64位是校验位,做加密中用不到。本文输入的格式为文本,例如明文‘12345678’,等同于16进制

1.密钥的生成

1.1实现字符到bit的转换

DES加密本质上是对二进制数据的加密,而正常我们输入电脑的多用的是文本形式,因此我们需要将字符转变为文本。我们把它放在类的成员函数中,在密钥生成阶段和明文加密阶段都会用到。本文以密钥“12345678”为例。主要分成两个函数: 第一个函数:选取8字节密钥,超过8字节只选取前面8字节,不足8字节用0补齐。

 1 //设置密钥的内容,截取或补齐
 2 bool DataEncrytionStandard::SetKey(const char* _key)
 3 {
 4  int lengthCout=0;
 5  while(_key!='\0' && lengthCout<8)
 6  {
 7   this->key[lengthCout] = _key[lengthCout];//密码的长度只截取前面8位,不够的话用‘0’补齐。
 8   lengthCout++;
 9  }
10  return true;
11 }

第二个函数将8字节转换为64bits。本文使用的是64位的array<bool,64>实现。过去曾用bitset尝试过,但是bitset默认存储的是二进制数字,比特位顺序和DES加密的顺序相反;同时bitset还要考虑小端规则,对于降下来的处理不是很方便,因此用数组替代。本人发现网上的一些C++实现没有注意到这些问题,导致加密的结果与标准结果不一样。现推荐一个网站可以查看每一步实现的数据变化:JavaScript DES Example

1array<bool,64> DataEncrytionStandard::CharToBits(char _inChar[8])
2{
3 array<bool,64> bits;
4 for(int i=0;i<8;i++)
5  for(int j=0;j<8;j++)
6   //这里注意顺序
7   bits[i*8+7-j] = (_inChar[i]>>j)&1;
8 return bits;
9}

获得密钥:0011000100110010001100110011010000110101001101100011011100111000

1.2密钥的PC-1转换

这个64位的秘钥首先根据表格PC-1进行变换,变成56位的密钥。这个表格含义是64bits密钥的第57位,变成新密钥的第1位;原密钥的第49位变成新密钥的第2位;以此类推。

 1            PC-1
 2
 357   49    41   33    25    17    9
 41   58    50   42    34    26   18
 510    2    59   51    43    35   27
 619   11     3   60    52    44   36
 763   55    47   39    31    23   15
 87   62    54   46    38    30   22
 914    6    61   53    45    37   29
1021   13     5   28    20    12    4
1
2 //第一次转换,将64bit的密钥根据PC-1变换转换成56bit。
3 array<bool,64> keyInit = CharToBits(this->key);
4 array<bool,56> keyPC_1;
5 for(int counter=0;counter<56;counter++)
6 {
7  keyPC_1[counter] = keyInit[PC_1[counter]-1];
8 }
9

56位密钥:00000000000000001111111111110110011001111000100000001111

1.3 密钥拆分与移位构成16轮子密钥

现在我们将56位的密钥拆分成前后两个等长部分(28位),C0、D0。我们现在创建16个块Cn 和 Dn, 1<=n<=16。每一对Cn 和 Dn都是由前一对Cn-1 和 Dn-1移位而来。具体说来,对于n = 1, 2, …, 16,在前一轮移位的结果上,使用下表进行一些次数的左移操作。这意味着,比如说,C3 和 D3是C2 和 D2移位而来的,具体来说,通过2次左移位;C16 和 D16 则是由C15和D15通过1次左移得到的。移位完成后,再将其拼接起来。

 1第n轮      左移位数
 21          1
 32          1
 43          2
 54          2
 65          2
 76          2
 87          2
 98          2
109          1
1110          2
1211          2
1312          2
1413          2
1514          2
1615          2
1716          1
18

具体代码如下所示:

 1 //进行16轮移位,获取16个子密钥块
 2 //array<array<bool,48>,16>subKeys; defined in the head file
 3 //array<array<bool,48>,16>subKeys; defined in the head file
 4 for(int iterator=0;iterator<16;iterator++)
 5 {
 6  if(iterator==0)
 7   offKeys[iterator] = SubKeyOff(keyPC_1,keyOff[iterator]);
 8  else
 9   offKeys[iterator] = SubKeyOff(offKeys[iterator-1],keyOff[iterator]);
10 }
11 
12 array<bool,56> DataEncrytionStandard::SubKeyOff(array<bool,56>_key56,int off)
13{
14 //将密钥拆分成左右两半,各28位。
15 list<bool> keyC;
16 list<bool> keyD;
17 //移位后的结果
18 array<bool,56> keyOffResult;
19 //前28位
20 for(int counter=0;counter<28;counter++)
21 {
22  keyC.push_back(_key56[counter]);
23 }
24 //后28位
25 for(int counter=28;counter<56;counter++)
26 {
27  keyD.push_back(_key56[counter]);
28 }
29 //循环移位,for内是一次移位
30 for(int i=0;i<off;i++)
31 {
32  bool temp = keyC.front();
33  keyC.pop_front();
34  keyC.push_back(temp);
35
36  temp = keyD.front();
37  keyD.pop_front();
38  keyD.push_back(temp);
39 }
40 for(int counter = 0;counter<28;counter++)
41 {
42  keyOffResult[counter] = keyC.front();
43  keyC.pop_front();
44 }
45 for(int counter = 28;counter<56;counter++)
46 {
47  keyOffResult[counter] = keyD.front();
48  keyD.pop_front();
49 }
50
51 return keyOffResult;
52}

可以得到如下16组子密钥。

C0 = 0000000000000000111111111111 D0 = 0110011001111000100000001111 C1: 0000000000000001111111111110 D1: 1100110011110001000000011110 C2: 0000000000000011111111111100 D2: 1001100111100010000000111101 C3: 0000000000001111111111110000 D3: 0110011110001000000011110110 C4: 0000000000111111111111000000 D4: 1001111000100000001111011001 C5: 0000000011111111111100000000 D5: 0111100010000000111101100110 C6: 0000001111111111110000000000 D6: 1110001000000011110110011001 C7: 0000111111111111000000000000 D7: 1000100000001111011001100111 C8: 0011111111111100000000000000 D8: 0010000000111101100110011110 C9: 0111111111111000000000000000 D9: 0100000001111011001100111100 C10: 1111111111100000000000000001 D10: 0000000111101100110011110001 C11: 1111111110000000000000000111 D11: 0000011110110011001111000100 C12: 1111111000000000000000011111 D12: 0001111011001100111100010000 C13: 1111100000000000000001111111 D13: 0111101100110011110001000000 C14: 1110000000000000000111111111 D14: 1110110011001111000100000001 C15: 1000000000000000011111111111 D15: 1011001100111100010000000111 C16: 0000000000000000111111111111 D16: 0110011001111000100000001111

1.4子密钥变换

以上获得的16个56位子密钥并不是最后加密用的子密钥,还需要根据PC-2变换成48位的子密钥。于是,第n轮的新秘钥Kn 的第1位来自组合子秘钥CnDn的第14位,第2位来自第17位,依次类推,知道新秘钥的第48位来自组合秘钥的第32位。这才是加密用的子密钥。

1            PC-2
214    17   11    24     1    5
33    28   15     6    21   10
423    19   12     4    26    8
516     7   27    20    13    2
641    52   31    37    47   55
730    40   51    45    33   48
844    49   39    56    34   53
946    42   50    36    29   32
1//第二次转换,通过PC-2将56位子密钥变成48位子密钥,得到最后的第n轮加密使用的子密钥。
2 for(int iterator=0;iterator<16;iterator++)
3 {
4  for(int counter=0;counter<48;counter++)
5   {
6    subKeys[iterator][counter] = offKeys[iterator][PC_2[counter]-1];
7   }
8 }

KS1:010100000010110010101100010101110010101011000010 KS2:010100001010110010100100010100001010001101000111 KS3:110100001010110000100110111101101000010010001100 KS4:111000001010011000100110010010000011011111001011 KS5:111000001001011000100110001111101111000000101001 KS6:111000001001001001110010011000100101110101100010 KS7:101001001101001001110010100011001010100100111010 KS8:101001100101001101010010111001010101111001010000 KS9:001001100101001101010011110010111001101001000000 KS10:001011110101000101010001110100001100011100111100 KS11:000011110100000111011001000110010001111010001100 KS12:000111110100000110011001110110000111000010110001 KS13:000111110000100110001001001000110110101000101101 KS14:000110110010100010001101101100100011100110010010 KS15:000110010010110010001100101001010000001100110111 KS16:010100010010110010001100101001110100001111000000

得到最终的16轮48位子密钥。

2.加密64位的数据块

2.1 字符到比特的转换

本文的明文将以“helloDES”为例。明文的预处理和密钥是一样的,我们这一节只考虑正好8个字节的明文。首先还是要将字符串转换成二进制bit位,这里用的方法和处理密钥的是一样的。

1 array<bool,64> msgInit = CharToBits(this->msg);

2进制明文:01101000 01100101 01101100 01101100 01101111 01000100 01000101 01010011

2.2 初始IP变换

IP是重新变换数据M的每一位产生的。产生过程由下表决定,表格的下标对应新数据的下标,表格的数值x表示新数据的这一位来自旧数据的第x位。原理和密钥的PC-1、PC-2变换也是一样的。区别是IP是一个64位到64位变换,位数不变。

1            IP
258    50   42    34    26   18    10    2
360    52   44    36    28   20    12    4
462    54   46    38    30   22    14    6
564    56   48    40    32   24    16    8
657    49   41    33    25   17     9    1
759    51   43    35    27   19    11    3
861    53   45    37    29   21    13    5
963    55   47    39    31   23    15    7

参照上表,明文2进制的第58位成为IP变换后的第1位,第50位成为IP变换后的第2位,第7位成为IP变换后的最后1位。

1 array<bool,64> msgIP;
2 for(int counter=0;counter<64;counter++)
3  msgIP[counter] = msgInit[IP[counter]-1];

IP转换后 : 11111111 10000000 01111110 11010010 00000000 00011111 00011101 10010000

2.3

3.输入明文拆分与填充

TODO

4.拼接密文块并输出

TODO

遇到的问题

问题1.为什么网上的DES加密算法得到的结果不一样?

这个问题主要涉及编码、输入格式、输出格式、加密模式这几个方面的问题,还有一些细节问题比如空格与回车。 首先是编码问题,在线的编码格式一般默认是UTF-8,因此如果网页编码不是UTF-8,则会导致加密的结果不一样。因为DES算法本质上是对二进制内容进行加密,同样的文字经过不同的编码映射成的二进制内容并不相同。 其次,是输入格式问题。一般在网页的输入是文本格式(Plain Text),但是许多教程为了方便理解,写的输入格式是16进制,比如 DES算法实例讲解 这篇文章里面主要用的是16进制格式作为讲解,对于许多在线工具,明文和密钥输入用的是文本格式。因此,在输入的时候一定要注意区分。 然后,是输出格式的问题。有些在线加密工具输出会自动进行Base64编码,这样结果和直接加密的结果完全不同。DES加密的密文是16进制格式的,无法一一对应成ASCII码。密文要么以16进制输出,要么输出一堆乱码,而Base64能将一个较长的16进制数组编码为一个字符串,方便处理。 最后,是加密模式的问题。DES本身采用的是ECB(电子密码本)模式,即将加密的数据分成若干组,每组的大小跟加密密钥长度相同,这样密文输出完全由明文和密钥决定。为了进一步加强安全性,有许多安全性扩展,就诞生了别的加密模式,比如加密块链模式CBC、加密反馈模式CFB等等。不同的模式加密结果也会完全不同。 在附带一点细节问题,即空格与回车的问题。尤其是在字符串处理的时候,有些字符串会带回车换行(0x0D 0x0A),这会造成最后一个64位字符块加密有些许差别。还有一些文本框自动(trigger)去除空格,就导致文本中的空格没有被计算在内,导致加密不同。