1. 项目概述:为什么要在Node.js里搞SM4?

最近在做一个需要对接国内某支付平台的项目,对方接口的敏感数据字段,比如手机号、身份证号,明确要求使用国密SM4算法进行加密传输。一开始我也懵了一下,毕竟平时AES、RSA用得顺手,SM4接触不多。但需求就是命令,硬着头皮也得搞。折腾了一圈,从找库、踩坑到最终稳定上线,发现用Node.js实现SM4加解密,说难不难,但里面的门道和细节还真不少。今天就把我这趟“填坑之旅”的完整过程、核心代码和避坑心得整理出来,如果你也面临类似的需求,或者对国密算法在Node.js中的应用感兴趣,这篇应该能帮你省下不少时间。

简单来说,SM4是一种分组对称加密算法,和AES属于同一类别,但它是咱们国家密码管理局认定的商用密码标准算法。它的分组长度和密钥长度都是128位。在Node.js环境下,由于官方 crypto 模块并未原生支持SM4,所以我们得借助第三方库。整个过程的核心就是:选对一个靠谱的库,然后搞懂它的几种工作模式(比如ECB、CBC),以及如何处理填充(Padding)。下面,我就从环境准备、库的选型、核心代码实现到线上实战问题,一步步拆开讲。

2. 核心依赖选型与项目初始化

2.1 为什么是 sm-crypto

面对Node.js里实现SM4的需求,第一步就是找轮子。npm上一搜,名字里带“sm4”、“gm-crypto”的库有好几个。经过一番对比和测试,我最终选择了 sm-crypto 这个库。理由很实在:

  1. 功能专注且全面 :它专注于国密算法,不仅支持SM2、SM3、SM4,而且对SM4的支持很完整,涵盖了ECB、CBC等常用模式,以及PKCS#5/PKCS#7填充。
  2. 纯JavaScript实现 :这意味着它不依赖任何本地编译的模块(比如C++插件)。在部署时,尤其是在一些受限的服务器环境或者Serverless(FaaS)环境下,你不需要操心Node-gyp编译、系统库缺失这些令人头疼的问题, npm install 之后直接就能用,兼容性极好。
  3. 文档和社区相对较好 :它的GitHub仓库star数较多,Issues里反馈的问题和解决方案也比较活跃,遇到问题更容易找到线索。
  4. API设计友好 :它的API设计借鉴了Node.js原生 crypto 模块的风格,对于熟悉 crypto 的开发者来说,上手成本很低。

当然,也有其他选择,比如 gm-crypto 。但 gm-crypto 在某些版本对SM4 CBC模式的支持上,我测试时遇到过一些奇怪的问题。而 bcprov-jdk18on 那是Java系的库,虽然功能强大,但在Node.js里直接用它并不方便。所以,综合来看, sm-crypto 是当前Node.js项目中最稳妥、最省心的选择。

注意 :选择第三方加密库,安全性和可靠性是首要考量。 sm-crypto 经过了较多项目的实践检验,但任何加密实现都应谨慎评估。对于极高安全要求的场景,建议深入测试或咨询密码学专家。

2.2 初始化一个Node.js项目

假设你现在要从零开始。首先,确保你的系统已经安装了Node.js和npm。你可以通过 node -v npm -v 来检查。如果还没装,去Node.js官网下载LTS版本安装即可,过程很简单,这里不赘述。

创建一个新的项目目录并初始化:

mkdir nodejs-sm4-demo
cd nodejs-sm4-demo
npm init -y

接下来,安装核心依赖 sm-crypto

npm install sm-crypto

为了便于我们测试和开发,再安装一个辅助工具 dotenv 来管理环境变量,以及 jest mocha 做单元测试(可选,但强烈建议)。这里我们先装上 dotenv

npm install dotenv

安装完成后,你的 package.json dependencies 里应该就有了 sm-crypto dotenv

3. SM4加解密核心原理与模式详解

在动手写代码前,有必要把SM4的几个关键概念捋清楚,这能帮你理解后续代码里的每一个参数。

3.1 基础概念:分组、密钥与填充

  • 分组长度(Block Size) :SM4和AES一样,属于分组密码。它一次处理一个数据块(Block),这个块的大小是 128位(16字节) 。这意味着,如果你要加密一段文本,无论长短,算法都会把它切成若干个16字节的块来处理。
  • 密钥长度(Key Length) :SM4的密钥长度固定为 128位(16字节) 。也就是说,你的加密密钥必须是恰好16个字符(如果按ASCII算)或经过编码后能对应16字节的数据。这和AES-128的密钥长度一致。
  • 填充(Padding) :由于数据长度不一定正好是16字节的整数倍,对于最后一个不足16字节的块,就需要进行填充。最常用的填充标准是 PKCS#7 (也叫PKCS#5)。它的规则很简单:缺几个字节,就用几来填充。例如,最后一个块还差5个字节满16字节,那么就填充5个字节的 0x05 。解密时,再根据最后一个字节的值移除填充。 sm-crypto 库默认就使用这种填充方式。

3.2 工作模式(Mode):ECB vs CBC

这是对称加密中最关键的概念之一,直接关系到加密结果的安全特性。

  • ECB(Electronic Codebook,电子密码本)模式

    • 原理 :最简单的模式。将明文分成独立的块,每个块用相同的密钥独立加密。相同的明文块一定会产生相同的密文块。
    • 优点 :简单,支持并行计算(加密解密都可以)。
    • 致命缺点 :不能隐藏数据模式。对于重复出现的明文块,会产生重复的密文块,看图类、格式固定的数据加密后,可能还能看出原始数据的轮廓, 安全性很低
    • 使用场景 :通常不推荐用于加密有意义的数据。可能在一些非常特殊、对性能要求极高且数据模式不重要的内部场景中使用。 在与外部系统对接时,除非对方明确指定,否则应避免使用ECB。
  • CBC(Cipher Block Chaining,密码分组链接)模式

    • 原理 :引入了一个 初始化向量(IV, Initialization Vector) 。在加密第一个明文块之前,先将其与一个IV进行异或(XOR)操作,然后再用密钥加密。得到的第一个密文块,又会作为“向量”与下一个明文块异或,再加密,如此链式进行下去。
    • 优点 :由于IV的引入和链式结构,即使完全相同的明文,使用不同的IV也会产生完全不同的密文。这很好地隐藏了数据模式,安全性远高于ECB。
    • 关键参数 :IV。它的长度必须等于分组大小,即 16字节 。并且,IV不需要保密,但必须是 不可预测的 (通常随机生成)。在解密时,需要使用加密时相同的IV。
    • 使用场景 这是目前最常用、也最推荐的分组加密模式 。绝大多数要求使用SM4的接口规范,默认或明确指定的都是CBC模式。

简单类比:ECB就像用同一个印章盖在所有独立的信封上,图案都一样;CBC则像是先把第一个信封涂上随机颜料再盖印,并且每个信封的印泥都会沾到下一个信封上,最终所有信封的印记都独一无二且相互关联。

4. 使用 sm-crypto 实现SM4加解密

理论铺垫完毕,现在上干货。我们会在项目根目录创建一个 sm4-util.js 工具文件。

4.1 基础加解密函数封装

首先,引入库并封装一个最基础的ECB模式加解密函数。虽然不推荐ECB,但了解其基本用法有助于理解。

// sm4-util.js
const sm4 = require('sm-crypto').sm4;

/**
 * SM4 ECB 模式加密 (PKCS#7 填充)
 * @param {string|Buffer} plaintext - 待加密的明文,字符串或Buffer
 * @param {string} key - 16字节的密钥(16个字符的字符串)
 * @param {string} [inputEncoding='utf8'] - 明文编码,如 'utf8', 'base64'
 * @param {string} [outputEncoding='base64'] - 密文输出编码,如 'base64', 'hex'
 * @returns {string} 加密后的密文字符串
 */
function encryptECB(plaintext, key, inputEncoding = 'utf8', outputEncoding = 'base64') {
  // 检查密钥长度
  if (Buffer.from(key, 'utf8').length !== 16) {
    throw new Error('SM4密钥必须为16字节(16个ASCII字符或等价的字节)');
  }

  // sm-crypto 的 sm4.encrypt 方法默认使用 ECB 模式,PKCS#7 填充
  // 它接受字符串明文和密钥,输出16进制字符串
  let cipherHex = sm4.encrypt(plaintext, key);

  // 将16进制字符串转换为指定的输出编码(如base64)
  const cipherBuffer = Buffer.from(cipherHex, 'hex');
  return cipherBuffer.toString(outputEncoding);
}

/**
 * SM4 ECB 模式解密 (PKCS#7 填充)
 * @param {string} ciphertext - 待解密的密文(base64或hex字符串)
 * @param {string} key - 16字节的密钥(必须与加密时相同)
 * @param {string} [inputEncoding='base64'] - 密文输入编码
 * @param {string} [outputEncoding='utf8'] - 明文输出编码
 * @returns {string} 解密后的明文字符串
 */
function decryptECB(ciphertext, key, inputEncoding = 'base64', outputEncoding = 'utf8') {
  if (Buffer.from(key, 'utf8').length !== 16) {
    throw new Error('SM4密钥必须为16字节');
  }

  // 将输入密文转换为16进制字符串,这是sm-crypto解密函数所期望的格式
  const cipherBuffer = Buffer.from(ciphertext, inputEncoding);
  const cipherHex = cipherBuffer.toString('hex');

  // sm-crypto 的 sm4.decrypt 方法解密
  let plaintext = sm4.decrypt(cipherHex, key);
  // 注意:sm4.decrypt 返回的是字符串,已经处理了去除填充
  return plaintext;
}

// 示例使用
const key = '0123456789abcdef'; // 正好16个字符
const plainText = 'Hello, SM4! 这是测试明文。';

try {
  const encrypted = encryptECB(plainText, key, 'utf8', 'base64');
  console.log('ECB加密结果 (Base64):', encrypted);

  const decrypted = decryptECB(encrypted, key, 'base64', 'utf8');
  console.log('ECB解密结果:', decrypted);
  console.log('解密是否成功?', decrypted === plainText);
} catch (error) {
  console.error('加解密过程出错:', error.message);
}

代码解读与注意事项:

  1. 密钥处理 :我们假设密钥是16个ASCII字符的字符串。 sm-crypto 内部会处理密钥。但务必确保密钥的字节长度是16。如果你的密钥是其他格式(比如从数据库读出的二进制),需要先转换成16字节的Buffer或对应字符串。
  2. 编码转换 sm-crypto encrypt decrypt 函数默认输入输出都是 16进制(hex)字符串 。为了更方便地与其他系统(通常使用Base64)交互,我们在封装函数里做了编码转换。 encrypt 时,将最终hex密文转成Base64; decrypt 时,先将Base64密文转回hex再解密。
  3. 错误处理 :简单的密钥长度校验是必要的。在生产环境中,你可能需要更完善的错误处理,比如捕获解密失败(可能由于密文被篡改或密钥错误)并返回友好提示。

4.2 更安全的CBC模式实现

如前所述,CBC模式才是实践中的主力。 sm-crypto 同样提供了支持。

// 继续在 sm4-util.js 中添加

/**
 * SM4 CBC 模式加密 (PKCS#7 填充)
 * @param {string|Buffer} plaintext - 待加密的明文
 * @param {string} key - 16字节的密钥
 * @param {string} iv - 16字节的初始化向量
 * @param {string} [inputEncoding='utf8'] - 明文编码
 * @param {string} [outputEncoding='base64'] - 密文输出编码
 * @returns {string} 加密后的密文字符串
 */
function encryptCBC(plaintext, key, iv, inputEncoding = 'utf8', outputEncoding = 'base64') {
  if (Buffer.from(key, 'utf8').length !== 16) {
    throw new Error('SM4密钥必须为16字节');
  }
  if (Buffer.from(iv, 'utf8').length !== 16) {
    throw new Error('SM4 CBC模式IV必须为16字节');
  }

  // sm-crypto 的 sm4.encrypt 方法,当传入第三个参数时,表示使用CBC模式,且参数为IV
  let cipherHex = sm4.encrypt(plaintext, key, {
    iv: iv,
    mode: 'cbc', // 明确指定模式,虽然传iv后默认就是cbc
    padding: 'pkcs#7' // 默认就是pkcs#7,可省略
  });

  const cipherBuffer = Buffer.from(cipherHex, 'hex');
  return cipherBuffer.toString(outputEncoding);
}

/**
 * SM4 CBC 模式解密 (PKCS#7 填充)
 * @param {string} ciphertext - 待解密的密文
 * @param {string} key - 16字节的密钥
 * @param {string} iv - 16字节的初始化向量(必须与加密时相同)
 * @param {string} [inputEncoding='base64'] - 密文输入编码
 * @param {string} [outputEncoding='utf8'] - 明文输出编码
 * @returns {string} 解密后的明文字符串
 */
function decryptCBC(ciphertext, key, iv, inputEncoding = 'base64', outputEncoding = 'utf8') {
  if (Buffer.from(key, 'utf8').length !== 16) {
    throw new Error('SM4密钥必须为16字节');
  }
  if (Buffer.from(iv, 'utf8').length !== 16) {
    throw new Error('SM4 CBC模式IV必须为16字节');
  }

  const cipherBuffer = Buffer.from(ciphertext, inputEncoding);
  const cipherHex = cipherBuffer.toString('hex');

  let plaintext = sm4.decrypt(cipherHex, key, {
    iv: iv,
    mode: 'cbc',
    padding: 'pkcs#7'
  });

  return plaintext;
}

// 示例使用CBC模式
const cbcKey = '1234567890abcdef'; // 16字节密钥
const cbcIv = 'fedcba0987654321';   // 16字节IV,应该是随机值,这里为演示固定
const cbcPlainText = '这是一段使用CBC模式加密的重要数据。';

try {
  const cbcEncrypted = encryptCBC(cbcPlainText, cbcKey, cbcIv, 'utf8', 'base64');
  console.log('\nCBC加密结果 (Base64):', cbcEncrypted);

  const cbcDecrypted = decryptCBC(cbcEncrypted, cbcKey, cbcIv, 'base64', 'utf8');
  console.log('CBC解密结果:', cbcDecrypted);
  console.log('CBC解密是否成功?', cbcDecrypted === cbcPlainText);
} catch (error) {
  console.error('CBC加解密过程出错:', error.message);
}

关于IV的致命细节:

  • IV必须是随机的、不可预测的 。上面示例中使用固定IV是为了演示, 在实际生产环境中绝对不可以这样做! 否则会丧失CBC模式的安全优势。每次加密都应该使用一个密码学安全的随机数生成器(CSPRNG)来生成新的IV。
  • IV不需要保密,但必须唯一 。通常,IV会随密文一起传输或存储。常见的做法是:生成一个16字节的随机IV,将它放在密文的前面(或后面),一起发给接收方。接收方先分离出IV,再用它和密钥解密。
  • Node.js中可以使用 crypto.randomBytes(16) 来生成安全的随机IV。

4.3 完整工具类与安全实践

结合安全实践,我们完善这个工具类,加入随机IV生成和IV与密文的组合/分离逻辑。

// sm4-util.js (完整版示例)
const sm4 = require('sm-crypto').sm4;
const crypto = require('crypto'); // 使用Node.js原生crypto生成安全随机数

class SM4Util {
  /**
   * 生成一个随机的16字节IV(初始化向量)
   * @returns {string} 返回16字节的16进制字符串表示的IV
   */
  static generateIV() {
    // crypto.randomBytes 是密码学安全的随机数生成器
    return crypto.randomBytes(16).toString('hex'); // 生成32位hex字符串,对应16字节
  }

  /**
   * SM4 CBC 加密,并将IV预置于密文前一起输出
   * @param {string} plaintext - 明文
   * @param {string} key - 16字节密钥(utf8字符串)
   * @param {string} [outputEncoding='base64'] - 最终组合输出的编码
   * @returns {string} 格式:IV(hex) + 密文数据。整体按outputEncoding编码。
   */
  static encryptCBCWithIV(plaintext, key, outputEncoding = 'base64') {
    if (Buffer.from(key, 'utf8').length !== 16) {
      throw new Error('SM4密钥必须为16字节');
    }

    // 1. 生成随机IV
    const ivHex = this.generateIV(); // 例如 '7f3a8b1c...' (32字符)
    const ivBuffer = Buffer.from(ivHex, 'hex');

    // 2. 使用该IV进行CBC加密
    const cipherHex = sm4.encrypt(plaintext, key, {
      iv: ivBuffer.toString('utf8'), // sm-crypto的IV参数需要字符串形式
      mode: 'cbc',
    });

    // 3. 将IV(hex)和密文(hex)拼接
    //    注意:IV已经是hex字符串,密文cipherHex也是hex字符串
    const combinedHex = ivHex + cipherHex; // 拼接,IV在前

    // 4. 将拼接后的hex字符串转换为最终输出格式
    const combinedBuffer = Buffer.from(combinedHex, 'hex');
    return combinedBuffer.toString(outputEncoding);
  }

  /**
   * SM4 CBC 解密,从组合数据中分离IV
   * @param {string} combinedCiphertext - 加密函数输出的组合密文
   * @param {string} key - 16字节密钥
   * @param {string} [inputEncoding='base64'] - 组合密文的输入编码
   * @returns {string} 解密后的明文
   */
  static decryptCBCWithIV(combinedCiphertext, key, inputEncoding = 'base64') {
    if (Buffer.from(key, 'utf8').length !== 16) {
      throw new Error('SM4密钥必须为16字节');
    }

    // 1. 将输入解码为Buffer
    const combinedBuffer = Buffer.from(combinedCiphertext, inputEncoding);
    const combinedHex = combinedBuffer.toString('hex');

    // 2. 分离IV和密文。IV固定占32个hex字符(16字节 * 2)
    const ivHex = combinedHex.substring(0, 32);
    const cipherHex = combinedHex.substring(32);

    const ivBuffer = Buffer.from(ivHex, 'hex');

    // 3. 使用分离出的IV进行解密
    const plaintext = sm4.decrypt(cipherHex, key, {
      iv: ivBuffer.toString('utf8'),
      mode: 'cbc',
    });

    return plaintext;
  }

  // 保留之前的encryptECB, decryptECB, encryptCBC, decryptCBC等函数...
}

// 使用示例
const secureKey = 'my-secret-key-16'; // 确保16字节

const sensitiveData = '用户身份证号:110101199001011234';
console.log('\n--- 安全CBC加密示例 ---');
console.log('原始数据:', sensitiveData);

const encryptedPacket = SM4Util.encryptCBCWithIV(sensitiveData, secureKey, 'base64');
console.log('加密后数据包(Base64):', encryptedPacket);

const decryptedData = SM4Util.decryptCBCWithIV(encryptedPacket, secureKey, 'base64');
console.log('解密后数据:', decryptedData);
console.log('验证:', decryptedData === sensitiveData);

// 重要:验证每次加密结果都不同(因为IV随机)
console.log('\n--- 验证IV随机性 ---');
const enc1 = SM4Util.encryptCBCWithIV('同一段话', secureKey);
const enc2 = SM4Util.encryptCBCWithIV('同一段话', secureKey);
console.log('加密结果1:', enc1.substring(0, 30) + '...');
console.log('加密结果2:', enc2.substring(0, 30) + '...');
console.log('两次加密结果是否相同?', enc1 === enc2); // 应该是 false

这个 SM4Util 类提供了一个更贴近生产环境的实践。 encryptCBCWithIV 方法将随机IV和密文捆绑输出,解密方只需用同一个密钥和对应方法即可还原,无需单独管理IV的传输。

5. 对接外部系统的实战要点

当你需要与第三方平台(如支付机构、政府平台)对接时,他们通常会提供一份详细的接口文档。以下是你需要重点关注和核对的点,也是我踩过坑的地方:

5.1 文档核对清单

  1. 算法标准 :确认是 SM4 ,而不是SM2(非对称)或SM3(哈希)。
  2. 工作模式 99%是CBC模式 ,但务必确认。如果是ECB,需要评估其安全性是否可接受(通常不可接受)。
  3. 密钥(Key)
    • 长度 :必须是16字节。文档可能会直接给出一个16字符的字符串,或者一个Base64/Hex编码的密钥,你需要解码后确认其字节长度。
    • 格式 :明确密钥的编码格式(UTF-8字符串?Hex?Base64?)。你的代码中加载密钥时需要匹配这个格式。
  4. 初始化向量(IV)
    • 长度 :必须是16字节。
    • :文档可能指定一个固定的IV(例如全零),也可能要求你使用随机IV并告知传递方式。 如果对方要求固定IV(如 0000000000000000 ),务必严格按照要求来,即使这降低了安全性。
  5. 填充(Padding) 几乎都是PKCS#7 。但需要确认,有些老旧系统可能用其他填充(如ZeroPadding)。
  6. 数据编码
    • 明文 :加密前,明文是什么编码?通常是UTF-8。
    • 密文输出 :对方要求你提供什么格式的密文? 绝大多数是Base64 ,也可能是Hex。你的加密函数输出必须匹配。
    • IV传递 :如果IV不固定,IV如何传递?是作为参数单独传,还是像我们上面那样预置在密文前?或者放在HTTP头里?必须严格按文档来。
  7. 字符集 :特别是中文,确保双方都使用 UTF-8 编码,避免出现乱码。

5.2 联调测试与问题排查

对接时,最有效的方法就是做加解密对齐测试。

  1. 向对方索要测试向量 :请求他们提供一组明确的测试数据:明文、密钥、IV(如果是CBC)、以及他们计算出的标准密文(Base64/Hex)。用你的代码加密同样的明文,看结果是否一致。
  2. 使用在线工具辅助验证(谨慎) :可以搜索“SM4在线加密”等关键词,找到一些网页工具。 注意:绝对不要用这些工具处理真实的敏感数据! 仅用于输入测试向量,验证你的代码结果与在线工具(或对方标准)是否一致,帮助定位问题是出在密钥、IV、编码还是模式上。
  3. 分步调试
    • 检查密钥和IV的字节 :用 Buffer.from(key, '指定编码').length 确认长度是否为16。
    • 检查中间结果 :在调用 sm4.encrypt 之前,打印出明文、密钥、IV的Hex或Base64值,与对方提供的测试数据比对。
    • 关注编码转换 sm-crypto 内部使用Hex,你与外部系统交互用Base64。确保在 Buffer.toString() Buffer.from() 时指定的编码正确无误。这是最容易出错的地方。

6. 生产环境部署与性能考量

6.1 密钥管理

千万不能把密钥硬编码在代码里! 这是安全大忌。正确的做法是:

  • 使用环境变量( process.env.SM4_KEY )。
  • 使用配置中心(如Consul, Apollo)。
  • 使用云服务商的密钥管理服务(如AWS KMS, 阿里云KMS)。可以将加密后的密钥放在环境变量中,运行时通过KMS解密。

在我们的工具类中,密钥应从外部注入:

// config.js 或 在应用启动时加载
require('dotenv').config(); // 从 .env 文件加载

const SM4_KEY = process.env.SM4_KEY;
if (!SM4_KEY || Buffer.from(SM4_KEY, 'utf8').length !== 16) {
  throw new Error('未配置或配置无效的SM4密钥,请检查SM4_KEY环境变量');
}
module.exports = { SM4_KEY };

6.2 错误处理与日志

在生产代码中,加解密函数应该被完善的try-catch包裹,并且记录适当的日志(注意不要记录明文或密钥本身)。

async function secureEncrypt(userData) {
  try {
    const encrypted = SM4Util.encryptCBCWithIV(userData, SM4_KEY, 'base64');
    return { success: true, data: encrypted };
  } catch (error) {
    console.error(`[SM4加密失败] 用户ID: xxx, 错误: ${error.message}`);
    // 监控上报
    // Sentry.captureException(error);
    return { success: false, message: '数据加密处理失败', code: 'ENCRYPT_ERROR' };
  }
}

6.3 性能与流式处理

对于大量数据或流式数据(如加密大文件),上述一次性加密解密的方式可能占用较多内存。 sm-crypto 目前似乎没有提供流式API。如果遇到性能瓶颈,可以考虑:

  1. 将大数据分块加密(需注意CBC模式块之间的依赖)。
  2. 评估是否可以使用更快的原生绑定库(但这会引入编译依赖,如 node-gm-crypto ,需权衡部署复杂度)。
  3. 对于CPU密集型的加解密操作,可以考虑放入Worker线程,避免阻塞Node.js主事件循环。

7. 常见问题与排查技巧实录

以下是我在开发和联调过程中遇到的一些典型问题及解决方法:

问题1:解密失败,报错“Invalid character”或“Padding error”。

  • 排查思路
    1. 检查密文编码 :确保解密时输入的密文编码与加密时输出的编码一致。比如加密输出Base64,解密输入也必须是Base64。一个常见错误是加密后得到了Hex字符串,却用Base64去解密。
    2. 检查密钥和IV :确认解密使用的密钥和IV与加密时 完全一致 ,包括大小写和不可见字符。建议在调试时,将密钥和IV的Hex或Base64值打印出来比对。
    3. 检查数据是否被篡改 :在网络传输或存储过程中,密文字符串是否被截断、添加了空格或换行符?URL传输时,Base64中的 + / 是否被错误转义?确保收到的密文是“干净”的。
    4. 核对填充方式 :极少数情况下,对方系统可能使用了不同的填充方式(如ZeroPadding),而 sm-crypto 默认是PKCS#7。需要查阅对方文档或联系确认。

问题2:加密后的Base64字符串,通过网络传输后对方解密失败。

  • 排查思路
    1. URL安全处理 :标准的Base64包含 + / = ,这些字符在URL中可能有特殊含义。如果通过URL参数或表单传递,需要对Base64进行URL安全处理(将 + 换成 - / 换成 _ ,并去掉末尾的 = )。双方需要约定好是否进行这种转换。Node.js中可以用:
      function base64ToUrlSafe(base64Str) {
        return base64Str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
      }
      function urlSafeToBase64(urlSafeStr) {
        let str = urlSafeStr.replace(/-/g, '+').replace(/_/g, '/');
        // 补上可能缺失的等号
        while (str.length % 4) {
          str += '=';
        }
        return str;
      }
      
    2. 字符集问题 :确保整个链路(你的Node.js服务、中间件、对方系统)都使用UTF-8编码处理这些字符串。

问题3:在Docker或某些Linux服务器上运行报错,但在本地Windows/Mac正常。

  • 排查思路
    1. Node.js版本 :确保服务器Node.js版本与本地开发环境一致或兼容。 sm-crypto 作为纯JS库,版本兼容性通常很好,但不同Node.js版本在Buffer等API上可能有细微差别。
    2. 编码一致性 :不同系统对默认字符集的解释可能不同。在所有涉及到字符串和Buffer转换的地方, 显式地指定编码 (如 'utf8' , 'base64' ),不要依赖默认值。

问题4:如何加密一个JSON对象?

  • 方法 :先将JSON对象序列化成字符串( JSON.stringify ),然后加密这个字符串。解密后得到字符串,再反序列化( JSON.parse )。
    const data = { userId: 12345, name: '张三' };
    const plainText = JSON.stringify(data);
    const encrypted = encryptCBC(plainText, key, iv);
    // 传输 encrypted...
    // 接收方解密后
    const decryptedStr = decryptCBC(encrypted, key, iv);
    const decryptedData = JSON.parse(decryptedStr);
    

最后,再强调一下最关键的心得: 对接外部系统,文档就是法律 。每一个参数、每一个编码、每一个步骤都必须与文档严格对齐。自己写一个完整的测试用例,用对方提供的示例数据从头跑到尾,确保结果分毫不差,这是顺利联调的唯一捷径。

更多推荐