程序員必須知道的加密、解密和簽名算法
- 對稱加密 =======
對稱加密,加密和解密使用相同的祕鑰,加密速度快、效率高。常見的有 DES(淘汰)、3DES(淘汰)、AES(用於替代 DES,是目前常用的)等。
1.1. DES(Data Encryption Standard)
DES 現在認爲是一種不安全的加密算法,已經有用窮舉法攻破 DES 密碼的報道了。3DES 是 DES 的加強版本(也被淘汰),是 DES 向 AES 過渡的加密算法。
1.2. AES(Advanced Encryption Standard)
AES 把明文按每組 16 個字節分成一組一組的、長度相等的數據,每次加密一組,直到加密完整個明文。在 AES 標準中,分組長度只能是 128 位,但是密鑰的長度可以使用 128 位、192 位或 256 位。
下面先來了解 “分組加密機制、填充模式、初始向量、加密模式” 等基本概念,最後給出 Java 代碼示例。
1.2.1. 分組密碼體制
分組密碼體制就是指將明文切成一段一段的來加密,而且每段數據的長度要求必須是 128 位 16 個字節,如果最後一段不夠 16 個字節了,就需要用 Padding 來把這段數據填滿 16 個字節,然後再把一段一段的密文拼起來形成最終密文的加密方式。
1.2.2. 填充模式 Padding
Padding 就是用來把不滿 16 個字節的分組數據填滿 16 個字節用的,它有三種模式 PKCS5、PKCS7 和 NOPADDING。
-
PKCS5 是指分組數據缺少幾個字節,就在數據的末尾填充幾個字節的幾,比如缺少 5 個字節,就在末尾填充 5 個字節的 5。
-
PKCS7 是指分組數據缺少幾個字節,就在數據的末尾填充幾個字節的 0,比如缺少 7 個字節,就在末尾填充 7 個字節的 0。
-
NoPadding 是指不需要填充,也就是說數據的發送方肯定會保證最後一段數據也正好是 16 個字節。
在 PKCS5 模式下,有這樣一種特殊情況,假設最後一段數據的內容剛好就是 16 個 16,這時解密端怎麼區分是填充還是數據呢?
對於這種情況,PKCS5 模式會自動幫我們在最後一段數據後再添加 16 個字節的數據,而且填充數據也是 16 個 16,這樣解密端就能知道誰是有效數據誰是填充數據了。同樣的道理,PKCS7 最後一段數據的內容是 16 個 0。
解密端需要使用和加密端同樣的 Padding 模式,才能準確的識別有效數據和填充數據。開發通常採用 PKCS7 Padding 模式。
1.2.3. 初始向量 IV
初始向量 IV 的作用是使加密更加安全可靠。使用 AES 加密時要主動提供初始向量,而且只需提供一個初始向量就夠了,後面每段數據的加密向量都是前面一段的密文。初始向量 IV 的長度規定爲 128 位 16 個字節,初始向量通常採用隨機生成。
1.2.4. 密鑰
AES 要求密鑰的長度可以是 128 位、192 位或者 256 位,位數越高,加密強度越大,但是加密的效率自然會低一些,因此要做好衡量。
1.2.5. 分組加密模式
分組加密算法只能對固定長度的分組進行加密,面對超過分組長度的明文,就需要對分組密碼算法進行迭代,以便將很長的明文全部加密。而迭代的方法就稱爲 “分組加密的模式”。分組密碼的模式有很多,常見的有:
-
ECB(電子密碼本模式 Electronic Codebook Book),相對的不安全,很少使用
-
CBC(密碼分組鏈接模式 Cipher Block Chaining),不支持並行計算,比 ECB 模式多了一個初始向量 IV,是這些模式中最安全的,也是最常用的模式
-
CFB(密碼反饋模式 Cipher FeedBack),可被施以” 重放攻擊 “
-
OFB(輸出反饋模式 Output FeedBack),可被主動攻擊者反轉密文而引起解密後明文中的相應比特也發生變化
-
CTR(計數器模式 Counter mode),與 OFB 一樣可被主動攻擊者反轉密文,但比 OFB 多了支持併發計算的特性
AES 和 RSA 都屬於分組加密算法。
1.2.6. AES Java 示例
AES-128-CBC 加解密
public static String cbcEncrypt(String plain, String key, String ivSeed) {
Assert.notNull(plain, "plain must not be null");
Assert.notNull(key, "key must not be null");
Assert.notNull(ivSeed, "ivSeed must not be null");
Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");
String base64 = null;
try {
// 生成祕鑰
SecretKeySpec keySpec = createKey(key);
// 設置算法/模式/填充方式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 設置偏移
IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
// 加密模式
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 加密
byte[] encrypted = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
// 轉 base64
base64 = Base64.getEncoder().encodeToString(encrypted);
} catch (Exception ex) {
log.error("exception: {}", ex.getMessage());
}
return base64;
}
public static String cbcDecrypt(String base64, String key, String ivSeed) {
Assert.notNull(base64, "base64 must not be null");
Assert.notNull(key, "key must not be null");
Assert.notNull(ivSeed, "ivSeed must not be null");
Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");
String plain = null;
try {
// base64 解碼
byte[] decodedBase64 = Base64.getDecoder().decode(base64);
// 生成祕鑰
SecretKeySpec keySpec = createKey(key, isBase64Key);
if(null != keySpec) {
// 設置偏移
IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
// 設置算法/模式/填充方式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 解密模式
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
// 解密
byte[] decrypted = cipher.doFinal(data);
// 轉 base64
plain = new String(decrypted, StandardCharsets.UTF_8);
}
} catch (Exception ex) {
log.error("exception: {}", ex.getMessage());
}
return plain;
}
private static SecretKeySpec createKey(String key) {
Assert.notNull(key, "key must not be null");
byte[] bytesKey = key.getBytes(StandardCharsets.UTF_8);
Assert.isTrue(bytesKey.length == 16, "key must be 16 bytes");
// 生成祕鑰
return new SecretKeySpec(bytesKey, "AES");
}
- 非對稱加密 ========
非對稱加密算法,需要兩個密鑰, 一個是公鑰 (public key),公開,任何人都可以獲取;另一個是 私鑰 (private key),不公開,由個人保存在安全的地方。公鑰用於加密,私鑰用於解密。
RSA (三位數學家名字的縮寫)算法是第一個能同時用於 加密 和 數字簽名 的非對稱加密算法,它能夠 抵抗 到目前爲止已知的 所有密碼攻擊,已被 ISO 推薦爲公鑰數據加密標準。
2.1. 使用場景
假設 A 和 B 之間要進行加密通信,那麼:
(1)B 向 A 發送加密數據
-
A 生成一對密鑰,私鑰由 A 自己保留不公開;而公鑰傳給 B,公開,任何人可以獲取
-
B 用該公鑰對消息進行加密,併發送給 A
-
A 接收到加密消息後,用私鑰對消息進行解密
在這個過程中,只有 2 次傳遞過程,第一次是 A 傳遞公鑰給 B,第二次是 B 傳遞加密消息給 A,即使都被截獲,也沒有危險性,因爲只有 A 的私鑰才能對消息進行解密,防止了消息內容的泄露。
(2)A 向 B 發送 “已收到” 回覆
-
A 用私鑰對消息加簽形成簽名,並將加簽的消息和消息本身一起傳遞給 B
-
B 收到消息後,用公鑰進行驗籤,如果驗簽出來的內容與消息本身一致,證明消息是 A 回覆的。
在這個過程中,算上前面的傳遞公鑰,也只有 2 次傳遞過程,一次是傳遞公鑰,第二次就是 A 傳遞加簽的消息和消息本身給 B,即使都被敵方截獲,也同樣沒有危險性,因爲只有 A 的私鑰才能對消息進行簽名,即使知道了消息內容,也無法僞造帶簽名的回覆給 B,防止了消息內容的篡改。
但是,綜合上面兩個場景會發現:
-
第一個場景雖然被截獲的消息沒有泄露,但是可以利用截獲的公鑰,將假指令進行加密,然後傳遞給 A。
-
第二個場景雖然截獲的消息不能被篡改,但是消息的內容可以利用公鑰驗籤來獲得,並不能防止泄露。
所以在實際應用中,要根據情況使用,可以雙方同時使用加密和簽名,比如 A 和 B 都有一套自己的公鑰和私鑰,當 A 要給 B 發送消息時,先用 B 的公鑰對消息加密,再對加密的消息使用 A 的私鑰加簽名,達到既不泄露也不被篡改,更能保證消息的安全性。
2.2. RAS 加密算法
2.2.1. 填充模式 Padding
Padding 常見模式如下表:
Padding 模式
RSA 常用的加密填充模式
-
RSA/None/PKCS1Padding(Java 默認的 RSA 實現)
-
RSA/ECB/PKCS1Padding
2.2.2. RSA Java 示例
/**
* 公鑰加密
*/
public static String ecbEncrypt(String data, String publicKeyBase64) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, toPublicKey(publicKeyBase64));
byte[] bytes cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(bytes);
}
/**
* 私鑰解密
*/
public static String ecbDecrypt(String base64, String privateKeyBase64) throws IllegalBlockSizeException, InvalidKeyException,
InvalidKeySpecException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
byte[] bytes = Base64.getDecoder().decode(base64.getBytes());
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, toPrivateKey(privateKeyBase64));
return new String(cipher.doFinal(data));
}
/**
* 生成隨機密鑰對
*/
public static HashMap<String, String> randomKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair pair = generator.generateKeyPair();
if (null == pair) {
return null;
}
PrivateKey pvt = pair.getPrivate();
PublicKey pub = pair.getPublic();
Base64.Encoder encoder = Base64.getEncoder();
String pvtVal = encoder.encodeToString(pvt.getEncoded());
String pubVal = encoder.encodeToString(pub.getEncoded());
HashMap<String, String> rsaKeyMap = new HashMap<>(2);
rsaKeyMap.put("privateKeyBase64", pvtVal);
rsaKeyMap.put("publicKeyBase64", pubVal);
return rsaKeyMap;
}
/**
* privateKeyBase64 私鑰轉爲 PrivateKey 對象
*/
private static PrivateKey toPrivateKey(String privateKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(privateKeyBase64.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
/**
* publicKeyBase64 公鑰轉爲 PublicKey 對象
*/
private static PublicKey toPublicKey(String publicKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(publicKeyBase64.getBytes());
X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(ks);
}
- 簽名 =====
加密是爲了防止信息被泄露,而簽名是爲了防止信息被篡改和僞造。
簽名 & 驗籤
3.1. 摘要算法
哈希函數(Hash function),又稱散列函數、散列算法,也叫摘要算法,它是一種不可逆的信息摘要算法。
好的散列算法具備如下特性:
-
單向性(one-way)即不可逆
-
抗衝突性(collision-resistant)即產生兩個相同散列值的概率很低(但輸入相同,則輸出的結果一定相同)
-
雪崩效應(avalanche effect)即原始數據的微小改動,會導致散列值的巨大差異
常見的用途:
-
密碼保護:把用戶密碼通過散列函數加密保存(保存散列值),只有用戶自己知道密碼的明文
-
簽名 & 驗籤:比如對接口調用、對消息進行簽名,接收方進行驗籤
-
數據完整性 / 一致性校驗:比如網上提供的文件下載通常都提供散列值和算法,便於用戶校驗
-
數據秒傳:上傳幾個 G 的大文件只用幾秒,就是通過對比文件的散列值實現的,散列值(信息的指紋)相同就認爲是同一個文件
常見的散列算法有” 報文摘要算法 MD“、” 安全散列算法 SHA“,以及” 消息認證碼算法 MAC“。
摘要算法
3.1.1. 報文摘要算法(MD 系列)
信息摘要算法(Message-Digest Algorithm)。最常用的是 MD5 (Message-Digest Algorithm 5),是一種被廣泛使用的密碼散列函數,可以產生出一個 128 位(16 字節)的散列值(hash value),常用於確保信息傳輸完整一致。
3.1.2. 安全散列算法(SHA 系列)
安全散列算法(Secure Hash Algorithm)是一種不可逆的信息安全算法,經過量化運算和轉換,可以把任意長度的數據生成不可逆的、固定長度的字符串,這個固定長度的字符串就是對相應的原始輸入字符串的散列(也稱爲摘要),可以作爲信息的指紋。
SHA-224,SHA-256,SHA-384,SHA-512 統稱爲 SHA-2,而 SHA-1 算法已經不夠安全,不建議繼續使用。
3.1.3. 消息認證碼(MAC 系列)
消息認證碼算法(Message Authentication Code)是含有加密密鑰的散列算法,它在 MD 和 SHA 算法特性的基礎上加入了加密密鑰,通過特別的計算方式來構造消息認證碼 (MAC) 的方法。因 MAC 算法融合了密鑰散列函數,通常也稱爲 HMAC 算法(Hash-based Message Authentication Code,散列消息認證碼)。
常見的有:HMAC-SHA224、HMAC-SHA256、HMAC-SHA384、HMAC-SHA512
3.2. 簽名驗籤原理
3.2.1. 簽名
對需要發送的報文 originData 計算摘要(相關摘要算法有 md5、sha256 等)特徵值 signBlock。使用私鑰 privateKey 對 signBlock 加密獲得數字簽名 signatureData。將 signatureData 與 originData 打包發一起送給對方。
3.2.2. 驗籤
接收方接收到數據後,把消息拆分爲 signatureData 與 originData 。對 originData 計算特徵值 signBlock,使用的算法必須要和發送方一致。使用公鑰 publicKey 對 signatureData 解密,獲得 signBlock1。比較 signBlock 和 signBlock1,若匹配則驗證成功,報文未被篡改。
3.3. RSA 簽名示例
/**
* 用私鑰對數據進行簽名並返回簽名後的base64
* @param data 代簽名的字符串
* @param base64PrivateKey 私鑰
*/
public static String sign(String data, String base64PrivateKey)
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
PrivateKey key = toPrivateKey(base64PrivateKey);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(key);
signature.update(data.getBytes());
return new String(Base64.getEncoder().encode(signature.sign()));
}
/**
* 驗籤
* @param data 原始數據
* @param base64PublicKey 公鑰
* @param sign 私鑰簽名後的數據
*/
public static boolean verify(String data, String base64PublicKey, String sign)
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
PublicKey key = toPublicKey(base64PublicKey);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(key);
signature.update(data.getBytes());
return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
}
/**
* base64PrivateKey 私鑰轉爲 PrivateKey 對象
*/
private static PrivateKey toPrivateKey(String base64PrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(base64PrivateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
/**
* base64PublicKey 公鑰轉爲 PublicKey 對象
*/
private static PublicKey toPublicKey(String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(base64PublicKey.getBytes());
X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(ks);
}
小結
本文主要介紹了常用的對稱加密算法、非對稱加密算法;常用的摘要算法、簽名算法。以及使用算法需要了解的基本概念(比如填充模式、IV 等),算法的使用場景,並且分別給出了 Java 示例代碼。希望對各位小夥伴們有幫助哦
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/g2lCS-SdV8W3GVq349oOag