一. DES概述
數據加密標準(Data Encryption Standard),一種使用密鑰加密的塊算法,1977年被美國聯邦政府的國家標準局確定為聯邦資料處理標準(FIPS),并授權在非密級政府通信中使用,隨后該算法在國際上廣泛流傳開來。需要注意的是,在某些文獻中,作為算法的DES稱為數據加密算法(Data Encryption Algorithm,DEA),已與作為標準的DES區分開來。
1. 幾個重要的歷史時間
1973年美國國家標準局(NBS)向社會公開征集加 密算法,以制定加密算法標準;
1974年第二次征集;
1975年選中IBM的算法,并公布征求意見;
1977年1月15日正式頒布;
1998年底以后停用;
1999年頒布3DES為新標準。
2. 標準加密算法的目標
用于加密保護政府機構和商業部門的非機密的敏感 數據。
用于加密保護靜態存儲和傳輸信道中的數據。
安全使用10 ~ 15年。
3.密碼的整體特點
分組密碼,明文、密文和密鑰的分組長度都是64位。
面向二進制的密碼算法,因而能夠加解密任何形式的計算機數據。
-
對合運算:
- f = f^-1
加密和解密共用同一算法,使工程實現的工作量減半。
綜合運用了置換、代替、代數等基本密碼技術。
基本結構屬于Feistel結構。
4. 應用
在全世界范圍得到廣泛應用。
許多國際組織采用為標準。
產品形式:軟件(嵌入式,應用軟件) 硬件(芯片,插卡)
5. 結論
用于其設計目標是安全的。
設計精巧、實現容易、使用方便,堪稱典范。
為國際信息安全發揮了重要作用。
二. 加密過程
64位密鑰經子密鑰產生算法產生出16個子密鑰: K1, K2, ..., K16 , 分別供第一次, 第二次, ..., 第十六次加密迭代使用。
64位明文經初始置換IP, 將數據打亂重排并分成左右兩半。左邊32位構成L0 , 右邊2位構成R0 。
第一次加密迭代:由加密函數f實現子密鑰k1對R0的加密,得到32位的f(R0, K1),然后L0⊕f(R0, K1),32位的結果作為第二次加密迭代的R1,以R0作為第二次加密迭代的L1。
第二次加密迭代至第十六次加密迭代分別用子密鑰K2 ,..., K16進行,其過程與第一次加密迭代相同。
第十六次加密迭代結束后,產生一個64位的數據組,以其左邊32位作為R16, 以其右邊32位作為L16 。
R16與L16合并,再經過逆初始置換IP ^–1, 將數據重新排列,便得到64位密文。
1. 子密鑰的產生
64位密鑰經過置換選擇1、循環左移、置換選擇2等變換,產生16個子密鑰 K1,K2。… K16,分別供各次加密迭代使用。
( 1 ). 置換選擇1 (Permuted Choice 1)
64位的密鑰分為8字節,每個字節的第八位是奇偶校驗位,前七位才是真正的密鑰位。奇偶校驗位用于密鑰檢錯,確保其完整性;它不是隨機的,可由前七位密鑰位算得。因此,DES真正的密鑰只有56位。
置換選擇1的作用:
去掉密鑰中的8個奇偶校驗位。
把其余的56位打亂重排,將前28位作為C0, 后28位作為D0 。
置換規則:C0的各位依次為原密鑰的第57, 49, ..., 1, ..., 44, 36位;D0的各位依次為原密鑰的第63, 55, ..., 7, ..., 12, 4位。
( 2 ). 循環左移 (Left Shift)
對Ci , Di分別循環左移n位,其中n是會隨著迭代次數變化的,其與迭代次數映射表如下所示
( 3 ). 置換選擇2 (Permuted Choice 2)
64位的密鑰分成32位的兩份,進過置換選擇后各成為28位的數據(即C0和D0),就是說Ci和Di都是28位的數據。將Ci和Di合并成一個56位的數據,置換選擇2從中選出一個48位的子密鑰Ki。規定子密鑰Ki的56位依次是該56位中間數據的第14, 17, ..., 5, 3, ..., 29, 32位,其置換表如下所示
( 4 ). 代碼實現
為了方便理清生成子密鑰的邏輯,其中的打印輸出的代碼并沒有給出,完整源碼見該項目Github倉庫
/**
* 生成16個48位的子密鑰
* @param keyBytes 64位原密鑰,用字符數組存儲其比特串
* @return 子密鑰數組,用二維字符數組表示
*/
private char[][] generateSubKeys(char[] keyBytes) {
char[][] subKeys = newchar[16][48];
// 置換選擇 1
char[] c = ArrayUtil.disruptArray(keyBytes,DESConstants.replace1C);
char[] d = ArrayUtil.disruptArray(keyBytes,DESConstants.replace1D);
// 循環左移
for(int i = 0;i < 16; i++) {
c = ArrayUtil.leftShift(c,DESConstants.moveBit[i]);
d = ArrayUtil.leftShift(d,DESConstants.moveBit[i]);
// 將Ci和Di合并得到56位的中間數據
char[] concatChars = ArrayUtil.concat(c,d);
// 置換選擇 2,得到48位的子密鑰Ki
char[] key = ArrayUtil.disruptArray(concatChars,
DESConstants.replace2);
subKeys[i] = key;
}
return subKeys;
}
2. 初始置換IP
初始置換是DES算法的第一步密碼變換,它的作用是將64位的明文打亂重排,并分成左右兩半。左邊32位作為L0,右邊32位作為R0,供后面迭代使用。規定置換后的64位數據的各位依次是原明文數據的第58, 50, ..., 2, 60, ..., 15, 7位,其置換表如下所示
3. 加密函數
加密函數是DES的核心,它的租用是在第i次迭代中用子密鑰Ki對R(i-1)進行加密。其運行規則是:在第i次迭代加密中選擇運算E對32位的R(i-1)的各位進行選擇排列,產生48位的結果,此結果與48位的子密鑰進行異或運算,然后送入替代函數組S。S由8個替代函數(替代盒,Substitute Box)組成,每個S盒有6位輸入和4位輸出。8個S盒的輸出合并得到一個32位的數組,此數據組經過置換運算P,將各位打亂重排,得到的結果便是加密函數的返回值f(R(i-1), Ki)。
1. 選擇運算 E
該過程對32位的數據組Half Block的各位進行選擇和排列,產生一個48位的結果,可見該運算是一個擴展運算,它將32位的數據擴展成了48位的數據,以便與48位的子密鑰進行異或運算,下面是選擇運算的運算矩陣,可見它是通過重復選擇某些數據位來達到數據擴展的目的的。
2. 替代函數組 S
由8個S盒(S1, S2, S3, S4, S5, S6, S7, S8)組成,S的輸入是一個48位的數據,從1到48位依次加到8個S盒的輸入端。每個S盒有一個替代矩陣,規定了其輸入輸出的替代規則。替代矩陣有4行16列,每行都是0到15這16個數字,但每行數字的排列都不同,且8個替代矩陣彼此不同。每個S盒有6位輸入,4位輸出,S盒的運算結果是用輸出數據替代輸入數據,故稱為替代函數。
S盒的替代規則為:6位輸入的第1位和第6位組成二進制數b1b6代表對應矩陣中被選中的行號,其余四位數字b2b3b4b5組成的二進制數代表對應矩陣中被選中的列號。以S1為例,假設輸入數據為b1b2b3b4b5b6 = 101011,則第1位和第6位組成二進制數b1b6 = 11 = 3,表示選中行號為3的那行;b2b3b4b5 = 0101 = 5,表示選中列號為5的那列,行列交點為9,則S1的輸出為1001。替代函數組 S中各S盒矩陣如下所示
3. 置換運算 P
該過程是把S盒輸出的32位數據打亂重排,得到32位的加密函數輸出,用P置換來擴散,將S盒的混淆租用擴散開來,正是置換P與S盒的互相配合提高了DES的安全性,置換矩陣P如下所示
4. 代碼實現
/**
* DES核心加密函數
* 包括擴展、替代、選擇等操作
* @param right R(i-1)
* @param subKey Ki
* @return result 運算結果
*/
private char[] coreEncrypt(char[] right, char[] subKey) {
// 1\. 選擇運算 E
char[] extendedRight = ArrayUtil.disruptArray(right, DESConstants.E);
// 2\. 將晉國選擇運算E擴展得到的48位的數據與子密鑰進行異或
char[] xorResult = ArrayUtil.xor(extendedRight,subKey);
// 3\. 用替代函數組進行替代
// 為便于處理,將上述1x48位的數據矩陣轉換成8x6的數據矩陣
char[][] twoDimensionArray = ArrayUtil.segmentDimension(xorResult,8,6);
StringBuilder outputBuilder = new StringBuilder();
// 根據替代規則進行替代
for(inti=0;i<twoDimensionArray.length;i++) {
char[] rowBits = {
twoDimensionArray[i][0],
twoDimensionArray[i][5]
};
char[]columnBits = {
twoDimensionArray[i][1], twoDimensionArray[i][2],
twoDimensionArray[i][3], twoDimensionArray[i][4]
};
// 獲取對應S盒的輸出的坐標
int rowIndex = Integer.parseInt(String.valueOf(rowBits), 2);
int columnIndex = Integer.parseInt(String.
valueOf(columnBits), 2);
// 獲取對應S盒的輸出
short output = DESConstants.SUBSTITUTE_BOX[i][rowIndex][columnIndex];
outputBuilder.append(Integer.toBinaryString((output&0x0f) + 0x10).substring(1));
}
char[] substitutedResult = outputBuilder.toString().toCharArray();
// 4\. 進行置換運算 P,返回28位的數據
return ArrayUtil.disruptArray(substitutedResult,DESConstants.P);
}
4. 整個加密過程
回顧DES的總體加密過程
64位密鑰經子密鑰產生算法產生出16個子密鑰: K1, K2, ..., K16 , 分別供第一次, 第二次, ..., 第十六次加密迭代使用。
64位明文經初始置換IP, 將數據打亂重排并分成左右兩半。左邊32位構成L0 , 右邊2位構成R0 。
第一次加密迭代:由加密函數f實現子密鑰k1對R0的加密,得到32位的f(R0, K1),然后L0⊕f(R0, K1),32位的結果作為第二次加密迭代的R1,以R0作為第二次加密迭代的L1。
第二次加密迭代至第十六次加密迭代分別用子密鑰K2 ,..., K16進行,其過程與第一次加密迭代相同。
第十六次加密迭代結束后,產生一個64位的數據組,以其左邊32位作為R16, 以其右邊32位作為L16 。
R16與L16合并,再經過逆初始置換IP ^–1, 將數據重新排列,便得到64位密文。
整體的加密過程函數
/**
* 對外的加密接口,主要邏輯封裝在encode方法中
* @param plaintext 明文
* @param key 密鑰
* @return encrypted text
* @throws UnsupportedEncodingException caused by String.getBytes()
*/
public Stringencrypt(String plaintext,String key) throws UnsupportedEncodingException {
// 獲取明文及密鑰對應的比特串,用字符數組存儲
char[] plaintextBytes = ArrayUtil.bytesToChars(
plaintext.getBytes("UTF-8"));
char[]keyBytes=ArrayUtil.bytesToChars(
key.getBytes("UTF-8"));
// 子密鑰的生成
char[][] subKeys = generateSubKeys(keyBytes);
char[] result = encode(plaintextBytes,subKeys);
// 使用Base64編碼對不可見及非打印字符進行編碼
returnBase64Util.encode(result);
}
/**
* 主體加密邏輯
* @param plaintextBytes 用字符數組存放的明文的比特串
* @param subKeys 子密鑰數組
* @return encryption 64位加密結果
*/
private char[] encode(char[] plaintextBytes, char[][] subKeys) {
// 初始置換 IP
char[] chars = ArrayUtil.disruptArray(plaintextBytes,DESConstants.IP);
// 將明文分成兩半
int length = chars.length;
String binaryArrayStr = String.valueOf(chars);
char[] left = binaryArrayStr.substring(0, length/2).toCharArray();
char[] right=binaryArrayStr.substring(length/2).toCharArray();
char[]coreEncrypted,xorResult;
for(inti=0;i<16;i++) {
// 調用核心加密函數,用子密鑰Ki對R(i-1)進行加密,得到28位數據
coreEncrypted = coreEncrypt(right, subKeys[i]);
// L(i - 1)與f(R(i - 1), Ki)進行異或運算
xorResult = String.valueOf(ArrayUtil.xor(left, coreEncrypted))
.substring(16).toCharArray();
left=right;
right=xorResult;
}
char[] calResult = ArrayUtil.concat(right,left);
// 逆初始置換
return ArrayUtil.disruptArray(calResult,DESConstants.inverseIP);
}
三. 解密過程
通過數學推理可證明DES具有可逆性和對合性的(限于蝙蝠,在此不作證明),即加密和解密可共用同一個運算,只是子密鑰的使用順序調轉而已,即第一次解密迭代使用子密鑰K16,第十六次解密迭代使用子密鑰K1。
代碼實現
基于上述的思想,我們可以將通過密鑰獲取到的子密鑰數組逆轉,然后調用encode方法即可
/**
* 解密
* @param encryptedText 加密數據
* @param key 密鑰
* @return decrypted origin plaintext
* @throws UnsupportedEncodingException caused by String.getBytes()
*/
@Override
public String decrypt(String encryptedText,String key) throws UnsupportedEncodingException {
// 將已使用Base64編碼的密文解碼
char[] encryptedTextBytes = Base64Util.decode(encryptedText);
char[]keyBytes = ArrayUtil.bytesToChars(
key.getBytes("UTF-8"));
// 將通過密鑰獲取到的子密鑰數組逆轉
char[][] inverseKeys = inverseSubKeys(generateSubKeys(keyBytes));
// 解密
char[] result = encode(encryptedTextBytes, inverseKeys);
// 將比特串還原為字符串明文
return ArrayUtil.segmentAndPrintChars("decrypt plaintext text", result);
}
/**
* 將通過密鑰獲取到的子密鑰數組逆轉
* @param subKeys 原子密鑰數組
* @return 翻轉的子密鑰數組
*/
private char[][] inverseSubKeys(char[][] subKeys) {
char[][] inverseKeys = new char[subKeys.length][];
for(inti = 0; i < subKeys.length; i++) {
inverseKeys[i] = subKeys[subKeys.length - 1 - i];
}
returninverseKeys;
}
四. 測試
1. 測試代碼
使用Junit進行單元測試
@Test
public void testService() {
Stringplaintext = "01234567", key = "12345678";
CipherService cipherService = new DESCipherService();
try{
String encryptedText = cipherService.encrypt(plaintext, key);
cipherService.decrypt(encryptedText,key);
}catch(UnsupportedEncodingExceptione) {
e.printStackTrace();
}
}
由于簡書文章的字數限制,測試結果請見Github倉庫
五. 代碼說明
1. 源碼
本文中的代碼已發布到Github倉庫供學習討論,如果你覺得寫得還不錯,歡迎star和fork; 如果對代碼有疑惑,歡迎通過郵箱與我討論。
2. 缺陷
該DES實現旨在理解DES的原理,在軟件工程的視角它存在一些實用性上的不足,例如
明文小于64位應該進行填充
明文大于64位應該進行分組,待加密完成后進行密文的合并
沒有就數據編碼進行擴展,暫不支持中文數據加密