本文主要是對《信息安全技術》的DES算法實驗作業的一些總結,不會着重地介紹算法原理,而會在算法實現過程中給出自己的理解(因爲有些部分我也不知道正確與否,如有錯誤請指教)。文章中出現的原理介紹和配圖,均參考自其它博客,相關鏈接將在文中給出。
另外,文中的代碼都是根據內容截取的,若想查看完整代碼,請參考 DES - github
一、DES算法簡介(參考自DES算法實例詳解)
DES(Data Encryption Standard)是一種用於電子數據加密的對稱密鑰塊加密算法。它以64bit一組的明文(Input)作爲算法的輸入,通過一系列複雜的操作,輸出同樣64bit長度的密文(Output)。DES 同樣採用64位密鑰(Key),但由於每8bit中的最後1位用於奇偶校驗,實際有效密鑰長度爲56bit(Tips:輸入的Key依然是64bit,只是在映射時不對每個字節最後1位進行處理,所以變爲了56bit)。
DES 使用加密密鑰定義變換過程,因此算法認爲只有持有加密所用的密鑰的用戶才能解密密文。DES的兩個重要的安全特性是混淆和擴散。其中混淆是指通過密碼算法使明文和密文以及密鑰的關係非常複雜,無法從數學上描述或者統計。擴散是指明文和密鑰中的每一位信息的變動,都會影響到密文中許多位信息的變動,從而隱藏統計上的特性,增加密碼的安全。
上圖是DES算法的流程圖。左側是算法的工作流程,可以看到:
(1)用64bit的密鑰Key產生16個48bit的子密鑰;
(2)對輸入的64bit明文先進行了Initial Permutation(IP初始置換);
(3)將64bit分爲左右兩部分(各32bit),用16個子密鑰輔助進行16輪的F變換;
(4)兩部分合成爲64bit,進行Final Permutation(FP最終置換/逆置換)形成密文輸出
而我們可以看到,在(3)中的16輪變換中,都需要右側圖中由密鑰(Key)產生的子密鑰(Sub-Key #i)。因此,我們可以首先來處理初始置換部分和子密鑰算法部分。
二、算法準備
2.1、數據結構
首先要確定的是數據結構,即如何保存算法過程中出現的二進制、八進制和字符串數據?顯然的,在C++中,我們可以全都用string來進行存儲,但是對於算法中經常進行的異或操作,就不是很方便了。
對於整數和單個字符,我們的數據範圍不會很大,不論是字符還是數字使用unsigned char即可。因此先定義類型:
typedef unsigned char Byte;
對於其它數據,我們使用string類型來保存這些數據。但是需要進行位操作時,我們就將這些數據轉換爲C++中bitset類型來保存。關於bitset類型的介紹可以參考博客C++ bitset 用法,這裏不再贅述。但是有一點需要特別特別注意,使用索引index來獲取數據時,是從低位開始的。也就是說,若有bitset<4> bs("0001"),那麼bs[0]==1,bs[3]==0。
2.2、置換操作
在算法過程中,我們經常可以看到對位數據進行置換操作。那麼到底置換操作是怎麼進行的呢?
// 要特別注意的是bitset的index由0->size-1是從 低位->高位,即右邊->左邊
// 置換操作模板函數
template <typename Input, typename Output>
void DES_Permutation(const Input& input, Output& output
, const Byte Table[], const Byte tableSize)
{
for(Byte i = 0; i < tableSize; ++i)
output[tableSize - i - 1] = input[input.size() - Table[i]];
}
舉一個簡單的例子,bitset<8> input("00101001"),input.size()爲8。現在有置換數組Table {3, 1, 7, 5, 2, 8, 4, 6},tableSize爲8。那麼現在再來一個bitset<8> output,經過循環置換操作後output的結果爲 "10010100"。
置換過程簡單來說,就是如下圖所示:從左邊開始數,input中第[3,1,7,5,2,8,4,6]個位依次映射到output第[1,2,3,4,5,6,7,8]中。由於我們使用的bitset中的索引0表示低位,即從右邊開始數,因此需要進行一個如代碼中所示的下標轉換。
2.3、置換表和常量定義
瞭解了2.2中的置換操作,就知道這些置換數組的重要性了。在DES算法中,我們有很多的置換表,這裏和一些常量、枚舉類型統一給出,若在下文遇到沒見過的數組、枚舉,請回這裏查找:
const Byte SIZE_INPUT = 64;
const Byte SIZE_OUTPUT = 64;
const Byte SIZE_DIVIDE = 28;
const Byte NUM_SONKEY = 16;
const Byte SIZE_SONKEY = 48;
enum plainTextMode {TEXT, BINARY, HEX}; // 明文模式:文本,二進制,十六進制
enum operateMode {ENCODE, DECODE}; // 工作模式:加密,解密
enum encodeMode {ECB, CBC, CFB, OFB};// 加密模式:ECB, CBC, CFB, OFB
// 初始置換表64bit
const Byte InitPerm_Table[] = {
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
};
// 選擇置換表1,28*2 bit,其中未出現的 8,16,24,32,40,48,56,64做奇偶校驗位
const Byte PC_Table1[] = {
// Ci
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,
// Di
63, 55, 47, 39, 31, 23, 15,
7, 62, 54, 46, 38, 30, 22,
14, 6, 61, 53, 45, 37, 29,
21, 13, 5, 28, 20, 12, 4
};
// 選擇置換表2,6*8 bit
const Byte PC_Table2[] = {
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
};
// 左移位數表
const Byte LeftMove_Table[] = {
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
};
// 擴展表
const Byte Expansion_Table[] = {
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
};
// S盒,8*64bit
const Byte SBox_Table[][4][16] = {
{
{14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7},
{0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8},
{4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0},
{15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13},
},
{
{15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10},
{3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5},
{0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15},
{13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9},
},
{
{10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8},
{13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1},
{13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7},
{1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12},
},
{
{7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15},
{13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9},
{10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4},
{3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14},
},
{
{2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9},
{14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6},
{4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14},
{11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3},
},
{
{12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11},
{10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8},
{9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6},
{4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13},
},
{
{4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1},
{13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6},
{1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2},
{6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12},
},
{
{13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7},
{1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2},
{7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8},
{2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11},
}
};
// P盒,4*8bit
const Byte PBox_Table[] = {
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
};
// 逆初始置換表64bit
const Byte FinalPerm_Table[] = {
40, 8, 48, 16, 56, 24, 64, 32,
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25
};
三、算法實現
3.1、初始置換
非常的簡單,有了上面2.2的置換操作DES_Permutation,我們只需要傳入特定的數據即可。首先創建一個64bit的IP用作輸出,其中plaintext爲64bit明文輸入(類型均爲bitset<64>)。
bitset<SIZE_OUTPUT> IP;
DES_Permutation(plaintext, IP, InitPerm_Table, SIZE_INPUT);
3.2、子密鑰生成
首先需要64bit的Key映射爲56bit,然後將其一分爲二爲Ci和Di(均爲28bit)。有人可能會問64bit是怎麼轉成56bit的?其實很簡單,回到2.3中的PC_Table1,它只有56個數,並且少了[8, 16, 24, 32, 40, 48, 56, 64]這8個下標,因此映射到K中的也只有56個bit。這正對應了本文開頭時Tips所述。
接下來是對Ci和Di進行16輪的複雜操作(搞清楚一遍的原理就夠了,剩下的都是for循環)。
1、首先對Ci和Di分別進行左移位操作(移幾位參考LeftMove_Table表),注意這裏的移位操作是循環的。也就是說,左移位溢出的位不會丟棄,而是移動到右邊去。舉個例子,有bitset<8> bs("10001011"),左移兩位,則變爲bs("00101110")。
// 旋轉左移
template <typename Input>
void DES_LeftRotation(Input& bs, Byte count)
{
while( count-- )
{
Byte bit = bs[SIZE_DIVIDE - 1];
bs <<= 1;
bs[0] = bit;
}
}
2、接下來,Ci和Di合併爲bitset<56>的臨時變量bs_merge,再使用PC_Table2表對其置換,即得到了第i個密鑰Ki[i]
依次對操作1和2循環16次,即完成了子密鑰的生成,以下給出生成子密鑰完整代碼實現:
// 生成的子密鑰保存在Ki[]中
void getKeyTable(const bitset<SIZE_INPUT>& key, bitset<SIZE_SONKEY> Ki[])
{
// 交換生成56bit的K
bitset<SIZE_DIVIDE*2> K;
DES_Permutation(key, K, PC_Table1, SIZE_DIVIDE*2);
// 一分爲二生成初始C0, D0
string str_K = K.to_string();
bitset<SIZE_DIVIDE> Ci(str_K.substr(0, SIZE_DIVIDE));
bitset<SIZE_DIVIDE> Di(str_K.substr(SIZE_DIVIDE));
// 生成16個子密鑰
for(Byte i = 0; i < NUM_SONKEY; ++i){
// 旋轉左移
DES_LeftRotation(Ci, LeftMove_Table[i]);
DES_LeftRotation(Di, LeftMove_Table[i]);
// 合併置換
bitset<2*SIZE_DIVIDE> bs_merge(Ci.to_string() + Di.to_string());
DES_Permutation(bs_merge, Ki[i], PC_Table2, SIZE_SONKEY);
}
}
3.3、Feistel輪函數
同樣的,對應上述生成的16個子密鑰,輪函數同樣需要進行16次。我們首先將經過初始置換生成的IP一分爲二,變爲28bit的Li和Ri。
// 生成初始L0, R0
string str_IP = IP.to_string();
bitset<SIZE_INPUT/2> Li(str_IP.substr(0, SIZE_INPUT/2)), tmpL;
bitset<SIZE_INPUT/2> Ri(str_IP.substr(SIZE_INPUT/2)) , tmpR;
其中的tmpL,tmpR用於暫存原來的Li和Ri值,因爲在輪函數中它們的值會變化,而最後又會用到原來的值。
接下來正式進入16輪循環。對於每一輪循環,首先用擴展表將Ri擴展至48bit,存放於Ei中。
bitset<SIZE_SONKEY> Ei; // 保存中間結果
DES_Permutation(Ri, Ei, Expansion_Table, SIZE_SONKEY);
然後將擴展結果Ei與子密鑰進行異或(加密過程對應第i個子密鑰,解密過程對應第16-i個子密鑰)。
// 與密鑰Ki異或
if( opMode == ENCODE ) Ei ^= Ki[i];
else Ei ^= Ki[15 - i];
再來就是對上述的Ei進行復雜的S盒映射操作,S盒是一個8*4*16的三維數組。
我們將Ei分爲8組,每組6bit,共操作8次。第i次(0~7)我們使用第i個子二維數組進行操作。其中這6bit的首尾位構成行號(0~3),中間四位構成列號(0~15)。然後到S盒中取數,生成4bit的結果,累加到結果res中。因爲這個過程有8輪,因此最終會生成32bit的數據。
string res = "";
for(Byte j = 0; j < SIZE_SONKEY; j += 6){
string str_col = Ei.to_string().substr(j+1, 4);
// 轉化行、列
Byte row = Ei[SIZE_SONKEY-j-1]*2 + Ei[SIZE_SONKEY-j-6];
Byte col = bitset<4>(str_col).to_ulong();
// 獲取S-BOX的值
Byte value = SBox_Table[j/6][row][col];
res += bitset<4>(value).to_string();
}
// 再轉換回32bit
bitset<32> bs_tmp(res);
最後將這32位數據最爲輸入,使用P盒進行置換,再將結果與上一輪的Li異或,便得到了下一輪的Ri;而下一輪的Li,則直接拷貝上一輪的Ri即可。
// P盒置換
DES_Permutation(bs_tmp, Ri, PBox_Table, SIZE_INPUT/2);
Ri ^= tmpL;
Li = tmpR;
注意到這16輪算法中的最後一輪我們是不需要交換Li和Ri位置的,而我們在循環中並未做特殊處理。因此最終我們得到的預輸出密文應當是Ri在左,Li在右。
bitset<64> res(Ri.to_string() + Li.to_string());
DES_Permutation(res, ciphertext, FinalPerm_Table, SIZE_OUTPUT);
將預輸出密文最後做一次IP逆置換,即得到了最終的密文ciphertext。至此,DES算法實現完成。
四、四種模式的實現
四種模式ECB, CBC, CFB, OFB的枚舉常量encodeMode均已在前文定義,並同時定義了明文的輸入類型plainTextMode以及算法的工作模式operateMode。以下四種模式我沒有單獨給定函數,而是集成在了2個函數中(CFB模式使用8位流密碼,我單獨爲它寫了一個函數),因此不會單獨給出實現,而是給出完整代碼,並加上註釋。
4.1、電碼本模式(ECB)
電碼本模式即是對輸入明文進行分組加密(通常爲64bit)。由於電碼本模式對任何分組都是使用同一密鑰加密,因此若分組中若有相同的明文組,那麼密文中也會出現幾個相同的密文組。因此ECB模式特別適合於數據較少的情況,如加密一個密鑰。
4.2、密文分組鏈接模式(CBC)
爲了克服上述ECB的問題,我們將明文組先和上一個密文組異或,再進行相同的操作。顯然,第一個分組加密時沒有上一個密文組,因此我們需要設置一個64bit初始向量IV充當這個角色。
4.3、密文反饋模式(CFB)
上述兩種模式都需要對明文進行64位的分組加密,若分組不夠64bit的,還需要補0。在CFB模式中,我們的分組是1個字符(8個位),因此不存在這種困擾。我們同樣需要一個64bit初始化向量IV充當輸入,輸出的IV_DES高8位與8bit明文異或得到密文;同時IV左移8位,用密文填充低8位後充當下一次的IV。特別提醒:CFB模式加/解密使用密鑰Ki的順序要一致!
4.4、輸出反饋模式(OFB)
與CFB模式類似,只是它又變回64bit分組了。因此,IV輸出的IV_DES不需要取高8位,而是直接與明文分組異或。下一個IV也不需要移位填充,而是直接由IV_DES充當即可。特別提醒:OFB模式加/解密使用密鑰Ki的順序要一致!
// ECB, CBC, OFB模式實現(均以64bit分組)
string str_binaryCpText = "";
for(int i = 0; i < str_binaryPlText.size(); i += SIZE_INPUT)
{
// 每次截取64bit明文進行加密
string sub_binary = str_binaryPlText.substr(i, SIZE_INPUT);
bitset<SIZE_INPUT> plaintext(sub_binary);
bitset<SIZE_OUTPUT> ciphertext;
// ECB 電子密本模式
if( enMode == ECB ){
myDES(plaintext, Ki, ciphertext, opMode);
}
// CBC 密文分組鏈接模式
else if( enMode == CBC )
{
if( opMode == ENCODE )
plaintext ^= IV;
myDES(plaintext, Ki, ciphertext, opMode);
if( opMode == ENCODE )
IV = ciphertext;
else{
ciphertext ^= IV;
IV = plaintext;
}
}
// OFB 輸出反饋模式
else if( enMode == OFB ){
// OFB模式加/解密時使用密鑰Ki的順序一致
myDES(IV, Ki, ciphertext, ENCODE);
IV = ciphertext;
ciphertext ^= plaintext;
}
// OFB 輸出反饋模式
else if( enMode == OFB ){
// OFB模式加/解密時使用密鑰Ki的順序一致
myDES(IV, Ki, ciphertext, ENCODE);
IV = ciphertext;
ciphertext ^= plaintext;
}
// 64bit明文加密後的密文累加
str_binaryCpText += ciphertext.to_string();
}
// OFB模式需要捨去填充的位
if( enMode == OFB ){
int size = str_binaryCpText.size();
str_binaryCpText = str_binaryCpText.substr(0, size - fillSize);
}
// CFB模式(8bit流分組)
string str_binaryCpText = "";
for(int i = 0; i < str_binaryPlText.size(); i += 8)
{
// 每次截取8bit明文進行加密
string sub_binary = str_binaryPlText.substr(i, 8);
bitset<8> plaintext(sub_binary);
bitset<SIZE_OUTPUT> IV_DES;
cout << "plaintext : " << plaintext << endl;
myDES(IV, Ki, IV_DES, ENCODE);
bitset<8> ciphertext(IV_DES.to_string().substr(0, 8));
ciphertext ^= plaintext;
cout << "ciphertext : " << ciphertext << endl << endl;
str_binaryCpText += ciphertext.to_string();
IV <<= 8;
if( opMode == ENCODE )
IV |= bitset<64>( ciphertext.to_string() );
else
IV |= bitset<64>( plaintext.to_string() );
}
四種模式中均爲對二進制流操作,而輸入明文可爲“文本”、“十六進制”、“二進制”,因此在這兩個函數的前後還有一些文本<->進制的轉換,具體函數及模式完整代碼請到Github倉庫中查看。