一、背景
- 加解密是程序猿無法繞過的必備技能,但不少人都對加解密存在誤解:比如經常會有人把MD5這種Hash算法也當成加密算法;
- 加解密算法衆多,但是我們實際應用的卻只有那麼2-3種,下面着重講下對稱加密算法和非對稱加密算法,以及對應的業務場景;
二、目標
- 藉助第三方基礎庫(BCP),完成常用的對稱加密算法AES256和非對稱加密算法RSA1024的開發及驗證;
- 分析關鍵代碼和業務場景,加深理解;
三、步驟
- Maven配置文件pom.xml中引入BCP依賴(完整源碼見woollay/springboot-shiro):
<!-- 加密組件包 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.62</version>
</dependency>
- 編寫AES256加解密代碼:
package com.justinsoft.encrypt;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
/**
* AES加解密算法
* @since: JDK 1.8
*/
public final class AESEncrypt
{
/**
* 加密
*
* @param data 待加密數據
* @param aesKey AES祕鑰
* @return
*/
public static byte[] encrypt(byte[] data, byte[] aesKey)
{
return doCipher(data, aesKey, Cipher.ENCRYPT_MODE);
}
/**
* 解密
*
* @param data 加密數據
* @param aesKey AES祕鑰
* @return
*/
public static byte[] decrypt(byte[] data, byte[] aesKey)
{
return doCipher(data, aesKey, Cipher.DECRYPT_MODE);
}
/**
* 生成祕鑰
*
* @param initKey 初始key值,不完全使用系統的隨機函數
* @return
* @throws Exception
*/
public static byte[] createKey(byte[] initKey) throws Exception
{
//1.添加BC算法支持
//Security.addProvider(new BouncyCastleProvider());
//2.使用BC算法生成祕鑰(添加用戶的初始祕鑰,不完全使用系統的隨機生成祕鑰的方法,可以避免系統漏洞)
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, BC_PROVIDER);
SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
secureRandom.setSeed(initKey);
//3.祕鑰的長度爲256(32字節)
keyGenerator.init(ALGORITHM_LEN, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
return secretKey.getEncoded();
}
static
{
//1.添加BC算法支持
Security.addProvider(new BouncyCastleProvider());
}
/**
* 轉換aesKey爲SecretKey對象
*
* @param aesKey
* @return
*/
private static SecretKey toKey(byte[] aesKey)
{
SecretKey key = new SecretKeySpec(aesKey, PADDING_ALGORITHM);
return key;
}
/**
* 加解密
*
* @param data 密文/明文
* @param aesKey AES祕鑰
* @param cipherMode 算法模式:加密{@link Cipher#ENCRYPT_MODE},加密{@link Cipher#DECRYPT_MODE}
* @return
*/
private static byte[] doCipher(byte[] data, byte[] aesKey, int cipherMode)
{
try
{
//1.獲取AES Key對象
Key key = toKey(aesKey);
//2.使用BC填充算法
Cipher cipher = Cipher.getInstance(PADDING_ALGORITHM, BC_PROVIDER);
//3.初始化,設置爲加密/解密模式
cipher.init(cipherMode, key);
//4.加密/解密
return cipher.doFinal(data);
}
catch (Exception e)
{
LOGGER.error("Failed to encrypt/decrypt data.", e);
throw new RuntimeException("Failed to encrypt/decrypt data.");
}
}
/**
* 私有化構造方法
*/
private AESEncrypt()
{
}
//日誌
private static final Logger LOGGER = LogManager.getLogger(AESEncrypt.class);
/**
* 密鑰算法
*/
private static final String ALGORITHM = "AES";
/**
* 加密算法的長度 AES256
*/
private static final int ALGORITHM_LEN = 256;
/**
* 使用BC作爲算法提供者
*/
private static final String BC_PROVIDER = BouncyCastleProvider.PROVIDER_NAME;
/**
* 加密/解密算法的填充方式
* <p>
* JAVA6支持PKCS5Padding填充方式
* BC支持PKCS7Padding填充方式
*/
private static final String PADDING_ALGORITHM = "AES/ECB/PKCS7Padding";
/**
* 隨機算法
*/
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
}
- AES256算法是對稱加密算法,對稱加密算法就是說加密和解密的密鑰對是相同的,即:用什麼祕鑰加密,就得用該祕鑰去解密。
特點:加解密效率比較高。
場景:需要頻繁加解密的業務場景。比如:數據庫登錄密碼的加密。
- AES256的驗證UT代碼:
package com.justinsoft.encrypt;
import org.apache.commons.codec.Charsets;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertTrue;
public class AESEncryptTest
{
@Test
public void createKey() throws Exception
{
String initKeyFactor = "com.justinsoft";
byte[] secretKey = AESEncrypt.createKey(initKeyFactor.getBytes(Charsets.UTF_8));
//1.生成祕鑰
System.out.println("secretKey=" + Arrays.toString(secretKey));
assertTrue(secretKey.length == 32);
}
@Test
public void encrypt()
{
// 驗證明文字符串長度較短的加密情況:
// 明文長度58byte,每16byte爲一個分組,密文應該是分成4組,每組16字節,所以密文一共64字節
String testText1 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
byte[] encryptText1 = AESEncrypt.encrypt(testByte1, EncryptKey.AES_KEY);
System.out.println("srcLen1=" + testByte1.length + ",encryptText1=" + Arrays.toString(encryptText1) + ",len="
+ encryptText1.length);
assertTrue(encryptText1.length == 64);
// 驗證明文字符串長度較長的加密情況:
// 明文長度65byte,每16byte爲一個分組,密文應該是分成5組,每組16字節,所以密文一共80字節
String testText2 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+!@#$%^h";
byte[] testByte2 = testText2.getBytes(Charsets.UTF_8);
byte[] encryptText2 = AESEncrypt.encrypt(testByte2, EncryptKey.AES_KEY);
System.out.println("srcLen2=" + testByte2.length + ",encryptText2=" + Arrays.toString(encryptText2) + ",len="
+ encryptText2.length);
assertTrue(encryptText2.length == 80);
}
@Test
public void decrypt()
{
// 驗證明文字符串長度較短的加密情況:
// 明文長度58byte,每16byte爲一個分組,密文應該是分成4組,每組16字節,所以密文一共64字節
String testText1 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
byte[] encryptText1 = AESEncrypt.encrypt(testByte1, EncryptKey.AES_KEY);
System.out.println("srcLen1=" + testByte1.length + ",encryptText1=" + Arrays.toString(encryptText1) + ",len="
+ encryptText1.length);
assertTrue(encryptText1.length == 64);
byte[] decryptByte1 = AESEncrypt.decrypt(encryptText1,EncryptKey.AES_KEY);
assertTrue(decryptByte1.length == 58);
String decryptText1 = new String(decryptByte1);
assertTrue(decryptText1.equals(testText1));
// 驗證明文字符串長度較長的加密情況:
// 明文長度65byte,每16byte爲一個分組,密文應該是分成5組,每組16字節,所以密文一共80字節
String testText2 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+!@#$%^h";
byte[] testByte2 = testText2.getBytes(Charsets.UTF_8);
byte[] encryptText2 = AESEncrypt.encrypt(testByte2, EncryptKey.AES_KEY);
System.out.println("srcLen2=" + testByte2.length + ",encryptText2=" + Arrays.toString(encryptText2) + ",len="
+ encryptText2.length);
assertTrue(encryptText2.length == 80);
byte[] decryptByte2 = AESEncrypt.decrypt(encryptText2,EncryptKey.AES_KEY);
assertTrue(decryptByte2.length == 65);
String decryptText2 = new String(decryptByte2);
assertTrue(decryptText2.equals(testText2));
}
}
- AES256異常處理。在運行過程中,有概率會出現如下異常:
java.security.InvalidKeyException: Illegal key size or default parameters
該問題的解決方案見網友總結的博客。簡單來說安裝版的jdk6/jdk7/jdk8中默認只支持AES128,如果要支持AES256,需要升級官方補丁。一般解壓版可以直接支持AES256。
- 編寫RSA1024加解密代碼:
package com.justinsoft.encrypt;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA非對稱加密算法
* <br>
* @since: JDK 1.8
*/
public final class RSAEncrypt
{
/**
* 加密,支持公鑰和私鑰
*
* @param data
* @param key
* @return
*/
public static byte[] encrypt(byte[] data, byte[] key)
{
try
{
Key curKey = getKey(key);
return doCipher(curKey, data, Cipher.ENCRYPT_MODE, MAX_ENCRYPT_BLOCK);
}
catch (Exception e)
{
LOGGER.error("Failed to encrypt data.", e);
throw new RuntimeException("Failed to encrypt data.");
}
}
/**
* 解密,支持公鑰和私鑰
*
* @param data
* @param key
* @return
*/
public static byte[] decrypt(byte[] data, byte[] key)
{
try
{
Key curKey = getKey(key);
return doCipher(curKey, data, Cipher.DECRYPT_MODE, MAX_DECRYPT_BLOCK);
}
catch (Exception e)
{
LOGGER.error("Failed to decrypt data.", e);
throw new RuntimeException("Failed to decrypt data.");
}
}
/**
* 獲取簽名
*
* @param data
* @param privateKey
* @return
*/
public static byte[] sign(byte[] data, byte[] privateKey)
{
try
{
PrivateKey key = getPrivateKey(privateKey);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(key);
signature.update(data);
return signature.sign();
}
catch (Exception e)
{
LOGGER.error("Failed to sign data.", e);
throw new RuntimeException("Failed to sign data.");
}
}
/**
* 校驗數字簽名
*
* @param data
* @param publicKey
* @param sign
* @return
*/
public static boolean verify(byte[] data, byte[] publicKey, byte[] sign)
{
try
{
PublicKey key = getPublicKey(publicKey);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(key);
signature.update(data);
return signature.verify(sign);
}
catch (Exception e)
{
LOGGER.error("Failed to verify signature data.", e);
throw new RuntimeException("Failed to verify signature data.");
}
}
/**
* 創建KeyPair
* <br>
* 目的是生成公鑰和私鑰
*
* @param initKey
* @return
* @throws Exception
*/
public static KeyPair createKey(byte[] initKey) throws Exception
{
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(ALGORITHM, BC_PROVIDER);
SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
secureRandom.setSeed(initKey);
keyGenerator.initialize(ALGORITHM_LEN, secureRandom);
return keyGenerator.generateKeyPair();
}
static
{
//1.添加BC算法支持
Security.addProvider(new BouncyCastleProvider());
}
/**
* 獲取key對象(公鑰)
*
* @param key
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] key) throws Exception
{
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
/**
* 獲取key對象(私鑰)
*
* @param key
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] key) throws Exception
{
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}
/**
* 加解密
* 1.私鑰加密,公鑰解密;
* 2.公鑰加密,私鑰解密;
*
* @param key 私鑰/公鑰
* @param data 密文/明文
* @param cipherMode 算法模式:加密{@link Cipher#ENCRYPT_MODE},加密{@link Cipher#DECRYPT_MODE}
* @param maxLen 最大長度
* @return
*/
private static byte[] doCipher(Key key, byte[] data, int cipherMode, int maxLen) throws Exception
{
//1.使用BC填充算法
Cipher cipher = Cipher.getInstance(PADDING_ALGORITHM, BC_PROVIDER);
//2.初始化,設置爲加密/解密模式
cipher.init(cipherMode, key);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int start = 0;
try
{
while (start < data.length)
{
//3. 判定一次加解密的最大長度,不能超過data的總長度
int limit = start + maxLen;
limit = Math.min(limit, data.length);
//4.分段加解密,並寫入字節流
byte[] cacheByte = cipher.doFinal(data, start, limit - start);
out.write(cacheByte, 0, cacheByte.length);
//5.把起始位置移至上一次的結束位置
start = limit;
}
//6.把字節流中的字節全部獲取出來
byte[] resultData = out.toByteArray();
return resultData;
}
finally
{
IOUtils.closeQuietly(out);
}
}
/**
* 獲取key
*
* @param keyByte
* @return
* @throws Exception
*/
private static Key getKey(byte[] keyByte) throws Exception
{
Key key;
if (keyByte.length == EncryptKey.RSA_PRI_KEY.length)
{
key = getPrivateKey(keyByte);
}
else
{
key = getPublicKey(keyByte);
}
return key;
}
/**
* 私有化構造方法
*/
private RSAEncrypt()
{
}
//日誌
private static final Logger LOGGER = LogManager.getLogger(RSAEncrypt.class);
/**
* 加密算法
*/
private static final String ALGORITHM = "RSA";
/**
* 簽名算法
*/
private static final String SIGNATURE_ALGORITHM = "SHA512withRSA";
/**
* 加密算法的長度 RSA1024
*/
private static final int ALGORITHM_LEN = 1024;
/**
* RSA 1024最大的明文長度(非固定值)
* 計算公式:
* 1.1024bit=128byte
* 2.128byte-11byte(PKCS1填充算法填充位)=117byte
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA 1024最大的密文長度(固定值)
* 計算公式:
* 1.1024bit=128byte
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* 使用BC作爲算法提供者
*/
private static final String BC_PROVIDER = BouncyCastleProvider.PROVIDER_NAME;
/**
* 加密/解密算法的填充算法
* <p>
*/
private static final String PADDING_ALGORITHM = "RSA/ECB/PKCS1Padding";
/**
* 隨機算法
*/
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
}
- RSA1024算法是非對稱加密算法,就是說加密和解密的密鑰對是不同的。
特點:相比對稱加密算法來說更安全,因爲加解密祕鑰完全分離,加密方無須告知解密方自己的祕鑰;解密方也無須告知加密方自己的祕鑰。加密的效率較低。
場景:簽名、證書等。如:https裏面就使用的是RSA加密算法生成的證書;github上的ssh公私鑰也是RSA加密算法生成的。
- RSA1024的驗證UT代碼:
package com.justinsoft.encrypt;
import org.apache.commons.codec.Charsets;
import org.junit.Test;
import java.security.KeyPair;
import java.util.Arrays;
import static org.junit.Assert.assertTrue;
public class RSAEncryptTest
{
@Test
public void createKey() throws Exception
{
String initKeyFactor = "com.justinsoft";
KeyPair keyPair = RSAEncrypt.createKey(initKeyFactor.getBytes(Charsets.UTF_8));
//1.生成公祕鑰
byte[] privateKey = keyPair.getPrivate().getEncoded();
byte[] publicKey = keyPair.getPublic().getEncoded();
System.out.println("privateKey=" + Arrays.toString(privateKey) + ",len=" + privateKey.length);
System.out.println("publicKey=" + Arrays.toString(publicKey) + ",len=" + publicKey.length);
assertTrue(privateKey.length > 12 && privateKey.length <= 1024);
assertTrue(publicKey.length > 12 && publicKey.length < privateKey.length);
}
/**
* 私鑰加密,公鑰解密
*/
@Test
public void decrypt()
{
String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
System.out.println("encryptByte1=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PRI_KEY);
System.out.println("encryptText1=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PUB_KEY);
System.out.println("decryptText1=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
//1、私鑰加密公鑰解密,驗證117字節的單次RSA加密
assertTrue(testText1.equals(new String(decryptByte1)));
}
/**
* 公鑰加密,私鑰解密
*/
@Test
public void decrypt2()
{
String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
System.out.println("encryptByte2=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PUB_KEY);
System.out.println("encryptText2=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PRI_KEY);
System.out.println("decryptText2=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
//1、公鑰加密,私鑰解密,驗證117字節明文的單次RSA加密
assertTrue(testText1.equals(new String(decryptByte1)));
}
@Test
public void decrypt3()
{
String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
System.out.println("encryptByte3=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PUB_KEY);
System.out.println("encryptText3=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
assertTrue(encryptByte1.length==256);
byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PRI_KEY);
System.out.println("decryptText3=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
//1、公鑰加密,私鑰解密,驗證120字節的多次循環RSA加密
assertTrue(testText1.equals(new String(decryptByte1)));
}
@Test
public void verify()
{
String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩";
byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
System.out.println("encryptByte4=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
byte[] signByte = RSAEncrypt.sign(testByte1,EncryptKey.RSA_PRI_KEY);
boolean isVerify = RSAEncrypt.verify(testByte1,EncryptKey.RSA_PUB_KEY,signByte);
assertTrue(isVerify);
}
}
- RSA1024算法需要注意的是一次只能加解密117個字節(原因見代碼中的註釋),所以,如果報文過長,需要進行分段加解密,核心代碼見RSA1024代碼。
四、總結
- AES256和RSA1024的使用方式差不多,都是要傳入要加密的內容和祕鑰,只不過AES256加解密的祕鑰是同一個,而RSA1024是不同的2個值,觀察完整的源碼就會發現公鑰和私鑰的長度都完全不同;RSA1024算法還可用於驗證簽名;需要特別強調的是:RSA1024每次加密同一份內容的密文也不相同;
- AES256用於數據庫登錄密碼或者部分C/S結構的客戶端密碼的加解密,效率也高;RSA1024主要用於https,github的ssh連接的加解密,加解密效率偏低;
五、參考
[1]開發中的幾種加密算法的使用場景
[2]AES的256位密鑰加解密報 java.security.InvalidKeyException: Illegal key size or default parameters 異常的處理及處理工具