
做鸿蒙开发只要涉及登录、支付、设备认证、安全传输就离不开非对称加解密算法。这一篇把鸿蒙上非对称加解密算法的落地讲透怎么生成 RSA、ECC、SM2 密钥怎么做加解密和签名验签以及我在真实项目里踩过的坑和最后的选型建议。上一篇已经把对称加密和消息摘要梳理完了这篇就专心搞非对称适合正在做鸿蒙原生应用、需要处理安全通信或数据校验的开发者参考。1. 非对称加解密在鸿蒙开发里到底扮演什么角色1.1 非对称算法解决的核心问题非对称加解密和对称算法最大的区别是它的密钥不是一个而是一对公钥可以公开私钥只能自己持有。公钥加密的数据只有对应私钥能解开私钥签名的数据只有对应公钥能验。这个特性解决了对称算法最头疼的分发问题双方不用事先约定同一个密钥只需要一方把公钥亮出来另一方用公钥加密数据传过去私钥持有者才能打开。在鸿蒙应用里最常见的场景就是客户端和服务端第一次建立安全通道。客户端拿到服务端公钥生成一个随机的对称密钥用服务端公钥加密后发给服务端服务端用私钥解密得到对称密钥之后所有业务数据都用对称算法加密。这样对称密钥没有被明文传输同时避免用非对称算法处理大数据。除了密钥交换非对称算法还大量用在签名验签上比如应用包完整性校验、设备证书鉴权、请求参数防篡改。还有一个容易被忽略的应用跨应用授权。比如你的应用要调用另一个应用提供的敏感能力对方会给你一个签名后的授权凭证你用对方公钥验签确认这个凭证确实来自对方而不是中间人伪造的。鸿蒙的分布式场景里设备之间的身份认证也经常走类似流程。可以说非对称算法是安全通信的地基不是某些业务才需要的“高级功能”。1.2 为什么真实项目几乎都用“混合加密”很多初学者拿到 RSA 之后第一反应是“我能不能用 RSA 加密整个文件”。答案是不行也不建议。RSA 这类非对称算法本质上是做大数模幂运算速度比 AES 慢好几个数量级而且明文长度受密钥长度限制。RSA2048 按 PKCS1 填充一次最多只能加密 245 字节按 OAEP 填充一次可能连 200 字节都不到。你拿它去加密一张图片或者一段视频要么分块拆到怀疑人生要么直接报错。所以实际项目里的标准做法是混合加密数据量大、性能敏感的部分用对称算法比如 AES-GCM只有对称密钥、少量核心参数、签名摘要这类小块数据才用非对称算法。我实测过同一台鸿蒙设备上AES-256-GCM 加密十几 MB 文件是毫秒级如果换成 RSA2048 加密同样大小得拆成几千块每块都做一次独立的大数运算耗时和耗电都扛不住。记住这个原则非对称算法负责“安全地传递钥匙”对称算法负责“高效地加密内容”。2. 鸿蒙Crypto框架的核心概念与算法规格2.1 先认识四个关键对象鸿蒙把加解密能力统一放在kit.CryptoArchitectureKit里面非对称部分主要用到四个对象对象作用典型方法AsyKeyGenerator生成非对称密钥对generateKeyPair()Cipher非对称加解密initSync()、doFinalSync()Sign私钥签名initSync()、updateSync()、signSync()Verify公钥验签initSync()、updateSync()、verifySync()理解起来其实很直观生成钥匙用 AsyKeyGenerator加密和解密用 Cipher签名和验签分别用 Sign 和 Verify。四个对象的生命周期都差不多先创建再 init 初始化需要分段的用 update 喂数据最后 doFinal 或者 sign/verify 收尾。这里想提醒一个容易忽略的点Cipher 既能加密也能解密区别只在 init 时传入的 mode 和 key。加密时CryptoMode.ENCRYPT_MODE配公钥解密时CryptoMode.DECRYPT_MODE配私钥。签名和验签则是对应关系更明确私钥签名公钥验签。方向搞反了初始化就会失败。2.2 算法规格字符串与密钥格式鸿蒙的 Crypto 接口经常要求传一个“算法规格字符串”比如RSA2048|PKCS1、ECC256、SM2_256。这种字符串看起来简单实际是最大的坑之一大小写不能错分隔符必须是竖线算法名、密钥长度、填充模式、摘要算法这些元素必须跟你手头的 SDK 版本匹配。我见过同事把RSA2048|PKCS1写成RSA_2048|PKCS1初始化直接抛参数错误。不同版本对算法规格的支持不完全一样尤其是 SM2、OAEP、PSS 这些相对新一点的能力老版本可能不识别。建议拿到一个新版本 SDK 后先把官方支持的算法规格列表跑一遍或者直接看接口文档里附带的枚举说明不要凭记忆拼字符串。密钥格式也需要注意。鸿蒙里生成或导出的密钥通常是 DER 编码的二进制数据用getEncoded()拿到 Uint8Array再按需转成 Base64 或者 PEM 格式跟服务端互换。公钥可以随便导出私钥导出要非常谨慎非必要不要出安全区。3. RSA密钥生成、加解密与签名验签实操3.1 生成RSA2048密钥对先来一段最基础的操作生成 RSA2048 密钥对。import { cryptoFramework } from kit.CryptoArchitectureKit; async function generateRsaKeyPair() { const asyKeyGenerator cryptoFramework.createAsyKeyGenerator(RSA2048); const keyPair await asyKeyGenerator.generateKeyPair(); return keyPair; }代码不长但有几个地方要说清楚。第一RSA2048意思是生成 2048 位的 RSA 密钥对这是目前最低建议线1024 位已经不安全了尽量别用。第二generateKeyPair()是异步方法内部涉及大量随机数生成和大素数计算耗时可能有几百毫秒一定要放到异步任务里别在 UI 线程同步调用。第三如果你的应用只是为了做临时校验密钥对可以只留在内存用完释放如果要长期保存私钥不要直接写进普通文件优先用系统提供的安全存储能力。公钥导出也很简单const pubKey keyPair.pubKey; const pubKeyBlob pubKey.getEncoded(); // 再把 pubKeyBlob.data 转成 Base64方便传给服务端不同 SDK 版本里getEncoded()的返回结构可能略有差异但方向一致拿到 DER 二进制之后自己再转格式。私钥导出我不建议在业务代码里做尤其不要打日志。3.2 加密解密填充模式与长度限制RSA 加解密的关键不是算法本身而是填充模式。鸿蒙里常见的有 PKCS1 和 OAEP。PKCS1 兼容性好很多老服务端只认这种OAEP 带随机性和更强的安全性证明在条件允许时优先选 OAEP。加密代码如下import { cryptoFramework } from kit.CryptoArchitectureKit; function rsaEncrypt(data: Uint8Array, pubKey: cryptoFramework.PubKey): Uint8Array { const cipher cryptoFramework.createCipher(RSA2048|PKCS1); cipher.initSync(cryptoFramework.CryptoMode.ENCRYPT_MODE, pubKey); const encryptBlob cipher.doFinalSync({ data }); return encryptBlob.data; }解密方向相反function rsaDecrypt(cipherData: Uint8Array, priKey: cryptoFramework.PriKey): Uint8Array { const decoder cryptoFramework.createCipher(RSA2048|PKCS1); decoder.initSync(cryptoFramework.CryptoMode.DECRYPT_MODE, priKey); const decryptBlob decoder.doFinalSync({ data: cipherData }); return decryptBlob.data; }我把这段逻辑列在一起是想强调一件事RSA2048 加密后密文长度固定是 256 字节不管明文是 1 字节还是 245 字节。你拿 RSA 加密一段短数据密文膨胀几十倍是很正常的下游存储和传输要预留空间。明文长度基本取决于两个因素密钥位数和填充开销。RSA2048 用 PKCS1上限是 245 字节用 OAEP 加 SHA256上限会掉到 190 字节左右。一旦超过上限doFinal会直接报错根本不会给你侥幸空间。3.3 签名验签方向别搞反签名验签是另一个高频场景比如客户端给请求参数签名服务端验签。签名用的是私钥验签用的是公钥这个方向跟加密正好相反也是最容易犯错的点。import { cryptoFramework } from kit.CryptoArchitectureKit; function signData(data: Uint8Array, priKey: cryptoFramework.PriKey): Uint8Array { const signer cryptoFramework.createSign(RSA2048|PKCS1|SHA256); signer.initSync(priKey); signer.updateSync({ data }); return signer.signSync().data; } function verifyData(data: Uint8Array, signData: Uint8Array, pubKey: cryptoFramework.PubKey): boolean { const verifier cryptoFramework.createVerify(RSA2048|PKCS1|SHA256); verifier.initSync(pubKey); verifier.updateSync({ data }); return verifier.verifySync({ data: signData }); }注意createSign(RSA2048|PKCS1|SHA256)这个规格里包含了三个信息算法 RSA2048、填充 PKCS1、摘要 SHA256。相同的签名数据用同样的规格验签才能通过。如果你在服务端用了 PSS 填充鸿蒙这边也要对应调整。还需要分清“加密”和“签名”不是一件事。加密是保护数据机密性签名是保护数据完整性和来源可信。有人会问“我用私钥加密别人用公钥解开不也能验证来源吗”。理论上 RSA 可以这么倒着用但这不是标准用法而且容易踩填充和语义的坑。在鸿蒙里就老老实实按 API 设计来数据加密用 Cipher数据签名用 Sign/Verify。4. ECC与SM2更现代的非对称方案4.1 ECC到底适合做什么ECC椭圆曲线密码这几年越来越受欢迎核心优势是“同样的安全强度下密钥更短”。256 位 ECC 密钥的安全强度大致相当于 3072 位 RSA但密钥长度短计算量小传输的数据量也小。在移动端这种资源敏感环境ECC 天然比 RSA 有优势。鸿蒙里生成 ECC 密钥对可以用类似的方法import { cryptoFramework } from kit.CryptoArchitectureKit; async function generateEccKeyPair() { const generator cryptoFramework.createAsyKeyGenerator(ECC256); const keyPair await generator.generateKeyPair(); return keyPair; }ECC256通常对应 secp256r1 曲线这也是行业内用的最多的曲线之一。但要注意ECC 在实际开发里主要用于两类场景签名验签和密钥协商。拿 ECC 做大块数据加密并不常见因为 ECC 加密算法本身有额外开销而且很多框架对 ECC 加密支持不完整。如果你需要的是“双方协商出一个对称密钥”那应该用 KeyAgreement 这类密钥协商能力不是一个 Cipher 就能解决的。我见过有人非要用 ECC 的 Cipher 接口去加密几百字节数据结果发现不同平台实现不一样联调非常痛苦。4.2 SM2国密算法的鸿蒙实践SM2 是基于椭圆曲线的国密算法支持加密、签名、密钥协商。国内很多政企、金融、能源类项目在对接时都要求支持国密算法鸿蒙在较新版本里也开放了 SM2 的接入能力。从代码结构上看SM2 的使用方式跟 ECC 很接近import { cryptoFramework } from kit.CryptoArchitectureKit; async function generateSm2KeyPair() { const generator cryptoFramework.createAsyKeyGenerator(SM2_256); const keyPair await generator.generateKeyPair(); return keyPair; }部分版本可能只认SM2而不是SM2_256我建议你先查一下当前 SDK 的算法规格列表或者两个都试一下初始化成功为准。签名规格一般会带上 SM3 摘要比如SM2|SM3具体拼法同样以官方文档为准。SM2 和 ECC 毕竟都是椭圆曲线族选型主要看对接方的要求。如果服务端、网关、加密机全部要求国密那就只能走 SM2没有多少讨价还价的余地。如果只是普通互联网应用没有强制要求ECC 或者 RSA 都可以没必要为了“看起来国产”强行引入一套可能不兼容的算法。4.3 三种算法选型参考算法常见密钥长度主要用途优点注意事项RSA2048 / 3072数据加密、签名兼容性最好几乎所有平台支持密钥长、计算慢不适合大数据量ECC256 / 384签名、密钥协商密钥短、性能好、数据体积小数据加密支持不如 RSA 普遍SM2256加密、签名、密钥协商国密标准国内业务对接友好依赖服务端与SDK版本联调成本可能高如果项目是从零开始服务端和客户端都是自己说了算我通常会优先选 ECC密钥短、签名体积小接口性能也好。如果必须对接存量系统对方只支持 RSA那就老老实实用 RSA2048不要试图在移动端强行掀桌子。有国密强制要求的场景直接走 SM2同时在接口设计上做好算法标识方便以后切换。5. 实战中最容易踩的坑与排查思路5.1 明文太长导致加解密失败这个问题在 RSA 场景里特别常见。你拿一个几百字节的 JSON 串直接加密结果doFinalSync抛异常错误信息大概意思就是数据长度超出范围。这不是代码 bug是 RSA 本身的能力边界。处理办法有两种。第一种是分块加密按填充模式算出单块上限然后循环加密。伪代码思路是这样chunkSize 245 // RSA2048 PKCS1 offset 0 while offset data.length: take min(chunkSize, data.length - offset) block data.slice(offset, offset take) encryptedBlock cipher.doFinalSync({ data: block }) result.push(encryptedBlock.data) offset take但分块加密有额外成本而且每块密文长度固定拼接时要注意顺序不要丢块。第二种也是我更推荐的不要拿 RSA 直接加密大数据先随机生成一个 AES 密钥用 RSA 加密这个 AES 密钥业务数据全部走 AES-GCM。这样既绕开长度限制又保证了性能。5.2 公私钥编码和格式转换问题鸿蒙导出的密钥通常是 DER 二进制而很多服务端返回的是 PEM 文本带-----BEGIN PUBLIC KEY-----头尾中间还有换行。初学者最容易在这里翻车直接拿 PEM 字符串当 Base64 解码结果解出来带着头尾、换行服务端验签失败。正确做法是先把 PEM 里的头尾去掉再把中间部分按 Base64 解码成 Uint8Array最后用鸿蒙的convertKey之类接口导入。反过来鸿蒙导出公钥后如果需要给服务端先转 Base64再按对方要求的格式拼 PEM。我建议在封装层统一做一个pubKeyToBase64()和base64ToPubKey()把格式转换收敛到一处别散落在各个业务文件里。5.3 同步API和异步API千万别乱用鸿蒙 Crypto 框架里有同步接口也有异步接口。generateKeyPair()这种耗时操作一般有 Promise 版本initSync、doFinalSync、signSync这种加了Sync后缀的是同步接口。它们不是随便替代的关系关键是看你在什么线程调用。在 UI 线程里我强烈不建议调doFinalSync去做大块数据的对称加密或者多次非对称分块操作会卡住界面用户一滑动页面就明显掉帧。正确做法是把加解密逻辑放到ohos.taskpool或者 Worker 里把原始数据传进去拿结果回来。这个坑在性能要求高的扫码登录、文件解密场景尤其明显。5.4 性能与私钥安全建议非对称算法性能差距很大我给了自己项目里的一组参考值相同设备上RSA2048 私钥签名一次大约要 1 到 2 毫秒验签会快一点ECC256 签名通常在 0.1 毫秒量级密钥生成速度也更快。如果你对接口耗时有要求非对称不该成为瓶颈但也不要用 RSA 去加密超过 1KB 的数据。私钥安全是另一个容易被忽略的点。不要把私钥明文写进配置文件、数据库或者日志。鸿蒙系统有安全能力封装优先用系统提供的密钥安全存储方案而不是自己把私钥导出后存到沙盒目录。我见过一个项目为了调试方便把私钥打印在日志里最后被安全扫描发现整个版本被打回教训很深刻。调试阶段可以临时用测试密钥但发布包一定不能带私钥。6. 我的一些实际操作体会6.1 项目里我默认怎么选踩过几次坑之后我现在面对“鸿蒙上做非对称加解密”这类需求时基本是这么决策的先问对接方支持什么算法。没有特殊要求默认走 ECC256用签名做接口防篡改用密钥协商做安全通道如果业务系统是老一代架构或者客户明确说要兼容第三方插件退回 RSA2048如果客户有明确国密要求SM2 没得跑而且协议栈最好在服务端也提前确认好。签名填充上我自己会优先看有没有 PSS 支持有就上 PSS没有再用 PKCS1。数据加密上能 OAEP 就 OAEP。这些选择不一定让代码更好看但能在不影响性能的前提下提高安全余量。换个角度讲算法选型本质上是“对手是谁、对接方是谁、维护成本能接受多少”的权衡不是越新越好也不是越复杂越好。6.2 一个能减少沟通成本的小技巧最后分享一个小技巧拿到一个新版本鸿蒙 SDK第一件事不是写业务代码而是把所有支持的算法规格字符串打印出来或者写一个小的自测页面把 RSA、ECC、SM2 的生成、加解密、签名验签各跑一遍。这样在跟服务端联调之前你就知道当前设备上哪些算法是真正可用的哪些字符串只是文档里写了但实际不支持。这个自测集还能当回归用例以后升级 SDK 或换设备型号时跑一遍就知道有没有行为变化。非对称加密这块说起来概念不难真正麻烦的是 Api 细节、算法规格、格式转换和边界条件。把基础流程跑通再把这些边界情况处理干净后面做安全通信也就顺了。