Java | 加密技术 | 对称加密算法(不含原理)

6 篇文章 0 订阅

主要参考:对称加密-AES

一、简介

1. 对称加密概念

对称加密就是指,加密和解密使用同一个密钥的加密方式,发送方使用密钥将明文数据加密成密文,然后发送出去,接收方收到密文后,使用同一个密钥将密文解密成明文读取。
基于“对称密钥”的加密算法主要有DES、3DES(TripleDES)、AES、RC2、RC4、RC5和Blowfish等。最常用的对称加密算法DES、3DES(TripleDES)和AES,AES安全性更高,速度更快,所以其应用最为广泛。

2. 优缺点

优点:加密计算量小、速度块,适合对大量数据进行加密的场景,相对非对称加密来说。
缺点:

  1. 密钥传输问题:密钥如何安全的网络传输到请求方是个核心关键问题,可以了解https。
  2. 密钥管理:每个用户需要对应一个密钥,所以会增加管理,相对无密钥的加密算法。

3. 关键概念

要理解AES的加密流程,会涉及到AES加密的五个关键词,分别是:分组密码体制Padding密钥初始向量IV四种加密模式,注意理解时,搞清楚字节、位、编码字符集的关系。

1. 分组密码体制

所谓分组密码体制就是指将明文切成一段一段的来加密,然后再把一段一段的密文拼起来形成最终密文的加密方式。AES采用分组密码体制,即AES加密会首先把明文切成一段一段的,而且每段数据的长度要求必须是128位16个字节,如果最后一段不够16个字节了,就需要用Padding来把这段数据填满16个字节,然后分别对每段数据进行加密,最后再把每段加密数据拼起来形成最终的密文。

2. Padding填充模式

Padding就是用来把不满16个字节的分组数据填满16个字节用的,它有三种模式PKCS5PKCS7NOPADDING

  • 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.jarUS_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. 进行加密和解密的前提是:加密方和解密方的密钥、初始化向量、填充模式要一致

二、代码

主要有有向量、无向量。

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();
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值