鸿蒙应用实战:基于AES-GCM与字谱替换的文本隐写加密工具
1. 项目概述:当加密结果伪装成一段“人话”
最近在捣鼓鸿蒙应用开发,做了个挺有意思的小工具。它的核心功能很简单:你把一段敏感文本(比如密码、私密笔记)交给我,我帮你用 AES-GCM 算法加密。但和普通加密工具输出一堆乱码不同,我这个工具吐出来的,是一段看起来完全正常、甚至有点文采的中文句子或段落。
比如,加密“我的银行卡密码是123456”后,你可能会得到“春风又绿江南岸,明月何时照我还”这样的结果。只有知道密钥和规则的人,才能从这句诗里还原出原始信息。这个想法源于对隐私保护场景的再思考——在某些需要记录或传输密文,但又不想引起旁人额外注意的场合,一段“正常”的文本就是最好的伪装。这不仅仅是加密,更是一种信息隐写术(Steganography)在文本层的应用。
这个项目主要面向两类开发者:一是对鸿蒙(HarmonyOS)应用开发感兴趣,想找点有挑战性的实战项目练手的朋友;二是对密码学应用、数据安全,特别是如何将加密结果“无害化”、“自然化”感兴趣的同好。整个实现涉及鸿蒙 UI 开发、AES-GCM 加解密、以及自定义的“字谱替换”算法,麻雀虽小,五脏俱全。
2. 核心思路拆解:AES-GCM 确保安全,字谱替换负责伪装
整个项目的架构可以清晰地分为前后两个环节: 安全加密 和 结果伪装 。两者缺一不可,且顺序固定。
2.1 为什么是 AES-GCM?
在密码学里,对称加密算法 AES 是事实上的标准,但单纯使用 AES 的 ECB 或 CBC 模式存在一些隐患,比如需要单独处理消息完整性验证(MAC)。AES-GCM(Galois/Counter Mode)将加密和认证一步到位,是我选择它的核心原因。
- 加密与认证一体化 :GCM 模式在加密的同时,会生成一个认证标签(Authentication Tag)。在解密时,会先验证这个标签,只有标签正确才进行解密。这能有效防止密文被篡改,如果攻击者改动了密文中的一个字节,解密时验证会失败,系统会抛出异常,而不是输出一个错误但可能被误解的明文。这对于我们后续的替换步骤至关重要——我们必须确保用于替换的“原料”是完整且未被篡改的。
- 需要初始向量(IV) :GCM 模式要求一个每次加密都不同的、非重复的初始向量。通常推荐使用密码学安全的随机数生成器(CSPRNG)生成 12 字节的 IV。这个 IV 不需要保密,可以和密文一起存储或传输。在我们的项目中,IV 会作为后续字谱替换的“种子”或参数之一,确保即使同一明文、同一密钥,每次加密产生的“中文句子”都不同,增加观察者分析的难度。
- 关联数据(AAD) :GCM 支持可选的关联数据,这部分数据会被认证但不被加密。我们可以利用这个特性,将一些不影响解密但有助于验证的上下文信息(比如加密时间、版本号)放进去,进一步提升安全性。
注意 :密钥(Key)的管理是安全的核心。在这个演示项目中,为了简化,密钥可能由用户输入或硬编码。但在真实生产环境中,密钥必须通过安全的密钥派生函数(如 PBKDF2, Argon2)从强密码生成,并妥善存储在安全的密钥库中,绝不能明文存储。
2.2 字谱替换:将二进制密文“翻译”成中文
这是项目中最具创意和挑战性的部分。AES-GCM 输出的密文和认证标签是一串二进制数据(通常表示为字节数组或十六进制字符串)。我们的目标是将这串“乱码”映射成一个由合法汉字构成的序列。
我将其称为“字谱替换”,灵感来源于密码学中的替换密码(Substitution Cipher),但规模和应用逻辑要复杂得多。
-
核心挑战 :二进制数据是任意的,而汉字是离散的字符集。我们需要建立一个双向、确定性的映射规则。
-
基本思路 :
- 编码(加密后) :将密文(含Tag)的字节数组,通过特定算法,转换为一串索引号。每个索引号对应一个预定义“字谱”中的一个汉字。
- 解码(解密前) :将收到的一串汉字,根据相同的“字谱”和算法,反向转换回字节数组,然后交给 AES-GCM 解密。
-
算法设计考量 :
- 字谱规模 :常用汉字约3500个,GB2312收录6763个。字谱越大,单个汉字能承载的信息量(熵)就越多,生成的句子就越短。但也要考虑生成文本的“自然度”,生僻字太多会显得突兀。我选择了一个约5000字的常用字谱,平衡了效率与自然性。
- 映射算法 :不能简单地将字节直接模运算映射到字谱索引,因为字节值(0-255)范围远小于字谱索引。我采用的是一种“分组编码”方式:将字节数组每两个字节分为一组(16位,范围0-65535),这个数值对字谱大小取模,得到汉字索引。如果字节数组长度为奇数,最后一个字节单独处理并与IV的某个字节结合生成索引。这样能均匀利用字谱。
- 引入IV增加随机性 :在分组编码时,将IV的字节作为扰动因子参与计算。这样,相同的明文和密钥,因IV不同,不仅密文不同,最终生成的汉字序列也会完全不同,避免了“相同秘密总是生成相同句子”的规律。
- 自然语言包装 :直接映射出的汉字序列可能是生硬的。为了更逼真,我增加了一个“后处理”层。例如,将生成的索引序列按一定规则插入到预制的“句子模板”中,或者使用一个简单的马尔可夫链模型,根据前一个生成的汉字从词频表中选取下一个更常见的搭配,让输出看起来更像连贯的文本。
3. 鸿蒙应用开发实战:从零构建加密工具
3.1 开发环境与项目搭建
我使用的是华为官方的 DevEco Studio(4.0 或以上版本),选择“Empty Ability”模板创建项目,语言为 ArkTS。这是鸿蒙主推的应用开发语言,语法类似 TypeScript,对前端和移动端开发者比较友好。
项目关键目录结构 :
MyCipherTool/
├── entry/src/main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets // 应用入口
│ │ ├── pages/
│ │ │ └── Index.ets // 主页面 UI 和逻辑
│ │ └── cipher/
│ │ ├── AesGcmUtil.ets // AES-GCM 加解密核心类
│ │ ├── WordSpectrum.ets // 字谱替换算法类
│ │ └── constants.ets // 字谱常量、默认密钥等
│ └── resources/ // 资源文件
└── oh-package.json5 // 项目依赖配置
首先,需要在 oh-package.json5 中声明加解密所需的依赖。鸿蒙系统提供了安全的加密算法库。
// oh-package.json5
{
"license": "...",
"devDependencies": { ... },
"dependencies": {
"@ohos/crypto-js": "file:../feature/crypto-js" // 假设使用一个封装好的JS库,实际需根据SDK调整
// 或者直接使用系统提供的安全算法接口,如 @ohos.security.cryptoFramework
}
}
实操心得 :鸿蒙的 API 仍在快速演进中,密码学相关接口的完整度和易用性在不同版本可能有差异。如果系统级
cryptoFramework的 AES-GCM 接口尚未完全满足需求,一个可行的备选方案是集成一个经过审计的、纯 ArkTS/JS 实现的加密库(如crypto-js的移植版),但需仔细评估其安全性和性能。本项目为演示原理,优先考虑使用系统安全 API。
3.2 UI 界面设计与布局
主页面 ( Index.ets ) 采用简单的 Column 垂直布局,包含以下几个核心组件:
- 文本输入框 (
TextArea) :用于输入待加密的明文或待解密的“伪装中文”。设置placeholder属性提示用户。 - 密钥输入框 (
TextInput) :用于输入加密密钥。为了安全,可以将其type属性设置为InputType.Password,显示为圆点。 - 操作按钮 (
Button) :两个按钮,分别触发“加密”和“解密”操作。 - 结果显示框 (
Text) :用于显示加密后生成的“中文句子”或解密后的原始明文。可以设置为多行、可滚动。 - 状态提示 (
Text) :用于显示操作状态,如“加密成功”、“解密失败:认证标签错误”等。
布局代码框架如下:
// Index.ets 部分代码
@Entry
@Component
struct Index {
@State private plainText: string = '';
@State private cipherText: string = '';
@State private key: string = 'MySecretKey123!'; // 默认密钥,仅演示用
@State private statusMsg: string = '就绪';
build() {
Column({ space: 20 }) {
Text('加密工具:密文即人言').fontSize(30).fontWeight(FontWeight.Bold)
TextArea({ placeholder: '请输入要加密的明文...' })
.width('90%')
.height(100)
.onChange((value: string) => {
this.plainText = value;
})
TextInput({ placeholder: '请输入加密密钥', text: this.key })
.width('90%')
.type(InputType.Password)
.onChange((value: string) => {
this.key = value;
})
Row({ space: 40 }) {
Button('加密', { type: ButtonType.Capsule })
.onClick(() => {
this.handleEncrypt();
})
Button('解密', { type: ButtonType.Capsule })
.onClick(() => {
this.handleDecrypt();
})
}
Text('结果:').fontSize(18).fontWeight(FontWeight.Medium)
Scroll() {
Text(this.cipherText)
.width('90%')
.padding(10)
.backgroundColor(Color.White)
.border({ width: 1, color: Color.Grey })
}
.width('90%')
.height(150)
Text(this.statusMsg).fontSize(14).fontColor(Color.Grey)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F5F5F5')
}
// 加密处理函数
private async handleEncrypt() {
// 调用 AesGcmUtil 和 WordSpectrum
this.statusMsg = '加密中...';
// ... 具体逻辑
}
// 解密处理函数
private async handleDecrypt() {
// 调用 WordSpectrum 和 AesGcmUtil
this.statusMsg = '解密中...';
// ... 具体逻辑
}
}
3.3 AES-GCM 加解密模块实现
在 AesGcmUtil.ets 中,我们封装与系统安全 API 的交互。这里以假设的系统 cryptoFramework API 为例。
// AesGcmUtil.ets
import cryptoFramework from '@ohos.security.cryptoFramework';
export class AesGcmUtil {
private static readonly ALGORITHM = 'AES-GCM';
private static readonly KEY_LENGTH = 256; // 比特
private static readonly IV_LENGTH = 12; // 字节,推荐值
private static readonly TAG_LENGTH = 16; // 比特,认证标签长度
/**
* 加密
* @param plainText 明文文本
* @param keyStr 密钥字符串
* @returns 包含密文和IV的Base64字符串(实际中IV和密文需分开处理)
*/
static async encrypt(plainText: string, keyStr: string): Promise<Uint8Array> {
try {
// 1. 将字符串密钥转换为 CryptoKey
const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES');
const keyData = new Uint8Array(this.stringToUtf8Bytes(keyStr));
// 注意:实际应用中,应从keyStr通过PBKDF2派生密钥,这里简化处理
const cryptoKey = await symKeyGenerator.convertKey({ data: keyData });
// 2. 生成随机IV
const iv = cryptoFramework.createRandom(this.IV_LENGTH);
// 3. 创建并初始化加密器
const cipher = cryptoFramework.createCipher(this.ALGORITHM);
await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, cryptoKey, {
iv: iv,
additionalData: new Uint8Array(), // 可选关联数据
authTagLength: this.TAG_LENGTH
});
// 4. 执行加密
const inputData = { data: this.stringToUtf8Bytes(plainText) };
const encryptUpdate = await cipher.update(inputData);
const encryptFinal = await cipher.doFinal();
// 5. 组合 IV + 密文 + 认证标签
// 注意:cipher.doFinal() 可能已经包含了认证标签,具体API需查阅文档
// 这里假设 encryptFinal.data 是密文,通过 cipher.getAuthTag() 获取标签
const cipherText = encryptUpdate.data.concat(encryptFinal.data);
const authTag = await cipher.getAuthTag();
const result = new Uint8Array(iv.length + cipherText.length + authTag.length);
result.set(iv, 0);
result.set(cipherText, iv.length);
result.set(authTag, iv.length + cipherText.length);
return result; // 返回完整的二进制数据块
} catch (error) {
console.error('加密失败:', error);
throw new Error(`加密过程出错: ${error.message}`);
}
}
/**
* 解密
* @param combinedData 包含IV、密文、Tag的二进制数据
* @param keyStr 密钥字符串
* @returns 解密后的明文字符串
*/
static async decrypt(combinedData: Uint8Array, keyStr: string): Promise<string> {
try {
// 1. 拆分数据
const iv = combinedData.slice(0, this.IV_LENGTH);
const cipherTextWithTag = combinedData.slice(this.IV_LENGTH);
// 假设最后 TAG_LENGTH/8 字节是认证标签
const tagLengthBytes = this.TAG_LENGTH / 8;
const cipherText = cipherTextWithTag.slice(0, cipherTextWithTag.length - tagLengthBytes);
const authTag = cipherTextWithTag.slice(cipherTextWithTag.length - tagLengthBytes);
// 2. 准备密钥(同加密)
const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES');
const keyData = new Uint8Array(this.stringToUtf8Bytes(keyStr));
const cryptoKey = await symKeyGenerator.convertKey({ data: keyData });
// 3. 创建并初始化解密器,传入认证标签
const decipher = cryptoFramework.createCipher(this.ALGORITHM); // 可能是createDecipher,依API而定
await decipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, cryptoKey, {
iv: iv,
additionalData: new Uint8Array(),
authTag: authTag, // 传入认证标签进行验证
authTagLength: this.TAG_LENGTH
});
// 4. 执行解密
const decryptUpdate = await decipher.update({ data: cipherText });
const decryptFinal = await decipher.doFinal(); // 这里会验证Tag,失败则抛出异常
// 5. 组合解密结果并转字符串
const plainData = decryptUpdate.data.concat(decryptFinal.data);
return this.utf8BytesToString(plainData);
} catch (error) {
console.error('解密失败:', error);
// 特别处理认证失败错误
if (error.message?.includes('auth') || error.message?.includes('tag')) {
throw new Error('解密失败:认证标签错误,密文可能已被篡改。');
}
throw new Error(`解密过程出错: ${error.message}`);
}
}
// 辅助函数:字符串转UTF-8字节数组
private static stringToUtf8Bytes(str: string): Uint8Array {
const encoder = new TextEncoder();
return encoder.encode(str);
}
// 辅助函数:UTF-8字节数组转字符串
private static utf8BytesToString(bytes: Uint8Array): string {
const decoder = new TextDecoder('utf-8');
return decoder.decode(bytes);
}
}
重要提示 :以上代码是概念性示例,鸿蒙
cryptoFramework的实际 API 签名和用法请务必查阅对应版本的官方文档。密钥处理(convertKey)部分极度简化,真实场景必须使用PBKDF2等算法进行密钥派生。
3.4 字谱替换算法核心实现
WordSpectrum.ets 是这个项目的灵魂。它管理一个字谱(汉字数组),并提供编码/解码方法。
// WordSpectrum.ets
export class WordSpectrum {
private spectrum: string[]; // 汉字字谱数组
private spectrumSize: number;
// 初始化字谱,可以从文件加载,这里硬编码示例
constructor() {
// 这里是一个极简示例字谱,实际应有数千字
this.spectrum = ['春', '风', '又', '绿', '江', '南', '岸', '明', '月', '何', '时', '照', '我', '还', '人', '山', '水', '天', '地', '一', '二', '三'];
this.spectrumSize = this.spectrum.length;
console.log(`字谱初始化完成,共 ${this.spectrumSize} 字`);
}
/**
* 将二进制数据编码为汉字序列
* @param data 二进制数据(通常是IV+密文+Tag)
* @returns 汉字字符串
*/
encode(data: Uint8Array): string {
let result = '';
// 使用数据本身的前几个字节作为简易扰动因子(实际可用IV)
let seed = 0;
for (let i = 0; i < Math.min(4, data.length); i++) {
seed = (seed << 8) | data[i];
}
for (let i = 0; i < data.length; i += 2) {
let index: number;
if (i + 1 < data.length) {
// 两个字节组成一个16位数
const word = (data[i] << 8) | data[i + 1];
index = (word ^ seed) % this.spectrumSize; // 用seed做异或扰动
} else {
// 最后一个字节单独处理
index = (data[i] ^ (seed & 0xFF)) % this.spectrumSize;
}
result += this.spectrum[index];
// 简单后处理:每4字加一个空格,更像自然文本
if ((result.replace(/\s/g, '').length) % 4 === 0 && i < data.length - 1) {
result += ' ';
}
}
return result;
}
/**
* 将汉字序列解码为二进制数据
* @param text 汉字字符串(可含空格)
* @returns 二进制数据
*/
decode(text: string): Uint8Array {
// 移除空格等分隔符
const cleanText = text.replace(/\s/g, '');
const bytePairs: number[] = [];
// 注意:解码是编码的逆过程,需要相同的seed。
// 但seed来源于原始数据的前几个字节,而我们现在只有汉字。
// 这是一个关键问题!说明我们的编码算法必须是完全确定且可逆的,不能依赖数据本身动态计算seed。
// 因此,需要修改设计。
console.warn('解码功能需要与编码匹配的确定性算法,当前示例算法不完整。');
// 以下为示意性代码,假设我们有一个确定性的映射表或算法
// 实际实现需要建立一个从汉字到其索引的Map,然后根据索引还原字节。
// 这要求编码算法是严格一一对应且可逆的。
return new Uint8Array(bytePairs);
}
// 一个更健壮、可逆的编码方案示例(BaseX编码思想)
public encodeRobust(data: Uint8Array): string {
const base = this.spectrumSize;
let num = 0n; // 使用BigInt处理大整数
// 将字节数组视为一个大端序的大整数
for (const byte of data) {
num = (num << 8n) | BigInt(byte);
}
// 将这个整数转换为“字谱进制”
let encoded = '';
while (num > 0) {
const remainder = Number(num % BigInt(base));
encoded = this.spectrum[remainder] + encoded;
num = num / BigInt(base);
}
// 如果data是0,需要特殊处理
if (encoded === '') {
encoded = this.spectrum[0];
}
return this.formatText(encoded);
}
public decodeRobust(text: string): Uint8Array {
const cleanText = text.replace(/\s/g, '');
const base = this.spectrumSize;
// 建立汉字到索引的映射
const charToIndex = new Map<string, number>();
this.spectrum.forEach((char, idx) => charToIndex.set(char, idx));
// 将“字谱进制”字符串转换回大整数
let num = 0n;
for (const char of cleanText) {
const index = charToIndex.get(char);
if (index === undefined) {
throw new Error(`发现非法字符: ${char}`);
}
num = num * BigInt(base) + BigInt(index);
}
// 将大整数转换回字节数组
const bytes: number[] = [];
while (num > 0) {
bytes.unshift(Number(num & 0xFFn));
num = num >> 8n;
}
// 处理前导零(BigInt转换会丢失最高位的0字节)
// 我们需要知道原始数据的长度。一个简单的方法是在编码时记录长度,或使用填充。
// 这是一个遗留问题,更完善的方案需要添加长度信息或使用固定块大小。
return new Uint8Array(bytes);
}
private formatText(text: string): string {
// 简单的格式化,每4字插入空格
let formatted = '';
for (let i = 0; i < text.length; i++) {
formatted += text[i];
if ((i + 1) % 4 === 0 && i !== text.length - 1) {
formatted += ' ';
}
}
return formatted;
}
}
关于可逆编码的关键说明 :最初的 encode/decode 示例存在缺陷,因为解码时需要编码过程中动态生成的 seed ,而这信息并未包含在输出文本中。 encodeRobust/decodeRobust 展示了一种更可靠的方法:将整个字节数组当作一个大整数,转换为以字谱大小为基数的表示法。这种方法严格可逆,但需要注意处理前导零字节和确定输出长度的问题。一个工程上的解决方案是:在编码前,在数据头部添加一个表示原始数据长度的字段(例如,2字节),这样解码时就能准确还原。
3.5 主页面逻辑串联
最后,在 Index.ets 的 handleEncrypt 和 handleDecrypt 方法中,将上述模块串联起来。
// Index.ets 中 handleEncrypt 函数示例
private async handleEncrypt() {
if (!this.plainText.trim()) {
this.statusMsg = '请输入明文';
return;
}
if (!this.key.trim()) {
this.statusMsg = '请输入密钥';
return;
}
this.statusMsg = '加密中...';
try {
// 1. 使用AES-GCM加密,得到二进制数据
const encryptedBytes = await AesGcmUtil.encrypt(this.plainText, this.key);
// 2. 使用字谱替换,将二进制数据编码为中文
const wordSpectrum = new WordSpectrum();
const chineseOutput = wordSpectrum.encodeRobust(encryptedBytes);
// 3. 更新UI
this.cipherText = chineseOutput;
this.statusMsg = `加密成功!生成 ${chineseOutput.length} 字。`;
this.plainText = ''; // 清空明文输入框,增强安全性感知
} catch (error) {
console.error('加密流程错误:', error);
this.statusMsg = `加密失败: ${error.message}`;
this.cipherText = '';
}
}
private async handleDecrypt() {
if (!this.cipherText.trim()) {
this.statusMsg = '请输入待解密的文本';
return;
}
if (!this.key.trim()) {
this.statusMsg = '请输入密钥';
return;
}
this.statusMsg = '解密中...';
try {
// 1. 使用字谱替换,将中文解码为二进制数据
const wordSpectrum = new WordSpectrum();
const decodedBytes = wordSpectrum.decodeRobust(this.cipherText);
// 2. 使用AES-GCM解密二进制数据
const decryptedText = await AesGcmUtil.decrypt(decodedBytes, this.key);
// 3. 更新UI
this.plainText = decryptedText;
this.statusMsg = '解密成功!';
this.cipherText = ''; // 清空密文显示
} catch (error) {
console.error('解密流程错误:', error);
this.statusMsg = `解密失败: ${error.message}`;
this.plainText = '';
}
}
4. 进阶优化与问题排查
4.1 提升“自然度”的进阶技巧
直接映射生成的汉字序列虽然“合法”,但可能缺乏语言节奏感。我们可以引入一些简单的自然语言处理(NLP)技巧来优化:
- 模板填充法 :预置多个优美的古诗词或常见句子模板,其中包含一些占位符。将编码生成的汉字序列,按顺序填充到这些占位符中。例如,模板“ 1 处 2 生 3 , 4 时 5 落 6 。”,用6个生成的汉字替换
__1__到__6__。这种方法能快速生成结构工整的文本。 - 基于词频的链式选择 :建立一个汉字二元或三元词频表。编码时,在根据算法选定第一个汉字后,后续汉字的选择不再完全随机,而是根据前一个或两个字,从高频搭配中选择下一个字。这需要预先准备一个较大的语料库进行统计。虽然不能完全控制最终输出,但能显著增加文本的连贯性。
- 引入虚词和标点 :在编码输出的固定位置(如每3-5个实词后),随机插入“的”、“了”、“在”、“和”等虚词,以及逗号、句号。这能立刻打破机械感。
4.2 性能与安全考量
- 性能 :AES-GCM 的加解密是计算密集型操作,尤其是在移动设备上处理较长的文本时。务必在异步函数中执行,避免阻塞UI线程。字谱替换中的大整数运算(BigInt)也可能成为瓶颈,对于超长文本,可以考虑分块处理。
- 安全强化 :
- 密钥派生 :绝对不要直接用用户输入的字符串作为密钥。必须使用
PBKDF2或Argon2等密钥派生函数,并加入盐值(Salt)。 - 盐值和IV的存储 :盐值可以固定或与用户身份绑定后存储在本地;IV 每次加密随机生成,需要和最终的“中文密文”一起存储或传输。一个简单的方法是将 IV(和 Salt)进行 Base64 编码后,作为“中文密文”的标题或注释附加在一起,但这样会降低隐蔽性。更隐蔽的做法是将这些元数据也编码进字谱序列的特定位置(比如开头几个字用特殊规则表示长度和IV),但这会大大增加算法复杂度。
- 认证标签验证 :务必验证 GCM 的认证标签。这是防止密文被篡改的生命线。任何验证失败都必须立即中止,并给出明确的“认证失败”错误,而不是尝试输出破损的明文。
- 密钥派生 :绝对不要直接用用户输入的字符串作为密钥。必须使用
4.3 常见问题与调试实录
在开发过程中,我遇到了几个典型问题:
-
解密时认证失败(Authentication Failed)
- 现象 :解密函数抛出异常,提示标签验证错误。
- 排查 :
- 首先检查加密和解密使用的密钥是否 完全一致 (包括大小写、空格)。
- 检查
IV是否被正确传递。加密生成的IV必须原封不动地用于解密。在我的实现中,IV是encodeRobust输入数据的一部分,所以只要编码/解码过程无损,IV就不会丢。 - 检查“中文密文”在传输或展示过程中是否被修改。多一个空格、少一个字符、繁体简体转换,都会导致解码出的二进制数据完全不同。因此,显示和复制“中文密文”时要格外小心,最好提供“一键复制”按钮,避免手动选取出错。
- 解决 :在
AesGcmUtil.decrypt方法中捕获特定错误,给用户更友好的提示,如“密文可能已被意外修改,请检查输入文本是否完整无误”。
-
字谱替换后解码出的字节数组长度不对
- 现象 :
decodeRobust得到的字节数组长度比原始的encryptedBytes短,导致解密失败。 - 原因 :
BigInt转换过程中,字节数组开头如果是0x00,在转换成整数时会被忽略,导致信息丢失。 - 解决 :这是“进制转换”类编码的通用问题。解决方法是在编码前,在数据最前面添加一个表示 原始数据长度 的固定字节(例如2字节,可表示最大长度为65535)。解码时,先还原出完整的大整数并转为字节数组,再根据头部的长度信息截取出正确的原始数据部分。
- 现象 :
-
生成的“中文”过于生硬或包含生僻字
- 现象 :输出文本像乱码,一眼就能看出不正常。
- 解决 :
- 优化字谱 :剔除非常用字和敏感字,优先选择构词能力强的汉字。可以参考《现代汉语常用字表》。
- 引入后处理 :采用前面提到的“模板填充法”或“链式选择”,这是提升自然度最有效的手段。
- 调整分组大小 :尝试以3个字节为一组进行编码,这样映射到的数字范围是 0-16777215,对5000字的字谱取模后分布更均匀,可能减少极端索引值(对应生僻字)的出现概率。
-
鸿蒙 API 兼容性问题
- 现象 :在模拟器上运行正常,在部分真机上崩溃或返回错误。
- 排查 :鸿蒙系统版本和 API 版本可能存在差异。特别是
cryptoFramework这类系统级 API。 - 解决 :
- 仔细查阅官方文档,确认使用的 API 在目标设备的最低支持版本上是否可用。
- 在代码中添加详细的
try-catch和日志,定位具体出错的 API 调用。 - 考虑准备一个备用的、纯 JS 实现的加密降级方案(虽然安全性可能稍弱),以增强应用的兼容性。
这个项目从构思到实现,最大的乐趣在于将严谨的密码学和富有创意的编码方案结合。AES-GCM 提供了军级的安全基础,而“字谱替换”则是在此基础上进行的一场文本伪装游戏。它不适合传输大量数据,但在某些需要将密码“写在显眼处”却又不想被人察觉的场景下,或许能派上意想不到的用场。在鸿蒙上实现,更是一次对 ArkTS 和鸿蒙安全 API 的良好探索。如果你也感兴趣,不妨从优化字谱、设计更妙的句子模板开始,打造属于你自己的“暗语生成器”。
更多推荐


所有评论(0)