Java中的AES加密解密
AES介绍
什么是AES
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
该算法为比利时密码学家Joan Daemen和Vincent Rijmen所设计,结合两位作者的名字,以Rijndael为名投稿高级加密标准的甄选流程。(Rijndael的发音近于"Rhine doll")
AES一般有五种工作体制:1.电码本模式(Electronic Codebook Book (ECB));2.密码分组链接模式(Cipher Block Chaining (CBC));3.计算器模式(Counter (CTR));4.密码反馈模式(Cipher FeedBack (CFB));5.输出反馈模式(Output FeedBack (OFB))。
AES加密位数
AES通常有128、192、256位(bit位)加密,主要区别在于秘钥的长度,分别对应16bytes、24bytes、32bytes。
在线加密和解密工具:https://encode-decode.com/encryption-functions/
Java中的AES
Java中一般把密码通过KeyGenerator以及SecureRandom(一般用hash、SHA256等)把密码长度按照指定的加密位数据改造成对应的字节长度,并不是直接用给定的密码加密或者解密,这样改造后的密码是以初始的密码为种子计算而来,解密的时候也需要先进行相应处理。
这样做的好处就是密码长度没有什么限制,实际密码是根据给定的keySize位数计算,直接得到对应的位数的秘钥。
注意:由于Java使用了hash改造密码,因此要和其他平台兼容的时候不能使用KeyGenerator这个操作,只能直接使用对应长度的秘钥
JDK默认并不支持AES 256,另外还有些加密模式也不支持(如:AES/ECB/PKCS7Padding),建议引入bouncycastle加密库(有些项目可能已经通过第三方库间接引入了)
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
</dependency>
Java的AES加密解密工具
AES的ECB和CBC模式都有,参考代码如下:
public class SimpleAESUtils {
private static final Logger logger = LoggerFactory.getLogger(SimpleAESUtils.class);
private static final String AES = "AES";
private static final String AES_MODE = "AES/ECB/PKCS7Padding"; // AES电子密码本
private static final String AES_MODE_CBC = "AES/CBC/PKCS7Padding"; // CBC模式
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 加密
*
* @param content 内容
* @param password 加密密码,长度对应位数,16-128、24-192、32-256,长度位数不对将加密失败
* @return
*/
public static byte[] encrypt(String content, byte[] password) {
try {
SecretKeySpec key = new SecretKeySpec(password, AES);
Cipher cipher = Cipher.getInstance(AES_MODE);// 创建密码器
byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
return cipher.doFinal(byteContent); // 加密
} catch (Exception e) {
logger.error("AES加密失败", e);
}
return new byte[0];
}
/**
* 解密
*
* @param encryptContent 已加密内容根据实际的内容格式转换成对应的byte数组
* @param password 加密密码,长度对应位数,16-128、24-192、32-256,长度位数不对将解密失败
* @return
*/
public static byte[] decrypt(byte[] encryptContent, byte[] password) {
try {
SecretKeySpec key = new SecretKeySpec(password, AES);
Cipher cipher = Cipher.getInstance(AES_MODE);// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
return cipher.doFinal(encryptContent); // 加密
} catch (Exception e) {
logger.error("AES解密失败", e);
}
return new byte[0];
}
/**
* 加密
*
* @param content 内容
* @param password 密码
* @param keySize 位数
* @return
*/
public static byte[] encrypt(String content, String password, int keySize) {
try {
byte[] encodedPassword = generatePassword(password, keySize);
return encrypt(content, encodedPassword);
} catch (Exception e) {
logger.error("AES加密失败", e);
}
return new byte[0];
}
/**
* 解密
*
* @param encryptContent 待解密内容
* @param password 密码
* @param keySize 秘钥位数
* @return
*/
public static byte[] decrypt(byte[] encryptContent, String password, int keySize) {
try {
byte[] encodedPassword = generatePassword(password, keySize);
return decrypt(encryptContent, encodedPassword);
} catch (Exception e) {
logger.error("AES解密失败", e);
}
return new byte[0];
}
/**
* 密码长度通过hash改造成对应的字节长度
*
* @param password
* @return
* @throws NoSuchAlgorithmException
*/
public static byte[] generatePassword(String password, int keySize) throws NoSuchAlgorithmException {
KeyGenerator kgen = KeyGenerator.getInstance("AES"); // 秘钥生成器
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); // hash算法
secureRandom.setSeed(password.getBytes());
kgen.init(keySize, secureRandom);
SecretKey secretKey = kgen.generateKey();
return secretKey.getEncoded();
}
/**
* 加密
*
* @param content 内容
* @param password 加密密码,长度对应位数,16-128、24-192、32-256,长度位数不对将加密失败
* @param iv 向量值
* @return
*/
public static byte[] encryptCBC(String content, byte[] password, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(password, AES);
Cipher cipher = Cipher.getInstance(AES_MODE_CBC);// 创建密码器
byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));// 初始化
return cipher.doFinal(byteContent); // 加密
} catch (Exception e) {
logger.error("AES加密失败", e);
}
return new byte[0];
}
/**
* 解密
*
* @param encryptContent 已加密内容根据实际的内容格式转换成对应的byte数组
* @param password 加密密码,长度对应位数,16-128、24-192、32-256,长度位数不对将解密失败
* @param iv iv 向量值
* @return
*/
public static byte[] decryptCBC(byte[] encryptContent, byte[] password, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(password, AES);
Cipher cipher = Cipher.getInstance(AES_MODE_CBC);// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));// 初始化
return cipher.doFinal(encryptContent); // 加密
} catch (Exception e) {
logger.error("AES解密失败", e);
}
return new byte[0];
}
}
ECB模式测试
public static void main(String... args) throws Exception {
String content = "123";
String password = "123456";
int keySize = 256;
// 指定keySize来加密解密,密码位数没有什么限制
byte[] encrypt = encrypt(content, password, keySize);
byte[] decrypt = decrypt(encrypt, password, keySize);
System.out.println(new String(decrypt, StandardCharsets.UTF_8).equals(content));
// 这里使用的是Base64格式的密码,decode之后数组的长度对应响应的加密位数
String password256Base64 = "a7SDfrdDKRBe5FaN2n3GftLKKtkfZS5GsdEjfdjsyBE=";
byte[] encrypt1 = encrypt(content, Base64.getDecoder().decode(password256Base64));
byte[] decrypt1 = decrypt(encrypt1, Base64.getDecoder().decode(password256Base64));
System.out.println(new String(decrypt1, StandardCharsets.UTF_8).equals(content));
}
CBC模式测试
CBC模式除了密码之外还需要一个向量IV值,IV的长度是16字节:
public static void main(String... args) throws Exception {
String cbcContent = "123";
String cbcPassword = "a7SDfrdDKRBe5FaN2n3GftLKKtkfZS5GsdEjfdjsyBE=";
String cbcIv = "1234567812345678";
byte[] encrypt2 = encryptCBC(cbcContent, Base64.getDecoder().decode(cbcPassword), cbcIv.getBytes());
byte[] decrypt2 = decryptCBC(encrypt2, Base64.getDecoder().decode(cbcPassword), cbcIv.getBytes());
System.out.println(new String(decrypt2, StandardCharsets.UTF_8).equals(cbcContent));
}
常见错误
AES要求加密秘钥的位数是128/192/256(对应bytes长度:16/24/32),如果秘钥长度不是这几个值就会报错。
org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher$InvalidKeyOrParametersException: Key length not 128/192/256 bits.
at org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineInit(Unknown Source)
at org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineInit(Unknown Source)
at java.base/javax.crypto.Cipher.implInit(Cipher.java:839)
at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:901)
at java.base/javax.crypto.Cipher.init(Cipher.java:1286)
at java.base/javax.crypto.Cipher.init(Cipher.java:1223)
at com.citsgbt.mobile.gateway.config.security.SimpleAESUtils.encrypt(SimpleAESUtils.java:93)
at com.citsgbt.mobile.gateway.config.security.SimpleAESUtils.main(SimpleAESUtils.java:182)
Caused by: java.lang.IllegalArgumentException: Key length not 128/192/256 bits.
at org.bouncycastle.crypto.engines.AESEngine.generateWorkingKey(Unknown Source)
at org.bouncycastle.crypto.engines.AESEngine.init(Unknown Source)
at org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher.init(Unknown Source)
at org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher$BufferedGenericBlockCipher.init(Unknown Source)
... 8 common frames omitted
JDK中如果没有相关的AES算法模式,会有如下错误:
java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/ECB/PKCS7Padding
at java.base/javax.crypto.Cipher.getInstance(Cipher.java:565)
at com.citsgbt.mobile.gateway.config.security.SimpleAESUtils.encrypt(SimpleAESUtils.java:91)
at com.citsgbt.mobile.gateway.config.security.SimpleAESUtils.encrypt(SimpleAESUtils.java:131)
at com.citsgbt.mobile.gateway.config.security.SimpleAESUtils.main(SimpleAESUtils.java:177)
需要引入第三方的加密库,并在代码中添加进去:
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}