- 加解密是程序猿无法绕过的必备技能,但不少人都对加解密存在误解:比如经常会有人把MD5这种Hash算法也当成加密算法;
- 加解密算法众多,但是我们实际应用的却只有那么2-3种,下面着重讲下对称加密算法和非对称加密算法,以及对应的业务场景;
<dependency>
<groupId>org.bouncycastlegroupId>
<artifactId>bcprov-jdk15onartifactId>
<version>1.62version>
dependency>
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; /** * 加密/解密算法的填充方式 *
* JAVA6支持PKCS5Padding填充方式 * BC支持PKCS7Padding填充方式 */ private static final String PADDING_ALGORITHM = "AES/ECB/PKCS7Padding"; /** * 随机算法 */ private static final String RANDOM_ALGORITHM = "SHA1PRNG"; }
特点:加解密效率比较高。
场景:需要频繁加解密的业务场景。比如:数据库登录密码的加密。
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));
}
}
java.security.InvalidKeyException: Illegal key size or default parameters
该问题的解决方案见网友总结的博客。简单来说安装版的jdk6/jdk7/jdk8中默认只支持AES128,如果要支持AES256,需要升级官方补丁。一般解压版可以直接支持AES256。
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非对称加密算法 *
* @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 *
* 目的是生成公钥和私钥 * * @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; /** * 加密/解密算法的填充算法 *
*/ private static final String PADDING_ALGORITHM = "RSA/ECB/PKCS1Padding"; /** * 随机算法 */ private static final String RANDOM_ALGORITHM = "SHA1PRNG"; }
特点:相比对称加密算法来说更安全,因为加解密秘钥完全分离,加密方无须告知解密方自己的秘钥;解密方也无须告知加密方自己的秘钥。加密的效率较低。
场景:签名、证书等。如:https里面就使用的是RSA加密算法生成的证书;github上的ssh公私钥也是RSA加密算法生成的。
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);
}
}
[1]开发中的几种加密算法的使用场景
[2]AES的256位密钥加解密报 java.security.InvalidKeyException: Illegal key size or default parameters 异常的处理及处理工具