国密sm4算法

发布时间 2023-10-30 11:07:28作者: 周文豪

一、概述

国密算法定义:即国家密码局认定的国产密码算法。

通过定义我们可以知道,国密算法有两个要素:

1、国家密码局认定

  在国家密码局官网上,可以看到由其发布的标准规范。

2、密码算法

首先知道什么是密码,密码就是将正常的信息加密后变为无法正常识别的编码,可以认为是一种混淆技术。

将明文数据通过密码算法变成密文后,有三个优点:

(1)、数据保密,混淆后的密文一般无直接意义。

(2)、数据完整,混淆后的密文缺失无法正常恢复。

(3)、身份验证,数据加解密或者对照,都要有密钥或算法的信息,一般可以认为是自己人。

  严格意义上的加密是必须保证能恢复明文信息的,但是我们平时说的一些加密,又要要求不能恢复原来的信息,如账号密码里的密码,一般是要求不允许恢复原文的。这时候就出现了一个问题,加密后还能不能恢复成加密前的样子?技术上能恢复的叫加密算法,不能恢复的叫哈希算法能恢复明文的算法里又分为两种:对称加密和非对称加密

  对称加密:加密时使用的密钥和解密时使用的密钥为同一个

  非对称加密:加解密时使用的密钥是一对,公开密钥和私有密钥,公开密钥用于数据加密,私有密钥用于数据解密

哈希算法:通过算法对明文数据进行混淆,同一个明文在同一个哈希算法下混淆结果一致

具体分类可见下表

下图是实际开发中遇到的要求:

为什么用国密算法

(1)、国密算法算法好,速度快。

(2)、支持国产。

(3)、有些企业或部门要求使用,从最近的情况来看,还是自家的比较安全。

  与DES和AES算法相似,国密SM4算法是一种分组加密算法。SM4分组密码(block cipher)算法是一种迭代分组密码算法,由加解密算法密钥扩展算法组成。

SM4是一种Feistel结构的分组密码算法,其分组长度和密钥长度均为128bits加密算法和密钥扩展算法迭代轮数均为32轮。SM4加解密过程的算法相同但是轮密钥的使用顺序相反

SM4密码算法使用模2加和循环移位作为基本运算。

密钥扩展算法:SM4算法使用128位的加密密钥,并采用32轮迭代加密结构,每一轮加密使用一个32位的轮密钥,总共使用32个轮密钥。因此需要使用密钥扩展算法,从加密密钥中产生32个轮密钥。

SM4算法使用128位的加密密钥,而UUID是一个128位的二进制数,故我们可以使用UUID来当作SM4算法的密钥,注意:需要将UUID的横杠-替换成空字符串

二、SM4加解密流程

SM4算法的加密大致流程如下:

密钥:加密密钥的长度为128比特,表示为MK = (MK0, MK1, MK2, MK3),其中MKi为32位,

轮密钥表示为(rk0, rk1, ……, rk31),其中rki为32位。

轮函数F:假设输入为(X0, X1, X2, X3),X为32位,则轮函数F为:F=(X0, X1, X2, X3, rk) = X0 ⊕ T(X1 ⊕ X2 ⊕ X3 ⊕ rk)

合成置换:T函数是一个可逆变换,由一个非线性变换r和线性变换L复合而成的,即T( )=L(r( ))

非线性变换有四个并行的S盒构成的,设输入为A=(a0, a1, a2, a3),输出为B=(b0, b1, b2, b3),其中ai和bi为8位。每个S盒的输入都是一个8位的字节,将这8位的前四位对应的16进制数作为行编号,后四位对应的16进制数作为列编号,然后用相应位置中的字节代替输入的字节。下图为S盒置换表:

 

线性变换L:线形变换的输入就是S盒的输出,即C=L(B)=B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24),线性变换的输入和输出都是32位的。

经过了32轮的迭代运算后,最后再进行一次反序变换即可得到加密的密文,即密文C=(Y0, Y1, Y2, Y3)=R(X32. X33, X34, X35)=(X35, X34, X33, X32)。

SM4算法的解密流程和加密流程一致,只不过轮密钥的使用顺序变成了(rk31, rk30, ……, rk0)

三、密钥扩展算法

密钥参量:轮密钥由加密密钥生成。FK=(FK0, FK1, FK2, FK3)为系统参数,以及固定参数CK=(CK0, CK1, ……,  CK31),其中FKi和CKi均为32位并用于密钥扩展算法。

系统参数FK的具体取值如下:

FK0=(A3B1BAC6), FK1=(56AA3350), FK2=(677D9197), FK3=(B27022DC)

固定参数CK的具体取值如下:

密钥扩展方法:设(K0, K1, K2, K3)=(MK0⊕FK0, MK1⊕FK1, MK2⊕FK2, MK3⊕FK3)

则rki=Ki+4=Ki⊕T‘(Ki+1⊕Ki+2⊕Ki+3⊕CKi)

其中T’()是将原来的T()中的线形变换L()替换成L'(B)=B⊕(B<<<13)⊕(B<<<23)

四、Sm4Utils工具类

代码如下:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;

/**
 * 国密sm4算法
 * author: dongliyuan
 */
public class Sm4Utils {

    static {
        // 防止内存中出现多次BouncyCastleProvider的实例
        if(null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    private static final String ENCODING = "UTF-8";
    private static final String ALGORITHM_NAME = "SM4";
    //加密算法/分组加密模式/分组填充方式
    //PKCS5Padding-以8个字节为一组进行分组加密
    //定义分组加密模式使用:PKCS5Padding
    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
    //128-32位16进制:256-64位16进制
    public static final int DEFAULT_KEY_SIZE = 128;

    public static final String CCBID = "7bc1b525c0964716bc2f1dbc97e316be";

    /**
     * 生成密钥
     * 建议使用org.bouncycastle.util.encoders.Hex将二进制转成HEX字符串
     * @return 密钥16位
     * @throws Exception
     */
    public static byte[] generateKey() throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(DEFAULT_KEY_SIZE, new SecureRandom());
        return kg.generateKey().getEncoded();
    }



    /**
     * 生成ECB暗号
     * @param algorithmName 算法名称
     * @param mode 模式
     * @param key
     * @return
     * @throws Exception
     */
    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, ALGORITHM_NAME);
        cipher.init(mode, sm4Key);
        return  cipher;
    }

    /**
     * 加密
     * @param hexKey 16进制字符串
     * @param paramStr 待加密字符
     * @return 加密后的结果
     * @throws Exception
     */
    public static String encryptEcb(String hexKey, String paramStr) throws Exception {
        String cipherText = "";
        //16进制字符串 ---> byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        //String ---> byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        //加密后的数组
        byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
        //byte[] ---> hexString
        cipherText = ByteUtils.toHexString(cipherArray);
        return cipherText;
    }

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

    /**
     * sm4解密
     * @param hexKey 16进制秘钥
     * @param cipherText 16进制的加密字符串(忽略大小写)
     * @return 解密后的字符串
     * @throws Exception
     */
    public static String decryptEcb(String hexKey, String cipherText) throws Exception {
        //用于接收解密后的字符串
        String decryptStr = "";
        //hexString ---> byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // hexString --->byte[]
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        //解密
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        //byte[] ---> String
        decryptStr = new String(srcData);
        return  decryptStr;
    }

    /**
     * 解密
     * @param key
     * @param cipherText
     * @return
     * @throws Exception
     */
    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);
    }

    public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception {
        //hexString -->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        //将16进制字符串转换成数组
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        //解密
        byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
        //将原字符串转成成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        //判断2个数组是否一致
        return Arrays.equals(decryptData, srcData);
    }

    public static void main(String[] args) throws Exception {
        System.out.println(encryptEcb(CCBID,"pts"));
        System.out.println(encryptEcb(CCBID,"ptS#1234"));
    }
}

使用工具类

我们可以使用UUID来作为SM4算法的密钥,只不过要将横杠-替换为空字符串,代码如下:

String salt = UUID.randomUUID().toString().replace("-", "");

自定义密钥:ad07f399bc438b4777bba85bfa05ca28

public class Sm4Test {
    public static void main(String[] args) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("userNo","zhangsan");
        jsonObject.put("passwd","1234");
        try {
            System.out.println(Sm4Util.encryptEcb("ad07f399bc438b4777bba85bfa05ca28",JSONObject.toJSONString(jsonObject)));
            System.out.println(Sm4Util.decryptEcb("ad07f399bc438b4777bba85bfa05ca28","9a0fd3c7ad5e41681766171c08522db37065b4327a915de2dfb03d321d8df87190bbfc67ce38b9a028b8e066bff46c53"));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

结果如下:

9a0fd3c7ad5e41681766171c08522db37065b4327a915de2dfb03d321d8df87190bbfc67ce38b9a028b8e066bff46c53
{"userNo":"zhangsan","passwd":"1234"}

五、微信小程序国密算法实现库sm-crypto

1、sm-crypto安装

使用此组件需要依赖小程序基础库 2.2.1 以上版本,同时依赖开发者工具的 npm 构建。

查看node和npm配置情况

 若发现无法查找命令node和npm,证明还未安装nodejs,可以参考下面博客链接进行安装配置node安装进

使用以下指令进行安装,打开终端,cd到小程序项目的根目录,在终端里执行以下命令:

npm install --save miniprogram-sm-crypto

2、加密

引入:

const sm4 = require('miniprogram-sm-crypto').sm4

对请求体进行加密

var data = {
            userNo: userno,
            passwd: password
        };
var encryptData = sm4.encrypt(JSON.stringify(data), app.globalData.key)

后台返回数据后解密

let decryptData = sm4.decrypt(res.data.result, app.globalData.key);

完整代码:

formSubmit: function(e) {
        var obj = e.detail.value;
        var userno = obj.userno.trim();
        var password = obj.password.trim();
        var data = {
            userNo: userno,
            passwd: password
        };
        var encryptData = sm4.encrypt(JSON.stringify(data), app.globalData.key)
        if (userno == '' || password == '') {
            wx.showToast({
                title: '请输入账号和密码',
                icon: 'none',
                duration: 2000
            })
        } else {
            wx.showLoading({
                title: '登录中...',
            });
            wx.request({
                method: 'POST',
                url: app.globalData.serverApi + "/login",
                header: {
                    'content-type': 'application/json',
                    'appKey': 'app_key'
                },
                data: {
                    applyData: encryptData
                },
                success(res) {
                    wx.hideLoading();
                    if (res.data.code == 0) {
                        //登录成功
                        wx.showToast({
                            title: res.data.message,
                            icon: 'success',
                            duration: 2000
                        })

                        let decryptData = sm4.decrypt(res.data.result, app.globalData.key);
                        //存到缓存
                        wx.setStorageSync('userInfo', JSON.parse(decryptData));

                        //跳转到首页
                        wx.reLaunch({
                            url: '/pages/codeActive/index'
                        })
                    } else {
                        //登录失败
                        wx.showToast({
                            title: res.data.message,
                            icon: 'none',
                            duration: 2000
                        })
                    }
                }
            })
        }
    }

后台使用上面的Sm4Utils工具类,在RequestBodyAdvice中对请求体进行解密,在ResponseBodyAdvice中对要返回前端的响应体进行加密。