日常開發中,無論你是使用什麼語言,都應該遇到過使用加解密的使用場景,比如介面資料需要加密傳給前端保證資料傳輸的安全;HTTPS使用證照的方式首先進行非對稱加密,將客戶端的私匙傳遞給服務端,然後雙方後面的通訊都使用該私匙進行對稱加密傳輸;使用MD5進行檔案一致性校驗,等等很多的場景都使用到了加解密技術。
很多時候我們對於什麼時候要使用什麼樣的加解密方式是很懵的。因為可用的加解密方案實在是太多,大家對加解密技術的型別可能不是很清楚,今天這篇文章就來梳理一下目前主流的加解密技術,本篇文件只針對演算法做科普性說明,不涉及具體演算法分析。日常使用的加解密大致可以分為以下四類:
- 雜湊函式(也稱資訊摘要)演算法
- 對稱加密演算法
- 非對稱加密演算法
- 組合加密技術
1. 雜湊函式演算法
聽名字似乎不是一種加密演算法,類似於給一個物件計算出hash值。所以這種演算法一般用於資料特徵提取。常用的雜湊函式包括:MD5、SHA1、SHA2(包括SHA128、SHA256等)雜湊函式的應用很廣,雜湊函式有個特點,它是一種單向加密演算法,只能加密、無法解密。
1.1 MD5
先來看MD5演算法,MD5演算法是廣為使用的資料特徵提取演算法,最常見的就是我們在下載一些軟體,網站都會提供MD5值給你進行校驗,你可以通過MD5值是否一致來檢查當前檔案是否被別人篡改。MD5演算法具有以下特點:
- 任意長度的資料得到的MD5值長度都是相等的;
- 對原資料進行任一點修改,得到的MD5值就會有很大的變化;
- 雜湊函式的不可逆性,即已知原資料,無法通過特徵值反向獲取原資料。(
需要說明的是2004年的國際密碼討論年會(CRYPTO)尾聲,王小云及其研究同事展示了MD5、SHA-0及其他相關雜湊函式的雜湊衝撞。也就是說,她找出了第一個 兩個值不同,但 MD5 值相同的碰撞的例子。這個應該不能稱之為破解
)
1.2 MD5用途:
- 防篡改。上面說過用於檔案完整性校驗。
- 用於不想讓別人看到明文的地方。比如使用者密碼入庫,可以將使用者密碼使用MD5加密儲存,下次使用者輸入密碼登入只用將他的輸入進行MD5加密與資料庫的值判斷是否一致即可,這樣就有效防止密碼洩露的風險。
- 用於檔案秒傳。比如百度雲的檔案秒傳功能可以用這種方式來實現。在你點選上傳的時候,前端同學會先計算檔案的MD5值然後與服務端比對是否存在,如果有就會告訴你檔案上傳成功,即完成所謂的秒傳。
在JDK中提供了MD5的實現:java.security
包中有個類MessageDigest
,MessageDigest 類為應用程式提供資訊摘要演算法的功能,如 MD5 或 SHA 演算法。資訊摘要是安全的單向雜湊函式,它接收任意大小的資料,輸出固定長度的雜湊值。
MessageDigest 物件使用getInstance
函式初始化,該物件通過使用 update 方法處理資料。任何時候都可以呼叫 reset 方法重置摘要。一旦所有需要更新的資料都已經被更新了,應該呼叫 digest 方法之一完成雜湊計算。
對於給定數量的更新資料,digest 方法只能被呼叫一次。digest 被呼叫後,MessageDigest 物件被重新設定成其初始狀態。
下面的例子展示了使用JDK自帶的MessageDigest類使用MD5演算法。同時也展示瞭如果使用了update
方法後沒有呼叫digest
方法,則會累計當前所有的update中的值在下一次呼叫digest方法的時候一併輸出:
package other;
import java.security.MessageDigest;
/**
* @author: rickiyang
* @date: 2019/9/13
* @description:
*/
public class MD5Test {
static char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
public static void main(String[] args) {
try {
//申明使用MD5演算法
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update("a".getBytes());//
System.out.println("md5(a)=" + byte2str(md5.digest()));
md5.update("a".getBytes());
md5.update("bc".getBytes());
System.out.println("md5(abc)=" + byte2str(md5.digest()));
//你會發現上面的md5值與下面的一樣
md5.update("abc".getBytes());
System.out.println("md5(abc)=" + byte2str(md5.digest()));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 將位元組陣列轉換成十六進位制字串
*
* @param bytes
* @return
*/
private static String byte2str(byte[] bytes) {
int len = bytes.length;
StringBuffer result = new StringBuffer();
for (int i = 0; i < len; i++) {
byte byte0 = bytes[i];
result.append(hex[byte0 >>> 4 & 0xf]);
result.append(hex[byte0 & 0xf]);
}
return result.toString();
}
}
輸出:
md5(a)=0CC175B9C0F1B6A831C399E269772661
md5(abc)=900150983CD24FB0D6963F7D28E17F72
md5(abc)=900150983CD24FB0D6963F7D28E17F72
2.1 SHA系列演算法
Secure Hash Algorithm,是一種與MD5同源的資料加密演算法。SHA演算法能計算出一個數位資訊所對應到的,長度固定的字串,又稱資訊摘要。而且如果輸入資訊有任何的不同,輸出的對應摘要不同的機率非常高。因此SHA演算法也是FIPS所認證的五種安全雜湊演算法之一。原因有兩點:一是由資訊摘要反推原輸入資訊,從計算理論上來說是極為困難的;二是,想要找到兩組不同的輸入資訊發生資訊摘要碰撞的機率,從計算理論上來說是非常小的。任何對輸入資訊的變動,都有很高的機率導致的資訊摘要大相徑庭。
SHA實際上是一系列演算法的統稱,分別包括:SHA-1、SHA-224、SHA-256、SHA-384以及SHA-512。後面4中統稱為SHA-2,事實上SHA-224是SHA-256的縮減版,SHA-384是SHA-512的縮減版。各中SHA演算法的資料比較如下表,其中的長度單位均為位:
類別 | SHA-1 | SHA-224 | SHA-256 | SHA-384 | SHA-512 |
---|---|---|---|---|---|
訊息摘要長度 | 160 | 224 | 256 | 384 | 512 |
訊息長度 | 小於264位 | 小於264位 | 小於264位 | 小於2128位 | 小於2128位 |
分組長度 | 512 | 512 | 512 | 1024 | 1024 |
計算字長度 | 32 | 32 | 32 | 64 | 64 |
計算步驟數 | 80 | 64 | 64 | 80 | 80 |
SHA-1演算法輸入報文的最大長度不超過264位,產生的輸出是一個160位的報文摘要。輸入是按512 位的分組進行處理的。SHA-1是不可逆的、防衝突,並具有良好的雪崩效應。
上面提到的MessageDigest
類同時也支援SHA系列演算法,使用方式與MD5一樣,注意SHA不同的型別:
MessageDigest md = MessageDigest.getInstance("SHA");
MessageDigest md = MessageDigest.getInstance("SHA-224");
MessageDigest md = MessageDigest.getInstance("SHA-384");
2. 對稱加密演算法
所謂的對稱加密,意味著加密者和解密者需要同時持有一份相同的密匙,加密者用密匙加密,解密者用密匙解密即可。
常用的對稱加密演算法包括DES演算法、AES演算法等。 由於對稱加密需要一個祕鑰,而祕鑰在加密者與解密者之間傳輸又很難保證安全性,所以目前用對稱加密演算法的話主要是用在加密者解密者相同,或者加密者解密者相對固定的場景。
對稱演算法又可分為兩類:
第一種是一次只對明文中的單個位(有時對位元組)運算的演算法稱為序列演算法或序列密碼;
另一種演算法是對明文的一組位進行運算,這些位組稱為分組,相應的演算法稱為分組演算法或分組密碼。現代計算機密碼演算法的典型分組長度為64位――這個長度既考慮到分析破譯密碼的難度,又考慮到使用的方便性。
2.1 BASE64演算法
我們很熟悉的BASE64演算法就是一個沒有祕密的對稱加密演算法。因為他的加密解密演算法都是公開的,所以加密資料是沒有任何祕密可言,典型的防菜鳥不防程式設計師的演算法。
BASE64演算法作用:
-
用於簡單的資料加密傳輸;
-
用於資料傳輸過程中的轉碼,解決中文問題和特殊符號在網路傳輸中的亂碼現象。
網路傳輸過程中如果雙方使用的編解碼字符集方式不一致,對於中文可能會出現亂碼;與此類似,網路上傳輸的字元並不全是可列印的字元,比如二進位制檔案、圖片等。Base64的出現就是為了解決此問題,它是基於64個可列印的字元來表示二進位制的資料的一種方法。
BASE64原理
BASE64的原理比較簡單,每當我們使用BASE64時都會先定義一個類似這樣的陣列:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
上面就是BASE64的索引表,字元選用了"A-Z、a-z、0-9、+、/" 64個可列印字元,這是標準的BASE64協議規定。在日常使用中我們還會看到“=”或“==”號出現在BASE64的編碼結果中,“=”在此是作為填充字元出現。
JDK提供了BASE64的實現:BASE64Encoder
,我們可以直接使用:
//使用base64加密
BASE64Encoder encoder = new BASE64Encoder();
String encrypt = encoder.encode(str.getBytes());
//使用base64解密
BASE64Decoder decoder = new BASE64Decoder();
String decrypt = new String(decoder.decodeBuffer(encryptStr));
2.2 DES
DES (Data Encryption Standard),在很長時間內,許多人心目中“密碼生成”與DES一直是個同義詞。
DES是一個分組加密演算法,典型的DES以64位為分組對資料加密,加密和解密用的是同一個演算法。它的金鑰長度是56位(因為每個第8 位都用作奇偶校驗),金鑰可以是任意的56位的數,而且可以任意時候改變。
DES加密過程大致如下:
- 首先需要從使用者處獲取一個64位長的密碼口令,然後通過等分、移位、選取和迭代形成一套16個加密金鑰,分別供每一輪運算中使用;
- 然後將64位的明文分組M進行操作,M經過一個初始置換IP,置換成m0。將m0明文分成左半部分和右半部分m0 = (L0,R0),各32位長。然後進行16輪完全相同的運算(迭代),這些運算被稱為函式f,在每一輪運算過程中資料與相應的金鑰結合;
- 在每一輪迭代中金鑰位移位,然後再從金鑰的56位中選出48位。通過一個擴充套件置換將資料的右半部分擴充套件成48位,並通過一個異或操作替代成新的48位資料,再將其壓縮置換成32位。這四步運算構成了函式f。然後,通過另一個異或運算,函式f的輸出與左半部分結合,其結果成為新的右半部分,原來的右半部分成為新的左半部分。將該操作重複16次;
- 經過16輪迭代後,左,右半部分合在一起經過一個末置換(資料整理),這樣就完成了加密過程。
對於DES解密的過程大家猛然一想應該是使用跟加密過程相反的演算法,事實上解密和加密使用的是一樣的演算法,有區別的地方在於加密和解密在使用密匙的時候次序是相反的。比如加密的時候是K0,K1,K2......K15,那麼解密使用密匙的次序就是倒過來的。之所以能用相同的演算法去解密,這跟DES特意設計的加密演算法有關,感興趣的同學可以深入分析。
2.3 AES
高階加密標準(AES,Advanced Encryption Standard),與DES一樣,使用AES加密函式和密匙來對明文進行加密,區別就是使用的加密函式不同。
上面說過DES的金鑰長度是56位元,因此演算法的理論安全強度是2^56。但以目前計算機硬體的製作水準和升級情況,破解DES可能只是山脈問題,最終NIST(美國國家標準技術研究所(National Institute of Standards and Technology))選擇了分組長度為128位的Rijndael演算法作為AES演算法。
AES為分組密碼,分組密碼也就是把明文分成一組一組的,每組長度相等,每次加密一組資料,直到加密完整個明文。在AES標準規範中,分組長度只能是128位,也就是說,每個分組為16個位元組(每個位元組8位)。金鑰的長度可以使用128位、192位或256位。金鑰的長度不同,推薦加密輪數也不同,如下表所示:
AES | 金鑰長度(32位位元字) | 分組長度(32位位元字) | 加密輪數 |
---|---|---|---|
AES-128 | 4 | 4 | 10 |
AES-192 | 6 | 4 | 12 |
AES-256 | 8 | 4 | 14 |
3. 非對稱加密
非對稱加密演算法的特點是,祕鑰一次會生成一對,其中一份祕鑰由自己儲存,不能公開出去,稱為“私鑰”,另外一份是可以公開出去的,稱為“公鑰”。
將原文用公鑰進行加密,得到的密文只有用對應私鑰才可以解密得到原文;
將原文用私鑰加密得到的密文,也只有用對應的公鑰才能解密得到原文;
因為加密和解密使用的是兩個不同
的金鑰,所以這種演算法叫作非對稱加密演算法
。
與對稱加密演算法的對比
- 優點:其安全性更好,對稱加密的通訊雙方使用相同的祕鑰,如果一方的祕鑰遭洩露,那麼整個通訊就會被破解。而非對稱加密使用一對祕鑰,一個用來加密,一個用來解密,而且公鑰是公開的,祕鑰是自己儲存的,不需要像對稱加密那樣在通訊之前要先同步祕鑰。
- 缺點:非對稱加密的缺點是加密和解密花費時間長、速度慢,只適合對少量資料進行加密。
在非對稱加密中使用的主要演算法有:RSA、Elgamal、ESA、揹包演算法、Rabin、D-H、ECC(橢圓曲線加密演算法)等。不同演算法的實現機制不同。
非對稱加密工作原理
下面我們就看一下非對稱加密的工作原理。
- 乙方生成一對金鑰(公鑰和私鑰)並將公鑰向其它方公開。
- 得到該公鑰的甲方使用該金鑰對機密資訊進行加密後再傳送給乙方。
- 乙方再用自己儲存的另一把專用金鑰(私鑰)對加密後的資訊進行解密。乙方只能用其專用金鑰(私鑰)解密由對應的公鑰加密後的資訊。
- 在傳輸過程中,即使攻擊者截獲了傳輸的密文,並得到了乙的公鑰,也無法破解密文,因為只有乙的私鑰才能解密密文。同樣,如果乙要回復加密資訊給甲,那麼需要甲先公佈甲的公鑰給乙用於加密,甲自己儲存甲的私鑰用於解密。
非對稱加密鼻祖:RSA
RSA演算法基於一個十分簡單的數論事實:將兩個大質數(素數)相乘十分容易,但是想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作為加密金鑰。比如:取兩個簡單的質數:67,73,得到兩者乘積很簡單4891;但是要想對4891進行因式分解,其工作量成幾何增加。
應用場景:
HTTPS請求的SSL層。
在JDK中也提供了RSA的實現,下面給出示例:
/**
* 建立密匙對
*
* @return
*/
private KeyPair genKeyPair() {
//建立 RSA Key 的生產者。
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
//利用使用者密碼作為隨機數初始化出 1024 位元 Key 的生產者。
//SecureRandom 是生成安全隨機數序列,password.getBytes() 是種子,只要種子相同,序列就一樣。
keyPairGen.initialize(1024, new SecureRandom("password".getBytes()));
//建立金鑰對
return keyPairGen.generateKeyPair();
}
/**
* 生成公匙
*
* @return
*/
public PublicKey genPublicKey() {
try {
//建立金鑰對
KeyPair keyPair = genKeyPair();
//生成公鑰
PublicKey publicKey = keyPair.getPublic();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 生成私匙
*
* @return
*/
public PrivateKey genPrivateKey() {
try {
//建立金鑰對
KeyPair keyPair = genKeyPair();
//生成私匙
PrivateKey privateKey = keyPair.getPrivate();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(privateKey.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 公鑰加密
*
* @param data
* @param publicKey
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey)
throws Exception {
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKey.getBytes());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 對資料加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 對資料分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > 117) {
cache = cipher.doFinal(data, offSet, 117);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * 117;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/**
* 私鑰解密
*
* @param encryptedData
* @param privateKey
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData,
String privateKey) throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKey.getBytes());
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 對資料分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > 118) {
cache = cipher.doFinal(encryptedData, offSet, 118);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * 118;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/**
* 私鑰加密
*
* @param data
* @param privateKey
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey)
throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(publicKey.getBytes());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 對資料分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > 117) {
cache = cipher.doFinal(data, offSet, 117);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * 117;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/**
* 公鑰解密
*
* @param encryptedData
* @param publicKey
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData,
String publicKey) throws Exception {
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKey.getBytes());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 對資料分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > 118) {
cache = cipher.doFinal(encryptedData, offSet, 118);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * 118;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
4. 組合加密
上面介紹的3種加密技術,每一種都有自己的特點,比如雜湊技術用於特徵值提取,對稱加密速度雖快但是有私匙洩露的危險,非對稱加密雖然安全但是速度卻慢。基於這些情況,現在的加密技術更加趨向於將這些加密的方案組合起來使用,基於此來研發新的加密演算法。
MAC(Message Authentication Code,訊息認證碼演算法)是含有金鑰雜湊函式演算法,相容了MD和SHA演算法的特性,並在此基礎上加上了金鑰。因此MAC演算法也經常被稱作HMAC演算法。MAC(Message Authentication Code,訊息認證碼演算法)是含有金鑰雜湊函式演算法,HMAC加密可以理解為加鹽的雜湊演算法,此處的“鹽”就相當於HMAC演算法的祕鑰。
HMAC演算法的實現過程需要一個加密用的雜湊函式(表示為H)和一個金鑰。
經過MAC演算法得到的摘要值也可以使用十六進位制編碼表示,其摘要值得長度與實現演算法的摘要值長度相同。例如 HmacSHA演算法得到的摘要長度就是SHA1演算法得到的摘要長度,都是160位二進位制數,換算成十六進位制的編碼為40位。
MAC演算法的實現:
演算法 | 摘要長度 | 備註 |
---|---|---|
HmacMD5 | 128 | JAVA6實現 |
HmacSHA1 | 160 | JAVA6實現 |
HmacSHA256 | 256 | JAVA6實現 |
HmacSHA384 | 384 | JAVA6實現 |
HmacSHA512 | 512 | JAVA6實現 |
HmacMD2 | 128 | BouncyCastle實現 |
HmacMD4 | 128 | BouncyCastle實現 |
HmacSHA224 | 224 | BouncyCastle實現 |
過程如下:
- 在金鑰key後面新增0來建立一個長為B(64位元組)的字串(str);
- 將上一步生成的字串(str) 與ipad(0x36)做異或運算,形成結果字串(istr);
- 將資料流data附加到第二步的結果字串(istr)的末尾;
- 做md5運算於第三步生成的資料流(istr);
- 將第一步生成的字串(str) 與opad(0x5c)做異或運算,形成結果字串(ostr),再將第四步的結果(istr) 附加到第五步的結果字串(ostr)的末尾做md5運算於第6步生成的資料流(ostr),最終輸出結果(out)
注意:如果第一步中,key的長度klen大於64位元組,則先進行md5運算,使其長度klen = 16位元組。
JDK中的實現:
public static void jdkHmacMD5() {
try {
// 初始化KeyGenerator
KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacMD5");
// 產生金鑰
SecretKey secretKey = keyGenerator.generateKey();
// 獲取金鑰
byte[] key = secretKey.getEncoded();
// byte[] key = Hex.decodeHex(new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e'});
// 還原金鑰
SecretKey restoreSecretKey = new SecretKeySpec(key, "HmacMD5");
// 例項化MAC
Mac mac = Mac.getInstance(restoreSecretKey.getAlgorithm());
// 初始化MAC
mac.init(restoreSecretKey);
// 執行摘要
byte[] hmacMD5Bytes = mac.doFinal("data".getBytes());
System.out.println("jdk hmacMD5:" + new String(hmacMD5Bytes));
} catch (Exception e) {
e.printStackTrace();
}
}