欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

适用于 Java 的 SM4(国家机密 4)加密和解密功能

最编程 2024-07-10 15:24:33
...

项目背景

在一些项目开发过程中, 肯定会涉及到数据传输. 为了保证传输数据的安全性, 所以需要对数据进行加解密, 简单介绍下 Java版 SM4(国密4) 加密算法的使用.

简介

  • 与DES和AES算法类似,SM4算法是一种分组密码算法。
  • 其分组长度为128bit,密钥长度也为128bit。
  • 加密算法与密钥扩展算法均采用32轮非线性迭代结构,以字(32位)为单位进行加密运算,每一次迭代运算均为一轮变换函数F。
  • SM4算法加/解密算法的结构相同,只是使用轮密钥相反,其中解密轮密钥是加密轮密钥的逆序。

原理

image.png

首先,将明文转化为字节,由于SM4加密算法按照128个位进行分组,所以很大几率会出现最后一个分组不够128位的情况,需要进行填充,填充方式有很多,比如ZeroPadding、PKCS7Padding、PKCS5Padding,不管使用哪种方式,最后每个分组都是128位。然后对每个分组执行上面的操作,每个分组按照32位一个字分成四个字,根据一定的规则计算出下一轮的结果。进行32轮的计算,最后将加密的结果逆序之后就可以了。

解密时只是将轮密钥的使用顺序进行逆向进行。

ECB模式与CBC模式

ECB 模式

  • 电子密码本模式,最古老,最简单的模式,将加密的数据分成若干组,每组的大小跟加密密钥相同。不足的部分进行填充
  • 按照顺序将计算所得的数据连在一起即可,各段数据之间互不影响
优点
  1. 简单
  2. 有利于并行计算
  3. 误差不会被传递
缺点
  1. 不能隐藏明文的模式
  2. 可能对明文进行主动攻击

CBC 模式

  • 密文分组链接模式,也需要进行分组,不足的部分按照指定的数据进行填充。
  • 需要一个初始化向量,每个分组数据与上一个分组数据加密的结果进行异或运算,最后再进行加密。将所有分组加密的结果连接起来就形成了最终的结果。
优点
  1. 不容易进行主动攻击
  2. 安全性好于ECB
缺点
  1. 不利于并行计算
  2. 误差传递
  3. 需要初始化向量

三种填充方式的比较

某些加密算法要求明文需要按一定长度对齐,叫做块大小(BlockSize),比如16字节,那么对于一段任意的数据,加密前需要对最后一个块填充到16 字节,解密后需要删除掉填充的数据

ZeroPadding,数据长度不对齐时使用0填充,否则不填充。

PKCS7Padding,假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。

PKCS5Padding,PKCS7Padding的子集,块大小固定为8字节。

由于使用PKCS7Padding/PKCS5Padding填充时,最后一个字节肯定为填充数据的长度,所以在解密后可以准确删除填充的数据,而使用ZeroPadding填充时,没办法区分真实数据与填充数据,所以只适合以\0结尾的字符串加解密。

代码实现

SmUtil.java 工具类

package cn.hutool.crypto;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.digest.SM3;
import cn.hutool.crypto.digest.mac.BCHMacEngine;
import cn.hutool.crypto.digest.mac.MacEngine;
import cn.hutool.crypto.symmetric.SM4;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;

/**
 * SM国密算法工具类<br>
 * 此工具类依赖org.bouncycastle:bcpkix-jdk15on
 *
 * @author looly
 * @since 4.3.2
 */
public class SmUtil {

	/**
	 * SM2默认曲线
	 */
	public static final String SM2_CURVE_NAME = "sm2p256v1";
	/**
	 * SM2推荐曲线参数(来自https://github.com/ZZMarquis/gmhelper)
	 */
	public static final ECDomainParameters SM2_DOMAIN_PARAMS;

	private final static int RS_LEN = 32;

	static {
		SM2_DOMAIN_PARAMS = BCUtil.toDomainParams(GMNamedCurves.getByName(SM2_CURVE_NAME));
	}

	/**
	 * 创建SM2算法对象<br>
	 * 生成新的私钥公钥对
	 *
	 * @return {@link SM2}
	 */
	public static SM2 sm2() {
		return new SM2();
	}

	/**
	 * 创建SM2算法对象<br>
	 * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>
	 * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密
	 *
	 * @param privateKeyStr 私钥Hex或Base64表示
	 * @param publicKeyStr  公钥Hex或Base64表示
	 * @return {@link SM2}
	 */
	public static SM2 sm2(String privateKeyStr, String publicKeyStr) {
		return new SM2(privateKeyStr, publicKeyStr);
	}

	/**
	 * 创建SM2算法对象<br>
	 * 私钥和公钥同时为空时生成一对新的私钥和公钥<br>
	 * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密
	 *
	 * @param privateKey 私钥
	 * @param publicKey  公钥
	 * @return {@link SM2}
	 */
	public static SM2 sm2(byte[] privateKey, byte[] publicKey) {
		return new SM2(privateKey, publicKey);
	}

	/**
	 * SM3加密<br>
	 * 例:<br>
	 * SM3加密:sm3().digest(data)<br>
	 * SM3加密并转为16进制字符串:sm3().digestHex(data)<br>
	 *
	 * @return {@link SM3}
	 */
	public static SM3 sm3() {
		return new SM3();
	}

	/**
	 * SM3加密,生成16进制SM3字符串<br>
	 *
	 * @param data 数据
	 * @return SM3字符串
	 */
	public static String sm3(String data) {
		return sm3().digestHex(data);
	}

	/**
	 * SM3加密,生成16进制SM3字符串<br>
	 *
	 * @param data 数据
	 * @return SM3字符串
	 */
	public static String sm3(InputStream data) {
		return sm3().digestHex(data);
	}

	/**
	 * SM3加密文件,生成16进制SM3字符串<br>
	 *
	 * @param dataFile 被加密文件
	 * @return SM3字符串
	 */
	public static String sm3(File dataFile) {
		return sm3().digestHex(dataFile);
	}

	/**
	 * SM4加密,生成随机KEY。注意解密时必须使用相同 {@link SymmetricCrypto}对象或者使用相同KEY<br>
	 * 例:
	 *
	 * <pre>
	 * SM4加密:sm4().encrypt(data)
	 * SM4解密:sm4().decrypt(data)
	 * </pre>
	 *
	 * @return {@link SymmetricCrypto}
	 */
	public static SM4 sm4() {
		return new SM4();
	}

	/**
	 * SM4加密<br>
	 * 例:
	 *
	 * <pre>
	 * SM4加密:sm4(key).encrypt(data)
	 * SM4解密:sm4(key).decrypt(data)
	 * </pre>
	 *
	 * @param key 密钥
	 * @return {@link SymmetricCrypto}
	 */
	public static SymmetricCrypto sm4(byte[] key) {
		return new SM4(key);
	}

	/**
	 * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2
	 *
	 * @param c1c2c3             加密后的bytes,顺序为C1C2C3
	 * @param ecDomainParameters {@link ECDomainParameters}
	 * @return 加密后的bytes,顺序为C1C3C2
	 */
	public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3, ECDomainParameters ecDomainParameters) {
		// sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。
		final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
		final int c3Len = 32; // new SM3Digest().getDigestSize();
		byte[] result = new byte[c1c2c3.length];
		System.arraycopy(c1c2c3, 0, result, 0, c1Len); // c1
		System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); // c3
		System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); // c2
		return result;
	}

	/**
	 * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密
	 *
	 * @param c1c3c2             加密后的bytes,顺序为C1C3C2
	 * @param ecDomainParameters {@link ECDomainParameters}
	 * @return c1c2c3 加密后的bytes,顺序为C1C2C3
	 */
	public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2, ECDomainParameters ecDomainParameters) {
		// sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。
		final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
		final int c3Len = 32; // new SM3Digest().getDigestSize();
		byte[] result = new byte[c1c3c2.length];
		System.arraycopy(c1c3c2, 0, result, 0, c1Len); // c1: 0->65
		System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); // c2
		System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); // c3
		return result;
	}

	/**
	 * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s<br>
	 * 来自:https://blog.****.net/pridas/article/details/86118774
	 *
	 * @param rsDer rs in asn1 format
	 * @return sign result in plain byte array
	 * @since 4.5.0
	 */
	public static byte[] rsAsn1ToPlain(byte[] rsDer) {
		ASN1Sequence seq = ASN1Sequence.getInstance(rsDer);
		byte[] r = bigIntToFixedLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue());
		byte[] s = bigIntToFixedLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue());
		byte[] result = new byte[RS_LEN * 2];
		System.arraycopy(r, 0, result, 0, r.length);
		System.arraycopy(s, 0, result, RS_LEN, s.length);

		return result;
	}

	/**
	 * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式<br>
	 * 来自:https://blog.****.net/pridas/article/details/86118774
	 *
	 * @param sign in plain byte array
	 * @return rs result in asn1 format
	 * @since 4.5.0
	 */
	public static byte[] rsPlainToAsn1(byte[] sign) {
		if (sign.length != RS_LEN * 2) {
			throw new CryptoException("err rs. ");
		}
		BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, RS_LEN));
		BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, RS_LEN, RS_LEN * 2));
		ASN1EncodableVector v = new ASN1EncodableVector();
		v.add(new ASN1Integer(r));
		v.add(new ASN1Integer(s));
		try {
			return new DERSequence(v).getEncoded("DER");
		} catch (IOException e) {
			throw new IORuntimeException(e);
		}
	}

	/**
	 * 创建HmacSM3算法的{@link MacEngine}
	 *
	 * @param key 密钥
	 * @return {@link MacEngine}
	 * @since 4.5.13
	 */
	public static MacEngine createHmacSm3Engine(byte[] key) {
		return new BCHMacEngine(new SM3Digest(), key);
	}

	/**
	 * HmacSM3算法实现
	 *
	 * @param key 密钥
	 * @return {@link HMac} 对象,调用digestXXX即可
	 * @since 4.5.13
	 */
	public static HMac hmacSm3(byte[] key) {
		return new HMac(HmacAlgorithm.HmacSM3, key);
	}

	// -------------------------------------------------------------------------------------------------------- Private method start

	/**
	 * BigInteger转固定长度bytes
	 *
	 * @param rOrS {@link BigInteger}
	 * @return 固定长度bytes
	 * @since 4.5.0
	 */
	private static byte[] bigIntToFixedLengthBytes(BigInteger rOrS) {
		// for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123,
		// r and s are the result of mod n, so they should be less than n and have length<=32
		byte[] rs = rOrS.toByteArray();
		if (rs.length == RS_LEN) {
			return rs;
		} else if (rs.length == RS_LEN + 1 && rs[0] == 0) {
			return Arrays.copyOfRange(rs, 1, RS_LEN + 1);
		} else if (rs.length < RS_LEN) {
			byte[] result = new byte[RS_LEN];
			Arrays.fill(result, (byte) 0);
			System.arraycopy(rs, 0, result, RS_LEN - rs.length, rs.length);
			return result;
		} else {
			throw new CryptoException("Error rs: {}", Hex.toHexString(rs));
		}
	}
	// -------------------------------------------------------------------------------------------------------- Private method end
}

test.java 方法

package com.sdboon.webdemo.controller;


import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author developer
 * @description: TODO
 * @date 2021/1/8 8:27
 */
@Controller
//@RequestMapping(value = "/index",method = {RequestMethod.GET,RequestMethod.POST})
public class HelloController {
    private Logger log = LoggerFactory.getLogger(this.getClass());
    @RequestMapping("/data")
    @ResponseBody
    public void data(){
       // log.error("我wwwww");
        String content = "Hello world!";
        // key必须是16位
        String key="ECfJZzIyYdmv5gXe";
        SymmetricCrypto sm4 = SmUtil.sm4(key.getBytes());
        String encryptHex = sm4.encryptHex(content);
        String decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);
        System.out.println(encryptHex+"\r\n"+decryptStr);
    }
}

结束

到这里基本就结束了, 供大家参考下^~^!

09425E83.png

  • 参考 www.jianshu.com/p/5ec8464b0…