一文带你学会国产加密算法SM4的java实现方案

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 今天给大家带来一个国产SM4加密解密算法的java后端解决方案,代码完整,可以直接使用,希望给大家带来帮助,尤其是做政府系统的开发人员,可以直接应用到项目中进行加密解密。

前言

今天给大家带来一个国产SM4加密解密算法的java后端解决方案,代码完整,可以直接使用,希望给大家带来帮助,尤其是做政府系统的开发人员,可以直接应用到项目中进行加密解密。
画重点!是SM4哦,不是SM。哈哈,各位要在知识里遨游,不要想歪。正文开始~

国产SM4加密解密算法概念介绍

SMS4算法是在国内广泛使用的WAPI无线网络标准中使用的加密算法,是一种32轮的迭代非平衡Feistel结构的分组加密算法,其密钥长度和分组长度均为128。SMS4算法的加解密过程中使用的算法是完全相同的,唯一不同点在于该算法的解密密钥是由它的加密密钥进行逆序变换后得到的。
SMS4分组加密算法是中国无线标准中使用的分组加密算法,在2012年已经被国家商用密码管理局确定为国家密码行业标准,标准编号GM/T 0002-2012并且改名为SM4算法,与SM2椭圆曲线公钥密码算法,SM3密码杂凑算法共同作为国家密码的行业标准,在我国密码行业中有着极其重要的位置。
SMS4算法的分组长度为128bit,密钥长度也是128bit。加解密算法均采用32轮非平衡Feistel迭代结构,该结构最先出现在分组密码LOKI的密钥扩展算法中。SMS4通过32轮非线性迭代后加上一个反序变换,这样只需要解密密钥是加密密钥的逆序,就能使得解密算法与加密算法保持一致。SMS4加解密算法的结构完全相同,只是在使用轮密钥时解密密钥是加密密钥的逆序。
S盒是一种利用非线性变换构造的分组密码的一个组件,主要是为了实现分组密码过程中的混淆的特性和设计的。SMS4算法中的S盒在设计之初完全按照欧美分组密码的设计标准进行,它采用的方法是能够很好抵抗差值攻击的仿射函数逆映射复合法。

SM4加密算法应用场景

SM4常用于政府系统的数据传输加密,比如当我们前端向后台传参数的时候,可以使用此算法。对参数的数据进行加密,然后后台对加密的数据进行解密再存储到数据库中,保证数据传输过程中,不受泄露。
本次提供的方案不仅提供sm4的加密解密,还提供了md5算法的完整性防篡改校验。

Java端解决方案

对于java端,我们使用的基于spring的aop切面和自定义注解来实现。整体思路为,当后台开启加密解密的时候,针对于打上注解的方法,寻找实体类中打上注解的字段进行加密和解密。再从前端传递请求的request中取出md5的header,进行md5的完整性,防篡改校验。

首先我们必须说的是两个工具类,一个是SM4Utils工具类,另一个则是md5工具类。
下面先来说一下SM4Utils。这个工具类用于SM4算法的加密和解密及密码校验。我们先直接看代码,然后后面对此进行解释。

SM4Utils

public class Sm4Utils {

    private static final String ENCODING = "UTF-8";
    public static final String ALGORIGTHM_NAME = "SM4";
    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS7Padding";
    public static final int DEFAULT_KEY_SIZE = 128;

    public Sm4Utils() {
    }

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     *  @Description:生成ecb暗号
     */
    private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName,BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORIGTHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }

    /**
     *  @Description:自动生成密钥
     */
    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORIGTHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }


    /**
     *  @Description:加密
     */
    public static String encryptEcb(String hexKey, String paramStr, String charset) throws Exception {
        String cipherText = "";
        if (null != paramStr && !"".equals(paramStr)) {
            byte[] keyData = ByteUtils.fromHexString(hexKey);
            charset = charset.trim();
            if (charset.length() <= 0) {
                charset = ENCODING;
            }
            byte[] srcData = paramStr.getBytes(charset);
            byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
            cipherText = ByteUtils.toHexString(cipherArray);
        }
        return cipherText;
    }

    /**
     *  @Description:加密模式之ecb
     */
    public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        byte[] bs = cipher.doFinal(data);
        return bs;
    }

    /**
     *  @Description:sm4解密
     */
    public static String decryptEcb(String hexKey, String cipherText, String charset) throws Exception {
        String decryptStr = "";
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        charset = charset.trim();
        if (charset.length() <= 0) {
            charset = ENCODING;
        }
        decryptStr = new String(srcData, charset);
        return decryptStr;
    }

    /**
     *  @Description:解密
     */
    public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }

    /**
     *  @Description:密码校验
     */
    public static boolean verifyEcb(String hexKey,String cipherText,String paramStr) throws Exception {
        boolean flag = false;
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        byte[] decryptData = decrypt_Ecb_Padding(keyData,cipherData);
        byte[] srcData = paramStr.getBytes(ENCODING);
        flag = Arrays.equals(decryptData,srcData);
        return flag;
    }

    /**
     *  @Description:测试类
    */
    public static void main(String[] args) {
        try {
            String json = "{\"name\":\"color\",\"sex\":\"man\"}";
            // 自定义的32位16进制密钥
            String key = "cc9368581322479ebf3e79348a2757d9";
            String cipher = Sm4Utils.encryptEcb(key, json,ENCODING);
            System.out.println(cipher);
            System.out.println(Sm4Utils.verifyEcb(key, cipher, json));
            json = Sm4Utils.decryptEcb(key, cipher,ENCODING);
            System.out.println(json);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

我们来仔细看一下上面的代码,首先定义了个4个静态不可修改都变量,用于下方方法的使用。包括编码utf-8,密码名称sm4,密码的分组方式SM4/ECB/PKCS7Padding和默认的key值长度128。
整体的方法我们分为生成ecb暗号,自动生成密钥,加密,解密,密码校验的算法。
以上为sm4utils的核心代码。

MD5Utils


/**
 *  @Description:md5加密工具
 */
public class Md5Utils {

    /**
     *  @Description:获得md5加密串
     */
    public static String getMD5String(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            return new BigInteger(1, md.digest()).toString(16);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

MD5Utils主要用于对参数的完整性校验,防止篡改。

此时我们已经实现了主要的加密解密工具,接下来实现spring的aop自定义注解,自定义注解我们要实现三个。
第一个是加密注解,用于方法上的,表示该方法的参数需要被加密。
第二个是解密注解,用于方法上的,表示该方法的参数需要被解密。
第三个是字段加密解密注解,用于标识实体类的字段是否需要被加密和解密。

加密的自定义注解

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Order(-2147483648)
public @interface EncryptMethod {
}

可以看到此时打了一个order注解,-2147483648用于标识优先级最高。

解密的自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Order(-2147483648)
@Inherited
public @interface DecryptMethod {
}

加密解密字段的自定义注解

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Order(-2147483648)
public @interface EncryptField {
}

注解的准备工作已经做完,接下来就是实现sm4的切面方法。不了解spring的aop的实现方法,可以去先补一下spring的相关知识,这里不做赘述。我们这里采用的切面都是环绕通知,切面的切点是加密解密的注解。

加密解密切面的实现

说一下切面的答题实现思路。我们可以看到@ConditionalOnProperty(prefix = "sm4", value = "enable", matchIfMissing = false),这个我们做成了根据配置文件的配置进行动态的开关。
我们在appication.yml文件中进行如下的配置。这样切面是否生效就取决于配置。然后我们捕捉加密的注解和解密的注解,然后对加了注解的方法中的逻辑进行加密和解密。
下方代码的切点方法为EncryptAOPCut和DecryptAOPCut。随后通过around对切点进行捕捉。分别调用的核心的加密算法encryptMethodAop和解密算法decryptMethodAop。

sm4:
  enable: true

核心的加密解密算法都是使用环绕通知的ProceedingJoinPoint类,从他的对象中我们可以取到spring的各种参数,包括request请求,请求的参数和response对象。


/**
 *  @Description:sm4加密解密切面
 */
@Order(-2147483648)
@Aspect
@Component
@ConditionalOnProperty(prefix = "sm4", value = "enable", matchIfMissing = false)
public class Sm4Aspect {

    private Logger log = LoggerFactory.getLogger(Sm4Aspect.class);
    private static final String DEFAULT_KEY = "cc9368581322479ebf3e79348a2757d9";

    public Sm4Aspect() {
    }

    @Pointcut("@annotation(com.jichi.aop.sm4.EncryptMethod)")
    public void EncryptAOPCut() {
    }

    @Pointcut("@annotation(com.jichi.aop.sm4.DecryptMethod)")
    public void DecryptAOPCut() {
    }

    @Around("EncryptAOPCut()")
    public Object encryptMethodAop(ProceedingJoinPoint joinPoint) {
        Object responseObj = null;
        try {
            responseObj = joinPoint.proceed();
            this.handleEncrypt(responseObj);
            //md5加密
            String md5Data = Md5Utils.getMD5String(new Gson().toJson(responseObj));
            SpringContextUtil.getHttpServletResponse().setHeader("md5",md5Data);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            this.log.error("encryptMethodAop处理出现异常{}", throwable);
        }
        return responseObj;
    }

    @Around("DecryptAOPCut()")
    public Object decryptMethodAop(ProceedingJoinPoint joinPoint) {
        Object responseObj = null;
        try {
            responseObj = joinPoint.getArgs()[0];
            //throw new RuntimeException("md5校验失败");
            this.handleDecrypt(responseObj);
            String md5 = "";
            md5 = Md5Utils.getMD5String(new Gson().toJson(responseObj));
            System.out.println(md5);
            String origianlMd5 = "";
            origianlMd5 = SpringContextUtil.getHttpServletRequest().getHeader("md5");
            if(origianlMd5.equals(md5)){
                responseObj = joinPoint.proceed();
            }else{
                this.log.error("参数的md5校验不同,可能存在篡改行为,请检查!");
                throw new Exception("参数的md5校验不同,可能存在篡改行为,请检查!");
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            this.log.error("decryptMethodAop处理出现异常{}", throwable);
        }
        return responseObj;
    }

    private void handleEncrypt(Object requestObj) throws Exception {
        if (!Objects.isNull(requestObj)) {
            Field[] fields = requestObj.getClass().getDeclaredFields();
            Field[] fieldsCopy = fields;
            int fieldLength = fields.length;
            for(int i = 0; i < fieldLength; ++i) {
                Field field = fieldsCopy[i];
                boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
                if (hasSecureField) {
                    field.setAccessible(true);
                    String plaintextValue = (String)field.get(requestObj);
                    String encryptValue = Sm4Utils.encryptEcb(DEFAULT_KEY, plaintextValue, "");
                    field.set(requestObj, encryptValue);
                }
            }
        }
    }

    private Object handleDecrypt(Object responseObj) throws Exception {
        if (Objects.isNull(responseObj)) {
            return null;
        } else {
            Field[] fields = responseObj.getClass().getDeclaredFields();
            Field[] fieldsCopy = fields;
            int fieldLength = fields.length;
            for(int i = 0; i < fieldLength; ++i) {
                Field field = fieldsCopy[i];
                boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
                if (hasSecureField) {
                    field.setAccessible(true);
                    String encryptValue = (String)field.get(responseObj);
                    String plaintextValue = Sm4Utils.decryptEcb(DEFAULT_KEY, encryptValue, "");
                    field.set(responseObj, plaintextValue);
                }
            }
            return responseObj;
        }
    }
}

代码实际应用

首先我们可以定义一个实体类,对实体类的字段进行加密或解密的标识。我们这里建立了一个Info实体类,对于其中的name属性,我们加了注解加密解密字段,对于sex属性我们不做任何处理。

@Data
public class Info {

    @EncryptField
    private String name;

    private String sex;

}

然后我们对于controller方法打上加密的方法或解密的方法。

@RestController
@RequestMapping("/demo/test")
public class TestController {


    @PostMapping("/saveInfo")
    @DecryptMethod
    public HashMap<String,String> saveInfo(@RequestBody Info info) {
        HashMap<String,String> result = new HashMap<String,String>();
        String name = info.getName();
        System.out.println(name);
        String sex= info.getSex();
        System.out.println(sex);
        result.put("flag","1");
        result.put("msg","操作成功");
        return result;
    }

}

注意到方法上的注解@DecryptMethod,以为这着我们的这个方法将会进行解密。如果是@EncryptMethod,则代表对方法进行加密。

总结

到此为止,涉及到java后端的代码解决方案已经完毕。示例代码已经给出,大家可以直接使用,本人亲测有效。文中难免有不足,欢迎大家批评指正。

相关文章
|
1月前
|
Java Maven 数据安全/隐私保护
如何实现Java打包程序的加密代码混淆,避免被反编译?
【10月更文挑战第15天】如何实现Java打包程序的加密代码混淆,避免被反编译?
45 2
|
21天前
|
算法 安全 物联网
关于SM2、SM3、SM4、SM9这四种国密算法
本文介绍了四种国密算法——SM2、SM3、SM4和SM9。SM2是一种基于椭圆曲线的非对称加密算法,用于数据加密和数字签名;SM3是哈希算法,用于数字签名和消息完整性验证;SM4是对称加密算法,用于数据加密和解密;SM9是基于标识的非对称密码算法,适用于物联网环境中的数据安全和隐私保护。
130 0
|
1月前
|
安全 算法 Java
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
本文提供了在数据库中对密码等敏感信息进行加盐加密的详细教程,包括手写MD5加密算法和使用Spring Security的BCryptPasswordEncoder进行加密,并强调了使用BCryptPasswordEncoder时需要注意的Spring Security配置问题。
139 0
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
|
2月前
|
安全 Java 数据安全/隐私保护
- 代码加密混淆工具-Java 编程安全性
在Java编程领域,保护代码安全与知识产权至关重要。本文探讨了代码加密混淆工具的重要性,并介绍了五款流行工具:ProGuard、DexGuard、Jscrambler、DashO 和 Ipa Guard。这些工具通过压缩、优化、混淆和加密等手段,提升代码安全性,保护知识产权。ProGuard 是开源工具,用于压缩和混淆Java代码;DexGuard 专为Android应用程序设计,提供强大加密功能;Jscrambler 基于云,保护Web和移动应用的JavaScript及HTML5代码;DashO 支持多种Java平台和
211 1
|
3月前
|
存储 算法 Java
在Java中使用MD5对用户输入密码进行加密存储、同时登录验证。
这篇文章详细介绍了在Java项目中如何使用MD5算法对用户密码进行加密存储和登录验证,包括加入依赖、编写MD5工具类、注册时的密码加密和登录时的密码验证等步骤,并通过示例代码和数据库存储信息展示了测试效果。
在Java中使用MD5对用户输入密码进行加密存储、同时登录验证。
|
3月前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
2月前
|
存储 算法 安全
超级好用的C++实用库之国密sm4算法
超级好用的C++实用库之国密sm4算法
55 0
|
3月前
|
安全 Java 应用服务中间件
网络安全的护城河:漏洞防御与加密技术深入浅出Java并发编程
【8月更文挑战第31天】在数字世界的棋盘上,每一次点击都可能是一步棋。网络安全的战场无声却激烈,漏洞如同裂缝中的风,悄无声息地侵袭着数据的堡垒。本文将揭示网络漏洞的隐蔽角落,探讨如何通过加密技术筑起防线,同时提升个人和组织的安全意识,共同守护我们的数字家园。
|
3月前
|
安全 算法 Java
java系列之~~网络通信安全 非对称加密算法的介绍说明
这篇文章介绍了非对称加密算法,包括其定义、加密解密过程、数字签名功能,以及与对称加密算法的比较,并解释了非对称加密在网络安全中的应用,特别是在公钥基础设施和信任网络中的重要性。
|
4月前
|
Java 关系型数据库 MySQL
GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现
在 2024 OpenTelemetry Community Day 会议中,阿里云可观测工程师张乎兴(望陶)和饶子昊(铖朴)为大家带来了《GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现》的演讲分享,介绍阿里云在相关领域的探索方案,本文是相关分享对应的中文整理。
252 18
下一篇
无影云桌面