news 2026/10/23 5:43:50

HTML页面国密SM4加密解密:纯JS实现与联调避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HTML页面国密SM4加密解密:纯JS实现与联调避坑指南

简介:一份面向网页前端开发者的国密加密解密演示资源,压缩包内共有两个文件,分别是一个网页页面和一个脚本文件,整体体积约五KB,非常轻量。页面完整展示了国密加密算法在密码块链接模式与电子密码本模式两种方式下的加解密流程,脚本则封装了核心算法逻辑,支持自定义密钥与初始化向量,开发者可在浏览器中直接运行体验,也能方便地提取代码嵌入实际项目。对于涉及用户敏感信息传输的前端场景,或希望理解国密算法实现细节的学习者,这套资源提供了直观的对照参考。通过页面可以清晰看到同一明文在不同加密模式、不同密钥和初始化向量作用下的密文变化,进而体会密码块链接模式前后密文关联带来的更强安全性,以及电子密码本模式实现简单但安全性相对较弱的特点。目前已有六百零六人浏览学习,适合需要快速上手前端国密加密、或寻找轻量级改造样例的开发者。

1. 网页里到底需不需要国密 SM4:先说结论再动手

做 html 页面国密 SM4 加密解密,通常不是开发者自己想出来的需求,而是后端或对接方指定:接口字段要加密、内部系统要过国密改造、或者某个平台只认 SM4。这时候前端就得在浏览器里用 JavaScript 把数据加密成密文,再交给后端去解;反过来,后端返回的密文也要在页面里解出来用。很多人第一反应是找个现成库,结果加密出来后端解不了,或者自己加完密自己都解不开。

这篇文章的目标就是把「HTML 页面国密 SM4 加密解密」这条路完整走一遍:先讲清楚 SM4 的分组、密钥、填充和模式这些必须知道的点,再给一份纯 JavaScript 可运行的实现,最后落到一个能直接打开跑的最小 HTML demo,以及联调时最容易出问题的几个坑。每段代码都能复制,每个参数我都会说明为什么这么设。

适合谁看:被要求做前端国密改造的 Web 开发者、自研系统里需要前后端 SM4 联调的团队成员。也要先泼一盆冷水:前端加密的密钥对使用者是透明的,SM4 在页面里解决的是「传输字段不被明文抓包、满足对接方算法要求」,它不是 HTTPS 的替代品,更不是把密钥写死在 JS 里就算安全。

2. SM4 算法基础与模式选型:先看懂分组和填充再写代码

2.1 128 比特分组、32 轮迭代:SM4 的算法骨架

SM4 是国密标准里的分组密码算法,结构上对齐 AES 的思路,但参数非常固定:分组长度固定 128 比特(16 字节),密钥长度固定 128 比特(16 字节),加密迭代 32 轮。这对前端开发者反而是好事,因为没有 AES-192/256 那种长度选项,不容易选错。

算法的核心是每一轮用一个轮密钥参与运算,把当前 4 个 32 位字重新组合。轮函数可以简化理解为:后一个字等于最前面的字异或上一个经过 S 盒非线性替换和线性变换的值。S 盒负责把 8 位映射成另外 8 位,线性变换负责把 32 位字做循环左移后混合。

对写代码的人来说,真正需要记住的只有两点:第一,加解密过程完全对称,解密就是把 32 个轮密钥倒过来用同一套轮函数;第二,轮密钥由主密钥经过密钥扩展生成,扩展过程里用到的变换和解密轮的线性变换是同一个。这两点理解了,后面看代码就不会觉得是黑匣子。

2.2 ECB 还是 CBC:根据密文可见性选模式

SM4 本身只定义了单块加密,怎么把多块数据串起来由模式决定。最常见的两个是 ECB 和 CBC。

ECB 模式下每个 16 字节块独立加密,互不影响。优点是简单、可以并行、加解密速度快;缺点是同一把密钥下,相同的明文块必然产生相同的密文块。如果加密的是结构化数据,比如 JSON 里大量相同字段值,密文会出现可识别的重复模式。

CBC 模式先用前一个密文块和当前明文块异或,再加密,第一个块则需要一个初始向量 IV。相同明文块在不同位置会被不同密文块掩盖,安全性明显更好,代价是块之间有依赖,不能并行,而且 IV 的生成规则前后端必须对齐。

对比项ECBCBC
是否依赖 IV不需要需要 16 字节 IV
相同明文块密文是否相同相同,特征会泄漏不同
能否并行加解密可以不可以
典型场景单块数据、密钥加密多块业务数据、长文本

我一般会直接建议用 CBC。ECB 模式留着的唯一理由是对端系统已经固定了协议没法改,否则没必要给自己埋一个数据特征泄漏的雷。

2.3 PKCS#7 填充和 UTF-8:最容易翻车的地方

分组密码要求明文长度必须是 16 的整数倍,不够就要填充。通用做法是 PKCS#7:差 n 字节补 n 个值为 n 的字节。比如明文 10 字节,差 6 个字节,就补 6 个 0x06;如果明文恰好是 16 字节的整数倍,仍然要补一整块 16 个 0x10,不然解密时不知道末尾有没有填充。这个「整块补」是新手最容易忘的玄学点。

function pkcs7Pad(data) { const padLen = 16 - (data.length % 16); const out = new Uint8Array(data.length + padLen); out.set(data); for (let i = data.length; i < out.length; i++) out[i] = padLen; return out; } function pkcs7Unpad(data) { const padLen = data[data.length - 1]; if (padLen < 1 || padLen > 16) throw new Error('padding 不合法'); for (let i = data.length - padLen; i < data.length; i++) { if (data[i] !== padLen) throw new Error('padding 内容不合法'); } return data.slice(0, data.length - padLen); }

padLen 的计算把长度补到 16 的倍数,解密时直接读最后一个字节就知道补了多少,再去校验每个填充字节是否一致。为什么不用 ZeroPadding?因为二进制数据末尾本身可能有 0x00,解出来无法判断哪些是填充,哪些是原文,信息会丢。

另一个必踩点是编码。JS 里字符串和字节数组互转必须用 TextEncoder / TextDecoder,默认走 UTF-8,和 Java、Go、Python 后端的 String.getBytes() 语义对齐。别用 String.fromCharCode 逐个转,中文会直接变乱码。

3. 纯 JS 实现国密 SM4:从 S 盒到模式封装的完整代码

3.1 S 盒、循环左移与 tau 变换

SM4 的 S 盒是标准里写死的 256 字节表,没有推导逻辑,只能整表抄。下面是完整常量,如果和后端测试向量对不上,第一个怀疑对象就是这里抄错位。

const SBOX = [ 0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05, 0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99, 0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62, 0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6, 0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8, 0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35, 0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87, 0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e, 0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1, 0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3, 0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f, 0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51, 0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8, 0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0, 0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84, 0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48 ]; // 32 位循环左移,>>> 0 保证结果按无符号处理 function rotl(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; } // tau:32 位字拆成 4 个字节,分别过 S 盒,再拼回 32 位 function tau(a) { return ( (SBOX[(a >>> 24) & 0xff] << 24) | (SBOX[(a >>> 16) & 0xff] << 16) | (SBOX[(a >>> 8) & 0xff] << 8) | SBOX[a & 0xff] ) >>> 0; } // 加密轮线性变换 L function L(b) { return (b ^ rotl(b, 2) ^ rotl(b, 10) ^ rotl(b, 18) ^ rotl(b, 24)) >>> 0; } // 密钥扩展用的线性变换 L',同时用于解密轮 function Lp(b) { return (b ^ rotl(b, 13) ^ rotl(b, 23)) >>> 0; }

rotl 的关键是右侧用>>> (32 - n)而不是>>,否则符号位会扩散。tau 里先用>>> 24取最高字节,再跟 0xff 与一下,保证取到的是一个 0 到 255 的索引,S 盒替换完再按各自位置移回去。L 和 L' 的差异只有循环左移的位数,加密轮用 L,密钥扩展和解密轮用 L',这是 SM4 能一套代码双向复用的基础。

3.2 密钥扩展:FK 常量与 32 个轮密钥

主密钥 16 字节先拆成 4 个 32 位字,分别异或固定常量 FK,再迭代 32 次生成轮密钥 rk。每一轮的输入是前 4 个字和当前 CK 常量,输出通过 L'(tau(...)) 计算。

const FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]; // 生成 32 个 CK 常量:第 i 个 CK 的 4 字节分别是 (4i+j)*7 的低 8 位 function genCK() { const ck = []; for (let i = 0; i < 32; i++) { let c = 0; for (let j = 0; j < 4; j++) { const b = ((4 * i + j) * 7) & 0xff; c = ((c << 8) | b) >>> 0; } ck.push(c | 0); } return ck; } // 密钥扩展,keyBytes 必须是 16 字节的 Uint8Array function expandKey(keyBytes) { if (keyBytes.length !== 16) throw new Error('SM4 密钥必须为 16 字节'); const CK = genCK(); const k = new Int32Array(36); for (let i = 0; i < 4; i++) { const mk = ( (keyBytes[i * 4] << 24) | (keyBytes[i * 4 + 1] << 16) | (keyBytes[i * 4 + 2] << 8) | keyBytes[i * 4 + 3] ) | 0; k[i] = mk ^ FK[i]; } const rk = new Int32Array(32); for (let i = 0; i < 32; i++) { const t = (k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ CK[i]) | 0; k[i + 4] = (k[i] ^ Lp(tau(t))) | 0; rk[i] = k[i + 4]; } return rk; }
// 单块加解密:block 16 字节,rk 是轮密钥数组;解密时传入反转后的 rk function cryptBlock(block, rk) { const x = new Int32Array(36); for (let i = 0; i < 4; i++) { x[i] = ( (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | (block[i * 4 + 2] << 8) | block[i * 4 + 3] ) | 0; } for (let i = 0; i < 32; i++) { x[i + 4] = (x[i] ^ L(tau(x[i + 1] ^ x[i + 2] ^ x[i + 3] ^ rk[i]))) | 0; } const out = new Uint8Array(16); for (let i = 0; i < 4; i++) { const w = x[32 + i] | 0; out[i * 4] = (w >>> 24) & 0xff; out[i * 4 + 1] = (w >>> 16) & 0xff; out[i * 4 + 2] = (w >>> 8) & 0xff; out[i * 4 + 3] = w & 0xff; } return out; }

expandKey 开头加长度校验,密钥传错时错误在加密阶段就抛出来,而不是等到联调才发现。CK 常量不用背,用(4i+j)*7的低 8 位生成,标准里就是这么定义的。cryptBlock 里注意| 0把每次运算结果转回 32 位有符号整数,JS 里位运算默认就是 32 位,但在移位组装字节时,显式| 0能避免隐式转换带来的意外结果。

3.3 ECB/CBC 封装、PKCS#7 与字节转换

有了单块加密,ECB 就是块与块之间毫无关联地依次处理。CBC 则要在每块加密前异或 IV 或上一块密文。

// ECB 模式;isDecrypt 为 true 时反转轮密钥,并对结果做 unpad function sm4Ecb(data, key, isDecrypt) { const rk = expandKey(key); const rounds = isDecrypt ? rk.slice().reverse() : rk; const input = isDecrypt ? data : pkcs7Pad(data); const out = new Uint8Array(input.length); for (let i = 0; i < input.length; i += 16) { const blk = cryptBlock(input.subarray(i, i + 16), rounds); out.set(blk, i); } return isDecrypt ? pkcs7Unpad(out) : out; } // CBC 模式;ivBytes 必须是 16 字节 function sm4Cbc(data, key, ivBytes, isDecrypt) { const rk = expandKey(key); const rounds = isDecrypt ? rk.slice().reverse() : rk; const input = isDecrypt ? data : pkcs7Pad(data); const out = new Uint8Array(input.length); const iv = ivBytes.slice(); for (let i = 0; i < input.length; i += 16) { const blk = input.subarray(i, i + 16); if (isDecrypt) { const plain = cryptBlock(blk, rounds); for (let j = 0; j < 16; j++) out[i + j] = plain[j] ^ iv[j]; iv.set(blk); // 当前密文块成为下一块的 IV } else { const xored = new Uint8Array(16); for (let j = 0; j < 16; j++) xored[j] = blk[j] ^ iv[j]; const cipher = cryptBlock(xored, rounds); out.set(cipher, i); iv.set(cipher); } } return isDecrypt ? pkcs7Unpad(out) : out; }

最后是字符串、hex、base64 的转换工具,联调时输出格式全看这些函数。

function utf8ToBytes(str) { return new TextEncoder().encode(str); } function bytesToUtf8(bytes) { return new TextDecoder().decode(bytes); } function bytesToHex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } function hexToBytes(hex) { if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2) throw new Error('hex 格式不正确'); const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16); return out; } function bytesToBase64(bytes) { let s = ''; bytes.forEach(b => s += String.fromCharCode(b)); return btoa(s); } function base64ToBytes(b64) { const s = atob(b64); const out = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); return out; }

3.4 用标准向量自检:跑通才敢联调

写完后第一件事不是接页面,而是跑标准测试向量。SM4 标准附录里有一组固定输入输出,用它能证明算法实现是对的,而不只是「自己加自己解看起来对」。

const key = hexToBytes('0123456789abcdeffedcba9876543210'); const pt = hexToBytes('0123456789abcdeffedcba9876543210'); const ct = sm4Ecb(pt, key, false); console.log(bytesToHex(ct)); // 期望输出:681edf34d206965e86b3e94f536e4246

把这段贴到浏览器控制台或 Node 里跑,输出和期望一致,说明 S 盒、密钥扩展、轮函数三个环节都对了。向量对不上时,按顺序排查:先看 S 盒有没有抄错行,再看 tau 取字节的移位方向,最后看 expandKey 里用的是 Lp 还是 L。这三个位置是纯 JS 实现 SM4 最容易写错的地方。

4. 在 HTML 页面里跑通 SM4:一个能直接用的最小 demo

4.1 页面结构:输入区、模式选择与按钮

下面这个单文件 HTML 不需要任何依赖,把第 3 章的 SM4 实现完整复制进 script 标签就能双击打开跑。页面里放了密钥、IV、模式、明文、密文五个输入区,加密结果以 hex 形式展示。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>HTML 页面国密 SM4 加解密 demo</title> <style> body { font-family: system-ui, sans-serif; max-width: 720px; margin: 32px auto; padding: 0 12px; } textarea, input, select { box-sizing: border-box; width: 100%; margin: 4px 0 12px; } textarea { min-height: 72px; } button { margin-right: 8px; padding: 6px 16px; } </style> </head> <body> <h2>HTML 页面国密 SM4 加解密</h2> <label>密钥(32 位 hex,即 16 字节)</label> <input id="key" value="0123456789abcdeffedcba9876543210"> <label>IV(32 位 hex,CBC 模式使用)</label> <input id="iv" value="00000000000000000000000000000000"> <label>工作模式</label> <select id="mode"> <option value="ecb">ECB</option> <option value="cbc">CBC</option> </select> <label>明文</label> <textarea id="plain">国密 SM4 前端加解密测试:hello, 中文内容。</textarea> <label>密文(hex)</label> <textarea id="cipher" placeholder="加密后这里显示 hex 密文"></textarea> <button id="encBtn">加密</button> <button id="decBtn">解密</button> </body> </html>

textarea 和 input 都设成 100% 宽度,便于观察长密文。直接把密钥和 IV 的默认值写成标准向量里的值,打开页面第一件事就是点解密、点加密各自验证一次,确认页面脚本没粘错。

4.2 事件绑定与加解密参数说明

脚本部分要插到 body 末尾的 script 标签里,逻辑很直白:加密时读明文转字节,按模式调用 sm4Ecb 或 sm4Cbc,结果转 hex 写回密文框;解密时反过来。

document.getElementById('encBtn').addEventListener('click', () => { try { const key = hexToBytes(document.getElementById('key').value.trim()); const iv = hexToBytes(document.getElementById('iv').value.trim()); const mode = document.getElementById('mode').value; const data = utf8ToBytes(document.getElementById('plain').value); const encrypted = mode === 'ecb' ? sm4Ecb(data, key, false) : sm4Cbc(data, key, iv, false); document.getElementById('cipher').value = bytesToHex(encrypted); } catch (e) { alert('加密失败:' + e.message); } }); document.getElementById('decBtn').addEventListener('click', () => { try { const key = hexToBytes(document.getElementById('key').value.trim()); const iv = hexToBytes(document.getElementById('iv').value.trim()); const mode = document.getElementById('mode').value; const cipherBytes = hexToBytes(document.getElementById('cipher').value.trim()); const decrypted = mode === 'ecb' ? sm4Ecb(cipherBytes, key, true) : sm4Cbc(cipherBytes, key, iv, true); document.getElementById('plain').value = bytesToUtf8(decrypted); } catch (e) { alert('解密失败:' + e.message); } });

参数有三个容易错的地方:密钥和 IV 都是 32 位 hex 字符,对应 16 字节,少一位都会在 hexToBytes 的正则校验里抛错;模式切换只影响调用哪个函数,但 CBC 模式下 IV 全零和随机 IV 的解密结果完全不同;解密输入的密文必须是 hex 格式,如果后端返回 base64,要先 base64ToBytes 再进 sm4Cbc。

4.3 和后端联调先对齐四件事

前后端各自实现都通过标准向量自检后,联调还对不上,问题几乎都出在协议细节。我每次联调前会先发一张表给后端同事确认,比反复试错快得多。

检查项约定内容对不上时的现象
密钥长度与编码16 字节,hex 还是 base64长度报错、密文不一致
IV 规则是否用随机 IV,放在密文前还是单独传第一块解密乱码
填充方式统一 PKCS#7padding error
输出编码hex 还是 base64,大小写是否敏感后端解析失败

这四项是「互相看着都对但结果就是不对」的高发区。密钥长度和填充方式最容易被忽略,因为两个实现都自洽,单测都过,只有拼在一起才露馅。

5. 常见问题与避坑:SM4 前端落地的 5 个真实踩坑记录

5.1 现象:密钥不是 16 字节,项目直接抛错

现象:写完代码自己测,console 报 "SM4 密钥必须为 16 字节",或者后端解出来的数据和明文完全对不上。原因:写惯了 AES,习惯性认为密钥可以 16/24/32 字节;也有人直接把字符串密钥用 UTF-8 编码,一量长度是 20 甚至更长。解决:SM4 固定 128 位密钥,没有别的长度选项。字符串密钥要先约定映射规则,最常用的是做一次 SM3 摘要取前 16 字节,或者要求调用方直接传 32 位 hex。

5.2 现象:英文正常,中文解密出来是乱码

现象:加密一段中文,再解密,出来的是一串 ​ 之类的乱码符号。原因:编码链断了。页面不是 UTF-8,或者有人用String.fromCharCode做了字节转换,或者后端按 GBK 解了前端按 UTF-8 加密的数据。解决:每一步都统一到 UTF-8:html 里写<meta charset="UTF-8">,字符串转字节只用 TextEncoder,字节转字符串只用 TextDecoder,后端对接时明确按 UTF-8 处理。

5.3 现象:后端报 padding error,或解密结果末尾多一串内容

现象:前端自己加自己解正常,后端一解就报填充错误;偶尔不报错,但明文末尾多了几个重复字符。原因:填充协议不一致。后端用的可能是 ZeroPadding,或者前端解密的 unpad 逻辑没做校验,把填充字节当成了原文。解决:联调前把「PKCS#7」三个字写进接口文档。需要兼容老系统时,在前端加一个 padding 可选项,但默认必须是 PKCS#7。

5.4 现象:相同开头的数据,密文前缀也一模一样

现象:两条 JSON 只有末尾不同,加密后密文前半段完全相同,一眼能看出数据相似。原因:用的是 ECB 模式,每个块独立加密,相同明文块生成相同密文块。解决:切到 CBC 模式,并且每次加密都生成新的随机 IV。IV 不需要保密,常见做法是随机生成 16 字节后拼在密文最前面,后端取前 16 字节当 IV、剩下的当密文。

5.5 现象:页面"加密了",但谁都能解密

现象:密钥写死在 JS 文件里,觉得数据已经受保护。原因:前端代码加载到浏览器里,任何使用者都能在开发者工具里看到并提取密钥,这不是 bug,是前端加密的固有属性。解决:想清楚威胁模型。HTML 页面里的 SM4 解决的是传输层不用明文、满足对接方算法要求这一类问题,防不住使用者自己。常见的加固做法是把密钥从接口下发而不是写死在代码里,但必须明白这只是提高门槛,真正的机密数据还是要走服务端处理。

6. 进阶:从 demo 到可交付的 SM4 前端模块

demo 能跑只是起点,交付给项目前我会再做四件事。第一,把标准向量写进自动化测试,每次改动后先跑向量再跑联调,防止回归。向量测试通过意味着算法核心没坏,后续所有模式、填充改动都可以放心叠加。

第二,用 crypto.getRandomValues 生成随机密钥和 IV,别再用 Math.random。一段简单的封装就能用:

function randomBytes(len) { const out = new Uint8Array(len); crypto.getRandomValues(out); return out; }

第三,密钥不要直接暴露在页面源码里。常见做法是登录后由后端接口下发,或者构建期注入到环境变量里,再或者把密钥放在后端、前端只拿加密后的会话密钥。需要说清楚的是,这些都挡不住真正拿到页面代码的人,只是让密钥不躺在静态文件里被扫到。

第四,注意性能边界。SM4 是 32 轮迭代的纯软件实现,在浏览器里加密几十 KB 的文本没有问题,但如果是几 MB 的文件,主线程会卡顿,要放进 Web Worker 处理。

最后说一个我自己的习惯:接任何国密 SM4 需求,第一件事不是写代码,而是先把标准向量跑通,再和后端把密钥长度、填充方式、IV 规则、输出编码这四件事写在文档里。四个点对齐了,剩下的代码其实就是复制粘贴加参数校验。这个习惯帮我省掉了至少两次联调翻车,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 5:43:40

如何测试订阅逻辑?基于Koudoku的RSpec集成测试完整实战

后端 【免费下载链接】koudoku Robust subscription support for Rails with Stripe. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ko/koudoku 点击查看 免费下载 Koudoku 是一款为 Rails 应用提供 Stripe 订阅支付支持的引擎&#xff0c;它内置了价格表页面、订阅管…

作者头像 李华
网站建设 2026/10/18 15:06:20

D3.js等值线图交互实战:从网格数据到可缩放高亮

简介&#xff1a;这份资源围绕 d3.js 交互式等值线图展开&#xff0c;面向具备一定前端基础、希望在地图可视化方向进阶的开发者与数据分析人员。它解决的是如何把地理边界与统计数据结合&#xff0c;快速生成可交互、可配色、可读图例的等值线地图&#xff0c;适用于区域数据展…

作者头像 李华
网站建设 2026/10/20 12:26:45

高项论文质量管理写作指南:从过程组到工具用法的落地拆解

每年软考成绩出来之后&#xff0c;评论区里最热闹的往往是论文。尤其是“论项目的质量管理”这个题目&#xff0c;经常出现在备选列表里&#xff0c;也年年有大把考生栽在它上面。按理说&#xff0c;质量管理是项目管理者最不陌生的领域&#xff1a;开发要做测试、交付要有验收…

作者头像 李华
网站建设 2026/10/22 8:38:36

aiogram 入门实战:用完全异步的 Python 框架快速构建 Telegram Bot

后端即时通讯API设计 【免费下载链接】aiogram aiogram is a modern and fully asynchronous framework for Telegram Bot API written in Python using asyncio 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ai/aiogram 点击查看 免费下载 aiogram 是一个基于 Pytho…

作者头像 李华