起因加密后的东西一眼就能看出被加密了说实话这事儿困扰我挺久的。我平时有个习惯会在备忘录里记一些账号密码、私密想法之类的东西。用过几款加密工具Base64 编码也好AES 加密也好输出的结果都是一堆乱码或者U2FsdGVkX1...这种字符串。问题在哪太明显了。任何人瞥一眼就知道这段内容被加密过。被加密本身就是一种信息——它告诉别人这里有秘密。我就想能不能让加密结果看起来像一段正常的中文句子外人看到觉得是一句莫名其妙但人畜无害的话只有知道密钥的人才能还原这就是密语盒子的出发点。做了什么AES-GCM 加密 自定义字谱映射核心思路分两步先用 AES-GCM 对明文做强加密密钥通过 PBKDF2 从用户输入的密码派生拿到密文字节流把密文字节流按自定义的「字谱」做映射把每个字节替换成一个汉字或可读字符举个例子假设我的字谱是一组 256 个汉字刚好对应一个字节的 256 种取值那加密后的每个字节都能找到对应的汉字。最终输出就是一串中文字比如“梧桐落叶时节青山远眺归雁南飞”外人看了觉得像诗句碎片实际上它是一段被加密的内容。这和简单的替换密码凯撒密码那种完全不同——底层是 AES-GCM 这样的认证加密字谱只是最后一层视觉伪装。破解者即使猜到了字谱映射关系没有密钥照样还原不了明文。为什么选鸿蒙今年上半年开始做鸿蒙适配1.2.0 版本已经上架。选 HarmonyOS 有几个原因鸿蒙的安全能力底座挺好ohos.security.cryptoFramework提供了完整的 AES-GCM 和 PBKDF2 支持不用自己引第三方库ArkTS 写 UI 确实快声明式的方式做表单交互很顺手说实话也有私心——鸿蒙应用市场现在竞争没那么卷同类工具几乎没有技术细节鸿蒙侧的加密实现加密核心用了ohos.security.cryptoFramework这里贴一下密钥派生和加密的关键逻辑import cryptoFramework from ohos.security.cryptoFramework // PBKDF2 密钥派生 async function deriveKey(password: string, salt: Uint8Array): PromisecryptoFramework.SymKey { let kdfSpec: cryptoFramework.PBKDF2Spec { algName: PBKDF2, password: password, salt: salt, iterations: 100000, keySize: 32 } let kdf cryptoFramework.createKdf(PBKDF2|SHA256) let keyBlob await kdf.generateSecret(kdfSpec) let symKeyGenerator cryptoFramework.createSymKeyGenerator(AES256) return await symKeyGenerator.convertKey({ data: keyBlob.data }) } // AES-GCM 加密 async function encrypt(plainText: Uint8Array, symKey: cryptoFramework.SymKey, iv: Uint8Array): PromiseUint8Array { let cipher cryptoFramework.createCipher(AES256|GCM|PKCS7) let gcmParams: cryptoFramework.GcmParamsSpec { iv: { data: iv }, aad: { data: new Uint8Array(0) }, authTag: { data: new Uint8Array(16) }, algName: GcmParamsSpec } await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, gcmParams) let output await cipher.doFinal({ data: plainText }) return output.data } 拿到加密后的字节数组后就是字谱映射的环节。这一步反而简单——本质就是一个 256 长度的数组查表 arkts function bytesToCipherText(encrypted: Uint8Array, charset: string[]): string { let result for (let i 0; i encrypted.length; i) { result charset[encrypted[i]] // 每个字节 0-255 对应字谱中的一个字符 } return result } 解密就是反过来先把密文中每个字符在字谱里查位置还原成字节再用 AES-GCM 解密。 ## ArkUI 做交互的一些体会 UI 层用的 ArkUI几个页面加密页、解密页、字谱管理页。 说实话 ArkUI 的 TextArea 组件在处理大段文本输入时体验还行但有个小坑——onChange 在输入法组合输入时的触发时机和我预期不太一样调了一阵才稳定。 字谱管理那边我用了 ohos.data.preferences 做本地持久化每套字谱就是一个 256 长度的字符串数组。用户可以自己编辑、导入导出这样不同朋友之间可以约定不同的字谱。 ## 关于完全离线这件事 密语盒子没有网络权限。对一个字节都不往外发。 这不是什么高尚的设计哲学纯粹是因为——一个加密工具如果联网我自己用着都不放心。密钥派生、加解密、字谱存储全在本地完成。鸿蒙的沙箱机制保证了 preferences 数据不会被其他应用读取。 ## 几个真实使用场景 1. **备忘录里存密码**把支付宝密码: abc123加密后变成碧海潮生曲终人散存在系统便签里。手机被人借去用也不担心。 2. **和朋友发密电**两个人约定同一套字谱和密码在微信聊天里发密文。截图发朋友圈别人也看不懂——关键是看起来不像被加密的不会引起好奇心。 3. **日记类内容**有些想法不想被任何人看到加密后发在自己的社交平台上当树洞只有自己能解。 ## 当前状态和一些数据 - 上架时间2024年鸿蒙应用市场可搜密语盒子 - - 当前版本1.2.0 - - 下载量说实话还很少刚起步阶段 - - 包体积控制在 5MB 以内没有网络模块没有大型依赖 下载量不高我觉得正常——这东西的目标用户本来就小众是那些对隐私有洁癖、同时觉得传统加密工具太丑太明显的人。 ## 踩过的坑 1. **字谱长度问题**一开始我想用 65536 个字符的字谱对应两个字节一组映射这样密文更短。但实际操作中找到 65536 个不重复的、看起来自然的汉字组合太难了而且 Unicode 有些区段的字在鸿蒙字体里显示不出来。最后还是退回到 256 字符的方案接受密文长一些。 2. **GCM 的 authTag 处理**AES-GCM 加密后会产生一个认证标签解密时需要用到。一开始我把 authTag 拼在密文尾部一起做字谱映射结果解密时拆分位置算错了调了一整个下午。 3. **PBKDF2 迭代次数**10 万次迭代在手机上大概要 200-300ms用户体感还行。试过 50 万次等待超过 1 秒就有人受不了了。 ## 写在后面 如果你也在做鸿蒙开发特别是安全相关的方向欢迎交流。ohos.security.cryptoFramework 这个模块的文档说实话还不够详细很多用法是我翻源码和试错试出来的。 鸿蒙应用市场搜索「密语盒子」可以直接下载体验。有问题评论区聊。