Node.js国密SM4加解密实战:从原理到支付对接完整指南
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 这个库。理由很实在:
- 功能专注且全面 :它专注于国密算法,不仅支持SM2、SM3、SM4,而且对SM4的支持很完整,涵盖了ECB、CBC等常用模式,以及PKCS#5/PKCS#7填充。
- 纯JavaScript实现 :这意味着它不依赖任何本地编译的模块(比如C++插件)。在部署时,尤其是在一些受限的服务器环境或者Serverless(FaaS)环境下,你不需要操心Node-gyp编译、系统库缺失这些令人头疼的问题,
npm install之后直接就能用,兼容性极好。 - 文档和社区相对较好 :它的GitHub仓库star数较多,Issues里反馈的问题和解决方案也比较活跃,遇到问题更容易找到线索。
- 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);
}
代码解读与注意事项:
- 密钥处理 :我们假设密钥是16个ASCII字符的字符串。
sm-crypto内部会处理密钥。但务必确保密钥的字节长度是16。如果你的密钥是其他格式(比如从数据库读出的二进制),需要先转换成16字节的Buffer或对应字符串。 - 编码转换 :
sm-crypto的encrypt和decrypt函数默认输入输出都是 16进制(hex)字符串 。为了更方便地与其他系统(通常使用Base64)交互,我们在封装函数里做了编码转换。encrypt时,将最终hex密文转成Base64;decrypt时,先将Base64密文转回hex再解密。 - 错误处理 :简单的密钥长度校验是必要的。在生产环境中,你可能需要更完善的错误处理,比如捕获解密失败(可能由于密文被篡改或密钥错误)并返回友好提示。
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 文档核对清单
- 算法标准 :确认是 SM4 ,而不是SM2(非对称)或SM3(哈希)。
- 工作模式 : 99%是CBC模式 ,但务必确认。如果是ECB,需要评估其安全性是否可接受(通常不可接受)。
- 密钥(Key) :
- 长度 :必须是16字节。文档可能会直接给出一个16字符的字符串,或者一个Base64/Hex编码的密钥,你需要解码后确认其字节长度。
- 格式 :明确密钥的编码格式(UTF-8字符串?Hex?Base64?)。你的代码中加载密钥时需要匹配这个格式。
- 初始化向量(IV) :
- 长度 :必须是16字节。
- 值 :文档可能指定一个固定的IV(例如全零),也可能要求你使用随机IV并告知传递方式。 如果对方要求固定IV(如
0000000000000000),务必严格按照要求来,即使这降低了安全性。
- 填充(Padding) : 几乎都是PKCS#7 。但需要确认,有些老旧系统可能用其他填充(如ZeroPadding)。
- 数据编码 :
- 明文 :加密前,明文是什么编码?通常是UTF-8。
- 密文输出 :对方要求你提供什么格式的密文? 绝大多数是Base64 ,也可能是Hex。你的加密函数输出必须匹配。
- IV传递 :如果IV不固定,IV如何传递?是作为参数单独传,还是像我们上面那样预置在密文前?或者放在HTTP头里?必须严格按文档来。
- 字符集 :特别是中文,确保双方都使用 UTF-8 编码,避免出现乱码。
5.2 联调测试与问题排查
对接时,最有效的方法就是做加解密对齐测试。
- 向对方索要测试向量 :请求他们提供一组明确的测试数据:明文、密钥、IV(如果是CBC)、以及他们计算出的标准密文(Base64/Hex)。用你的代码加密同样的明文,看结果是否一致。
- 使用在线工具辅助验证(谨慎) :可以搜索“SM4在线加密”等关键词,找到一些网页工具。 注意:绝对不要用这些工具处理真实的敏感数据! 仅用于输入测试向量,验证你的代码结果与在线工具(或对方标准)是否一致,帮助定位问题是出在密钥、IV、编码还是模式上。
- 分步调试 :
- 检查密钥和IV的字节 :用
Buffer.from(key, '指定编码').length确认长度是否为16。 - 检查中间结果 :在调用
sm4.encrypt之前,打印出明文、密钥、IV的Hex或Base64值,与对方提供的测试数据比对。 - 关注编码转换 :
sm-crypto内部使用Hex,你与外部系统交互用Base64。确保在Buffer.toString()和Buffer.from()时指定的编码正确无误。这是最容易出错的地方。
- 检查密钥和IV的字节 :用
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。如果遇到性能瓶颈,可以考虑:
- 将大数据分块加密(需注意CBC模式块之间的依赖)。
- 评估是否可以使用更快的原生绑定库(但这会引入编译依赖,如
node-gm-crypto,需权衡部署复杂度)。 - 对于CPU密集型的加解密操作,可以考虑放入Worker线程,避免阻塞Node.js主事件循环。
7. 常见问题与排查技巧实录
以下是我在开发和联调过程中遇到的一些典型问题及解决方法:
问题1:解密失败,报错“Invalid character”或“Padding error”。
- 排查思路 :
- 检查密文编码 :确保解密时输入的密文编码与加密时输出的编码一致。比如加密输出Base64,解密输入也必须是Base64。一个常见错误是加密后得到了Hex字符串,却用Base64去解密。
- 检查密钥和IV :确认解密使用的密钥和IV与加密时 完全一致 ,包括大小写和不可见字符。建议在调试时,将密钥和IV的Hex或Base64值打印出来比对。
- 检查数据是否被篡改 :在网络传输或存储过程中,密文字符串是否被截断、添加了空格或换行符?URL传输时,Base64中的
+和/是否被错误转义?确保收到的密文是“干净”的。 - 核对填充方式 :极少数情况下,对方系统可能使用了不同的填充方式(如ZeroPadding),而
sm-crypto默认是PKCS#7。需要查阅对方文档或联系确认。
问题2:加密后的Base64字符串,通过网络传输后对方解密失败。
- 排查思路 :
- 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; } - 字符集问题 :确保整个链路(你的Node.js服务、中间件、对方系统)都使用UTF-8编码处理这些字符串。
- URL安全处理 :标准的Base64包含
问题3:在Docker或某些Linux服务器上运行报错,但在本地Windows/Mac正常。
- 排查思路 :
- Node.js版本 :确保服务器Node.js版本与本地开发环境一致或兼容。
sm-crypto作为纯JS库,版本兼容性通常很好,但不同Node.js版本在Buffer等API上可能有细微差别。 - 编码一致性 :不同系统对默认字符集的解释可能不同。在所有涉及到字符串和Buffer转换的地方, 显式地指定编码 (如
'utf8','base64'),不要依赖默认值。
- Node.js版本 :确保服务器Node.js版本与本地开发环境一致或兼容。
问题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);
最后,再强调一下最关键的心得: 对接外部系统,文档就是法律 。每一个参数、每一个编码、每一个步骤都必须与文档严格对齐。自己写一个完整的测试用例,用对方提供的示例数据从头跑到尾,确保结果分毫不差,这是顺利联调的唯一捷径。
更多推荐
所有评论(0)