前端查询条件加密传输方案(SM2加解密)

发布于:2025-07-06 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、需求背景

控台项目甲方进行安全测试,测试报告其中一条:敏感信息明文传输

1

敏感信息明文传输

中危

查询接口传输手机号、银行卡号等敏感信息时未加密/脱敏处理。

二、解决方案

讨论出的方案是通过前端查询条件加密,后端对加密的数据解密的方式,加解密使用SM2国密方案。具体方案:

1.每次登录进控台时生成SM2公私钥对,其中私钥放进登录的session user信息中,公钥返回给前端;

2.前端拿到公钥放入cookie,前端查询条件使用cookie SM2公钥加密放入临时字段,传输时将临时字段数据赋值给需要加密的字段,将加密后的数据发送post;(如果是对整个查询条件加密则更简单);

原传输参数:&regCapital=112&acct=6222081202000000206

原传输参数:&regCapital=112&acct=564a761bc0d8c260d84abf25fa516eb09f04cb49bfb557943060c0879a4159736a70f4b2463fc65941bd1f29731f35570467e98ec916d091ad651de7afeb1ac8ff1ac05a9bc6df12bbd447fa1dec75a517c611a8557fe7a8e378055cff3f3d4f299b69e621ebd1260cfd331167d45f03

3.后端接收到加密后的数据,使用session中私钥进行解密。

三、代码实现

1.Sm2Utils.java文件,包含生成公私钥对,后端对前端加密数据解密的方法。

package com.xxx.xxx.util;

import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName Sm2Utils
 * @Description 生成SM2公私钥对
 * @Author whb
 * @Date 2024/10/9 15:53
 * @Version 1.0
 **/
public class Sm2Utils {
    private static final X9ECParameters SM2_EC_PARAMS = GMNamedCurves.getByName("sm2p256v1");
    private static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(
            SM2_EC_PARAMS.getCurve(),
            SM2_EC_PARAMS.getG(),
            SM2_EC_PARAMS.getN(),
            SM2_EC_PARAMS.getH());

    /**
     * 生成SM2密钥对
     * @return String[0]=公钥(04开头未压缩格式), String[1]=私钥(64字符16进制)
     */
    public static String[] generateKeyPair() {
        try {
            // 1. 初始化密钥生成器
            ECKeyPairGenerator generator = new ECKeyPairGenerator();
            ECKeyGenerationParameters keyGenParams = new ECKeyGenerationParameters(DOMAIN_PARAMS, new SecureRandom());
            generator.init(keyGenParams);

            // 2. 生成密钥对
            AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();

            // 3. 获取私钥(去掉前面的00)
            ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters) keyPair.getPrivate();
            String privateKey = leftPad(privateKeyParams.getD().toString(16), 64, '0');

            // 4. 获取公钥(04开头未压缩格式)
            ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters) keyPair.getPublic();
            ECPoint publicKeyPoint = publicKeyParams.getQ();
            String publicKey = "04" +
                    leftPad(publicKeyPoint.getAffineXCoord().toBigInteger().toString(16), 64, '0') +
                    leftPad(publicKeyPoint.getAffineYCoord().toBigInteger().toString(16), 64, '0');

            return new String[]{publicKey, privateKey};
        } catch (Exception e) {
            throw new RuntimeException("生成SM2密钥对失败", e);
        }
    }

    private static String leftPad(String input, int size, char padChar) {
        if (input.length() >= size) {
            return input;
        }
        StringBuilder sb = new StringBuilder(size);
        for (int i = input.length(); i < size; i++) {
            sb.append(padChar);
        }
        sb.append(input);
        return sb.toString();
    }

    /**
     * SM2解密
     * @param privateKeyHex 16进制私钥(64字符)
     * @param cipherDataHex 加密数据(16进制字符串)
     * @return 解密后的原文
     */
    public static String decrypt(String privateKeyHex, String cipherDataHex) {
        try {
            // 1. 验证私钥格式
            if (privateKeyHex == null || privateKeyHex.length() != 64) {
                throw new IllegalArgumentException("私钥必须是64字符的16进制字符串");
            }

            // 2. 验证加密数据格式
            if (cipherDataHex == null || cipherDataHex.length() < 194) {
                throw new IllegalArgumentException("加密数据格式不正确,长度不足");
            }

            // 3. 准备私钥参数
            BigInteger privateKeyD = new BigInteger(privateKeyHex, 16);
            ECPrivateKeyParameters privateKeyParams = new ECPrivateKeyParameters(privateKeyD, DOMAIN_PARAMS);

            // 4. 初始化SM2解密引擎(使用C1C3C2模式与前端一致)
            SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
            engine.init(false, privateKeyParams);

            // 5. 解码加密数据
            byte[] cipherData = Hex.decode(cipherDataHex);

            // 6. 验证数据头
            if (cipherData[0] != 0x04) {
                throw new IllegalArgumentException("加密数据必须以04开头");
            }

            // 7. 解密数据
            byte[] decrypted = engine.processBlock(cipherData, 0, cipherData.length);

            return new String(decrypted, "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException("SM2解密失败: " + e.getMessage(), e);
        }
    }

    /**
     * 生成SM2密钥对
     * @return 包含私钥(64字符)和公钥(130字符04开头)的数组
     */
    public static Map<String, String> genSm2KeyPair() {
        try {
            Map<String, String> keyMap = new HashMap<>(2);
            String[] keys =  generateKeyPair();

            keyMap.put("sm2PriKey", keys[1]);
            keyMap.put("sm2PubKey", keys[0]);
            return keyMap;
        } catch (Exception e) {
            throw new RuntimeException("生成SM2密钥对失败", e);
        }
    }

    /**
     * SM2解密
     * @param cipherDataHex 前端加密后的16进制字符串
     * @param privateKeyHex 16进制格式的私钥(64字符)
     * @return 解密后的原始字符串
     */
    public static String queryDataSm2Decrypt(String cipherDataHex, String privateKeyHex) {
        return decrypt(privateKeyHex, "04"+cipherDataHex);
    }
}

2.login.java执行公私钥对

SessionUser suer = new SessionUser(userInfo, custInfo, roleIdArray, roleName, roleRank);
            Map<String, String> sm2KeyMaps = Sm2Utils.genSm2KeyPair();
            suer.setSm2PriKey(sm2KeyMaps.get("sm2PriKey"));
SessionUser.pushUser(suer, request);


.........

//页面使用SM2公钥加签sm2PubKey
mav.addObject("sm2PubKey", sm2KeyMaps.get("sm2PubKey"));

3.前端页面对数据加密

登录后js对公钥存放到cookie中

jQuery.cookie("login_sm2pubkey","${sm2PubKey}");

页面使用,先引用sm2.js,下载sm-crypto/dist at master · JuneAndGreen/sm-crypto · GitHub

<script type="text/javascript" src="${rc.contextPath}/js/sm-crypto/sm2.js${refreshDate}"></script>

......

<td scope="row">
                            <input name="certNo" class="form-control" style="height:30px;width:300px;"/>
                        </td>
                        <td style="display:none"><input name="certNoSm2" class="form-control"/>
                        </td>

......
//先对数据加密
encryptData("userInfoQueryForm");
//post发送
post......


function encryptWithSM2(param,sm2PubKey) {
        // 执行SM2加密
        const cipherMode = 1; // 1 - C1C3C2 模式
        const encryptData = sm2.doEncrypt(param, sm2PubKey, cipherMode);
        return encryptData;
    }
    function encryptData(formid) {
        var sm2PubKey = jQuery.cookie("login_sm2pubkey");
        var formData=document.getElementById(formid);
        if (!formData.certNo.value == "") {
            formData.certNoSm2.value = encryptWithSM2(formData.certNo.value,sm2PubKey)
        }
    }

4.后端解密

String sm2HexD = SessionUser.getUser(request).getSm2PriKey();
            String certNo = queryForm.getCertNo();
            if (com.changingpay.commons.util.StringUtils.isNotEmpty(sm2HexD)) {
                certNo = Sm2Utils.queryDataSm2Decrypt(queryForm.getCertNo(),sm2HexD);
            }

四、SM2解密坑

1.前端使用cipherMode = 1; // 1 - C1C3C2 模式,后端解密也需要用C1C3C2 模式,不然报错

2.后端解密时,需要对前端加密的数据前面加04,不然解密报错Invalid point encoding 0x-。。


网站公告

今日签到

点亮在社区的每一天
去签到