一、需求背景
控臺項目甲方進行安全測試,測試報告其中一條:敏感信息明文傳輸
1 | 敏感信息明文傳輸 | 中危 | 查詢接口傳輸手機號、銀行卡號等敏感信息時未加密/脫敏處理。 |
二、解決方案
討論出的方案是通過前端查詢條件加密,后端對加密的數據解密的方式,加解密使用SM2國密方案。具體方案:
1.每次登錄進控臺時生成SM2公私鑰對,其中私鑰放進登錄的session user信息中,公鑰返回給前端;
2.前端拿到公鑰放入cookie,前端查詢條件使用cookie SM2公鑰加密放入臨時字段,傳輸時將臨時字段數據賦值給需要加密的字段,將加密后的數據發送post;(如果是對整個查詢條件加密則更簡單);
原傳輸參數:®Capital=112&acct=6222081202000000206
原傳輸參數:®Capital=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-。。