前后端SM2加密交互问题解析与解决方案

1,643 阅读3分钟

SM2 前后端加密交互问题解析与解决方案

问题背景

在工作中,遇到前后端使用SM2加密交互时,遇到了前端使用公钥加密,后端用私钥解密解不开的问题,经过查询思考后发现了该问题的原因并解决,特此记录。

SM2 国密算法加解密交互中,前后端通常使用不同的编码方式:

  • 前端(如 sm-crypto)生成的密文默认是 Hex 编码(不带 04 前缀)。
  • 后端(如 Java 的 BouncyCastle 或 Hutool)生成的密文通常是 Base64 编码(可能含 04 前缀,目前使用Hutoo-all测试不含04 前缀)。

这导致 前端加密后端解密 或 后端加密前端解密 时出现格式不匹配问题。


核心问题分析

场景问题原因
前端加密 → 后端解密解密失败前端 Hex 密文不带 04,但后端需要 04
后端加密 → 前端解密解密失败后端 Base64 转 Hex 后含 04,但前端不需要

解决方案

1. 前端加密 → 后端解密

步骤:

  1. 前端生成 Hex 密文(如 "a1b2c3...")。

  2. 前端需补 04 后传给后端:

    const cipherText = sm2.doEncrypt(plainText, publicKey); // 生成 Hex 密文
    const cipherTextWith04 = "04" + cipherText; // 补 04
    
  3. 后端解码时直接处理:

    String cipherText = "04a1b2c3..."; // 前端传来的 Hex
    byte[] cipherBytes = Hex.decode(cipherText); // Hex 转字节
    String plainText = sm2Decrypt(cipherBytes, privateKey); // 解密
    

2. 后端加密 → 前端解密

步骤:

  1. 后端生成 Base64 密文(如 "BElNTU9S...")。

  2. 后端转 Hex 并去掉 04

    public static String encryptForFrontend(String plainText, String publicKey) {
        String base64Cipher = sm2Encrypt(plainText, publicKey); // Base64 密文
        byte[] bytes = Base64.getDecoder().decode(base64Cipher);
        String hexCipher = Hex.encodeHexString(bytes); // Base64 → Hex
        if (hexCipher.startsWith("04")) {
            hexCipher = hexCipher.substring(2); // 去掉 04
        }
        return hexCipher;
    }
    
  3. 前端直接解密:

    const plainText = sm2.doDecrypt(cipherText, privateKey); // 自动处理无 04 的 Hex
    

完整工具类(Java 版)

import org.apache.commons.codec.binary.Hex;
import java.util.Base64;

public class Sm2CryptoUtils {

    /**
     * 后端加密 → 前端可解密的 Hex(无 04)
     */
    public static String encryptForFrontend(String plainText, String publicKey) {
        String base64Cipher = sm2Encrypt(plainText, publicKey);
        byte[] bytes = Base64.getDecoder().decode(base64Cipher);
        String hexCipher = Hex.encodeHexString(bytes);
        return remove04Prefix(hexCipher);
    }

    /**
     * 解密前端传来的 Hex(补 04)
     */
    public static String decryptFromFrontend(String hexCipher, String privateKey) {
        hexCipher = add04PrefixIfNeeded(hexCipher);
        byte[] cipherBytes = Hex.decodeHex(hexCipher);
        return sm2Decrypt(cipherBytes, privateKey);
    }

    private static String remove04Prefix(String hex) {
        return hex.startsWith("04") ? hex.substring(2) : hex;
    }

    private static String add04PrefixIfNeeded(String hex) {
        return hex.startsWith("04") ? hex : "04" + hex;
    }
}

前端适配代码(JavaScript 版)

import { sm2 } from 'sm-crypto';

/**
 * 加密 → 传给后端(补 04)
 */
function encryptForBackend(plainText, publicKey) {
  const cipherText = sm2.doEncrypt(plainText, publicKey); // Hex 密文
  return "04" + cipherText; // 补 04
}

/**
 * 解密后端传来的 Hex(去 04)
 */
function decryptFromBackend(cipherText, privateKey) {
  if (cipherText.startsWith("04")) {
    cipherText = cipherText.substring(2); // 去 04
  }
  return sm2.doDecrypt(cipherText, privateKey);
}

关键点总结

  1. 编码差异

    • 前端默认 Hex,后端默认 Base64。
    • SM2 密文的 04 是椭圆曲线未压缩标识,部分库需要它,部分库不需要。
  2. 转换规则

    方向操作
    前端 → 后端Hex 补 04
    后端 → 前端Base64 → Hex → 去 04
  3. 为什么 04 重要?

    • 某些库(如 Java 的 BouncyCastle)要求密文含 04,而 sm-crypto 默认不带。

测试用例

1. 前端加密 → 后端解密

// 前端
const cipherText = encryptForBackend("Hello", "04公钥...");
// 发送 cipherText(带 04)到后端
// 后端
String plainText = Sm2CryptoUtils.decryptFromFrontend(cipherText, "私钥");

2. 后端加密 → 前端解密

// 后端
String cipherText = Sm2CryptoUtils.encryptForFrontend("Hello", "公钥");
// 返回 cipherText(无 04)到前端
// 前端
const plainText = decryptFromBackend(cipherText, "私钥");

**常见问题 **

Q1:为什么前端生成的 Hex 不带 04
A1:sm-crypto 默认使用压缩格式,而 04 是未压缩标识。可通过配置强制包含:

const cipherText = sm2.doEncrypt(plainText, publicKey, { mode: "uncompressed" }); // 含 04

Q2:后端如何判断 Hex 是否需要补 04
A2:SM2 密文长度应为 64 字节(128 字符)或 65 字节(130 字符,含 04)。如果长度是 128,补 04;如果是 130,直接使用。


通过上述方案,可彻底解决 SM2 前后端加解密的编码兼容性问题!