Java-加密技术-对称加密算法
主要参考:对称加密-AES
一、简介
1. 对称加密概念
对称加密就是指,加密和解密使用同一个密钥的加密方式,发送方使用密钥将明文数据加密成密文,然后发送出去,接收方收到密文后,使用同一个密钥将密文解密成明文读取。
基于“对称密钥”的加密算法主要有DES、3DES(TripleDES)、AES、RC2、RC4、RC5和Blowfish等。最常用的对称加密算法DES、3DES(TripleDES)和AES,AES安全性更高,速度更快,所以其应用最为广泛。
2. 优缺点
优点:加密计算量小、速度块,适合对大量数据进行加密的场景,相对非对称加密来说。
缺点:
- 密钥传输问题:密钥如何安全的网络传输到请求方是个核心关键问题,可以了解https。
- 密钥管理:每个用户需要对应一个密钥,所以会增加管理,相对无密钥的加密算法。
3. 关键概念
要理解AES的加密流程,会涉及到AES加密的五个关键词,分别是:分组密码体制、Padding、密钥、初始向量IV和四种加密模式,注意理解时,搞清楚字节、位、编码字符集的关系。
1. 分组密码体制
所谓分组密码体制就是指将明文切成一段一段的来加密,然后再把一段一段的密文拼起来形成最终密文的加密方式。AES采用分组密码体制,即AES加密会首先把明文切成一段一段的,而且每段数据的长度要求必须是128位16个字节,如果最后一段不够16个字节了,就需要用Padding来把这段数据填满16个字节,然后分别对每段数据进行加密,最后再把每段加密数据拼起来形成最终的密文。
2. Padding填充模式
Padding就是用来把不满16个字节的分组数据填满16个字节用的,它有三种模式PKCS5
、PKCS7
和NOPADDING
。
- PKCS5是指分组数据缺少几个字节,就在数据的末尾填充几个字节的5。
- PKCS7是指分组数据缺少几个字节,就在数据的末尾填充几个字节的0。
- NoPadding是指不需要填充,也就是说数据的发送方肯定会保证最后一段数据也正好是16个字节。
原生jdk不支持PKCS7Padding
填充方式,通过BouncyCastle组件来让java里面支持PKCS7Padding填充,具体添加组件方式见代码。
3. 密钥
AES要求密钥的长度可以是128位16个字节
、192位
或者256位
,位数越高,加密强度自然越大,但是加密的效率自然会低一些,因此要做好衡量。我们开发通常采用128位16个字节的密钥,我们使用AES加密时需要主动提供密钥,而且只需要提供一个密钥就够了,每段数据加密使用的都是这一个密钥,密钥来源为随机生成,只要保障密钥符合位数规则,以及解密和加密的密钥是同一个就行了。
Java本身限制密钥的长度是128位(16字节),有些需要的密钥长度是256位,如果超过位数会报:java.security.InvalidKeyException: Illegal key size
。因此需要到Java官网上下载一个Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。下载后解压,可以看到local_policy.jar
和US_export_policy.jar
以及readme.txt
。如果安装了JRE,将两个jar文件放到 %JRE_HOME%\lib\security
下覆盖原来文件,记得先备份。如果安装了JDK,将两个jar文件也放到 %JDK_HOME%\jre\lib\security
下。
官方网站提供了JCE无限制权限策略文件的下载: JDK6的下载地址、 JDK7的下载地址、 JDK8的下载地址
4. 初始向量
初始向量IV:初始向量IV的作用是使加密更加安全可靠,我们使用AES加密时需要主动提供初始向量,而且只需要提供一个初始向量就够了,后面每段数据的加密向量都是前面一段的密文。初始向量IV的长度规定为128位16个字节
,初始向量的来源为随机生成。
5. 四种加密模式
AES一共有四种加密模式,分别是ECB(电子密码本模式)、CBC(密码分组链接模式)、CFB、OFB,我们一般使用的是CBC模式。四种模式中除了ECB相对不安全之外,其它三种模式的区别并没有那么大。
6. 加密简易原理
ECB模式
CBC模式
这里可以理解为什么初始化向量长度和明文分组长度一致了。
详细过程可以参考:对称加密-AES
4. 注意事项
- 进行加密和解密的前提是:加密方和解密方的密钥、初始化向量、填充模式要一致。
二、代码
主要有有向量、无向量。
1. 无向量,AES/ECB/PKCS7Padding模式
参考:
无向量,AES/ECB/PKCS7Padding模式。
如果想改为 PKCS5Padding 填充模式也是可以的,这里只需要修改工具类的静态常量Cipher_Mode的取值即可
package com.debug.steadyjack.springbootMQ.server.util;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
/**
* AES加密算法util
* Created by steadyjack on 2018/4/21.
*/
public class AESUtil {
private static final String EncryptAlg ="AES";
//算法/模式/填充 填充模式:默认PKCS5Padding 填充模式,通过BouncyCastle组件来让java里面支持PKCS7Padding填充
private static final String Cipher_Mode="AES/ECB/PKCS7Padding";
private static final String Encode="UTF-8";
private static final int Secret_Key_Size=32;//有些jar包自生可能只支持16位的密钥
private static final String Key_Encode="UTF-8";
/**
* AES/ECB/PKCS7Padding 加密
* @param content:要加密的内容
* @param key 密钥
* @return aes加密后 转base64
* @throws Exception
*/
public static String aesPKCS7PaddingEncrypt(String content, String key) throws Exception {
try {
//使支持PKCS7Padding,jdk自带的只是支PKCS5Padding填充模式,不支持PKCS7Padding,需要额外添加 BouncyCastleProvider() 这样的provider
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//设置库里的加密/解密算法类型:AES/ECB/PKCS7Padding,其它重载方法可看源码。
Cipher cipher = Cipher.getInstance(Cipher_Mode);
//调整密钥长度,转字节
byte[] realKey=getSecretKey(key);
//Cipher.ENCRYPT_MODE:加密;
//new SecretKeySpec(realKey,EncryptAlg):生成加密/解密需要的key
//realKey:密钥字符串的字节;
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(realKey,EncryptAlg));
//将加密对象加密,并指定编码方式
byte[] data=cipher.doFinal(content.getBytes(Encode));
//对称加密完成,将结果进行Base64编码,你也可以直接转为相应的字符串
String result=new Base64().encodeToString(data);
return result;
} catch (Exception e) {
e.printStackTrace();
throw new Exception("AES加密失败:content=" +content +" key="+key);
}
}
/**
* AES/ECB/PKCS7Padding 解密
* @param content
* @param key 密钥
* @return 先转base64 再解密
* @throws Exception
*/
public static String aesPKCS7PaddingDecrypt(String content, String key) throws Exception {
try {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//和上面对应,因为上面Base64编码了,所以这里将字符串进行Base64解码成对应字节
byte[] decodeBytes=Base64.decodeBase64(content);
//下面和上面一样,不再赘述。
Cipher cipher = Cipher.getInstance(Cipher_Mode);
byte[] realKey=getSecretKey(key);
//Cipher.DECRYPT_MODE:解码
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(realKey,EncryptAlg));
byte[] realBytes=cipher.doFinal(decodeBytes);
return new String(realBytes, Encode);
} catch (Exception e) {
e.printStackTrace();
throw new Exception("AES解密失败:Aescontent = " +e.fillInStackTrace(),e);
}
}
/**
* 对密钥key进行处理:如密钥长度不够位数的则 以指定paddingChar 进行填充;
* 此处用空格字符填充,也可以 0 填充,具体可根据实际项目需求做变更
* @param key
* @return
* @throws Exception
*/
public static byte[] getSecretKey(String key) throws Exception{
final byte paddingChar=' ';
byte[] realKey = new byte[Secret_Key_Size];
byte[] byteKey = key.getBytes(Key_Encode);
for (int i =0;i<realKey.length;i++){
if (i<byteKey.length){
realKey[i] = byteKey[i];
}else {
realKey[i] = paddingChar;
}
}
return realKey;
}
}
2. 有向量,AES/CBC/PKCS5Padding模式
有向量 CBC/PKCS5Padding ,CBC模式需要有密钥以及初始化向量的
package com.debug.steadyjack.springbootMQ.server.util;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
/**
* AES加解密工具
* Created by steadyjack on 2018/2/9.
*/
public class EncryptUtil {
private static final String CipherMode="AES/CBC/PKCS5Padding";
private static final String SecretKey="debug";
private static final Integer IVSize=16;
private static final String EncryptAlg ="AES";
private static final String Encode="UTF-8";
private static final int SecretKeySize=32;
private static final String Key_Encode="UTF-8";
/**
* 创建密钥
* @return
*/
private static SecretKeySpec createKey(){
StringBuilder sb=new StringBuilder(SecretKeySize);
sb.append(SecretKey);
if (sb.length()>SecretKeySize){
sb.setLength(SecretKeySize);
}
if (sb.length()<SecretKeySize){
while (sb.length()<SecretKeySize){
sb.append(" ");
}
}
try {
byte[] data=sb.toString().getBytes(Encode);
return new SecretKeySpec(data, EncryptAlg);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 创建16位向量: 不够则用0填充
* @return
*/
private static IvParameterSpec createIV() {
StringBuffer sb = new StringBuffer(IVSize);
sb.append(SecretKey);
if (sb.length()>IVSize){
sb.setLength(IVSize);
}
if (sb.length()<IVSize){
while (sb.length()<IVSize){
sb.append("0");
}
}
byte[] data=null;
try {
data=sb.toString().getBytes(Encode);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new IvParameterSpec(data);
}
/**
* 加密:有向量16位,结果转base64
* @param context
* @return
*/
public static String encrypt(String context) {
try {
byte[] content=context.getBytes(Encode);
SecretKeySpec key = createKey();
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.ENCRYPT_MODE, key, createIV());
byte[] data = cipher.doFinal(content);
String result=Base64.encodeBase64String(data);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 解密
* @param context
* @return
*/
public static String decrypt(String context) {
try {
byte[] data=Base64.decodeBase64(context);
SecretKeySpec key = createKey();
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.DECRYPT_MODE, key, createIV());
byte[] content = cipher.doFinal(data);
String result=new String(content,Encode);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception{
//密钥 加密内容(对象序列化后的内容-json格式字符串)
String content="{\"domain\":{\"method\":\"getDetails\",\"url\":\"http://www.baidu.com\"},\"name\":\"steadyjack_age\",\"age\":\"23\",\"address\":\"Canada\",\"id\":\"12\",\"phone\":\"15627284601\"}";
String encryptText=encrypt(content);
String decryptText=decrypt(encryptText);
System.out.println(String.format("明文:%s \n加密结果:%s \n解密结果:%s ",content,encryptText,decryptText));
}
}
3. 有向量、AES/CBC/PKCS7Padding模式
参考:第三方接口签名加密
有向量 CBC/PKCS7Padding ,CBC模式另一个版本写法
import java.security.MessageDigest;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import lombok.SneakyThrows;
public class SecurityUtil {
public static final String CHARSET = "UTF-8";
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
'f' };
static {
//使支持PKCS7Padding 填充模式
Security.addProvider(new BouncyCastleProvider());
}
private static String hex(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
for (int j = 0; j < len; j++) {
buf.append(HEX_CHARS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_CHARS[bytes[j] & 0x0f]);
}
return buf.toString();
}
//SHA1摘要算法
@SneakyThrows
private static byte[] sha1(byte[] input) {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(input);
return messageDigest.digest();
}
//Base64编码
@SneakyThrows
private static String encodeBase64(byte[] data) {
return new String(Base64.getEncoder().encode(data), CHARSET);
}
//Base64解码
@SneakyThrows
private static byte[] decodeBase64(String data) {
return Base64.getDecoder().decode(data.getBytes(CHARSET));
}
// 对称加密/解密算法
/**
encrypt:加密/解密
*/
@SneakyThrows
private static byte[] aes(byte[] data, byte[] key, byte[] iv, boolean encrypt) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
new IvParameterSpec(iv));
return cipher.doFinal(data);
}
/**
* 生成签名
*
* @param fieldValues 参与签名的字段
* @return 签名
*/
@SneakyThrows
public static String generateSign(Object... fieldValues) {
String joinedStr = Arrays.asList(fieldValues).stream()
.map(fieldValue -> fieldValue != null ? fieldValue.toString() : "").sorted()
.collect(Collectors.joining(""));
return hex(sha1(joinedStr.getBytes(CHARSET)));
}
/**
* 加密业务数据
*
* @param rawData 业务数据
* @param key 密钥
* @param iv 初始化向量
* @return 加密业务数据
*/
@SneakyThrows
public static String encryptData(String rawData, String key, String iv) {
return encodeBase64(aes(rawData.getBytes(CHARSET), decodeBase64(key), decodeBase64(iv), true));
}
/**
* 解密业务数据
*
* @param encryptedData 加密业务数据
* @param key 密钥
* @param iv 初始化向量
* @return 业务数据
*/
@SneakyThrows
public static String decryptData(String encryptedData, String key, String iv) {
return new String(aes(decodeBase64(encryptedData), decodeBase64(key), decodeBase64(iv), false), CHARSET);
}
// 测试Demo:签名
public static void testGenerateSign() {
String appid = "fs8342f8542c13e9ba0400342d015716";
long timestamp = 1554189997590l;
String nonce = "YDBuFQgbmladR3oK";
String data = "{\"count\":1,\"productId\":\"c3e8fd65544f11e9ba0400155d015705\"}";
String key = "4jSuQA0HNraqPjfq";
System.out.println("[签名测试]");
System.out.println("appid:" + appid);
System.out.println("timestamp:" + timestamp);
System.out.println("nonce:" + nonce);
System.out.println("data:" + data);
System.out.println("key:" + key);
String sign = generateSign(appid, timestamp, nonce, data, key);
System.out.println("签名:" + sign);
System.out.println("----------------");
}
// 测试Demo:加密解密
public static void testEncryptData() {
String data = "{\"key\":\"value\"}";
String key = "D3QE5sMzHlNCpxQ5+GMOzK==";
String iv = "Qb6HMJIDlico8/9lWyKpLf==";
System.out.println("[加解密测试]");
System.out.println("原始数据:" + data);
String encryptedData = encryptData(data, key, iv);
System.out.println("加密数据:" + encryptedData);
System.out.println("解密数据:" + decryptData(encryptedData, key, iv));
System.out.println("----------------");
}
@SneakyThrows
public static void main(String[] args) {
testGenerateSign();
testEncryptData();
}
}