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)将加密和认证一步到位,是我选择它的核心原因。

  1. 加密与认证一体化 :GCM 模式在加密的同时,会生成一个认证标签(Authentication Tag)。在解密时,会先验证这个标签,只有标签正确才进行解密。这能有效防止密文被篡改,如果攻击者改动了密文中的一个字节,解密时验证会失败,系统会抛出异常,而不是输出一个错误但可能被误解的明文。这对于我们后续的替换步骤至关重要——我们必须确保用于替换的“原料”是完整且未被篡改的。
  2. 需要初始向量(IV) :GCM 模式要求一个每次加密都不同的、非重复的初始向量。通常推荐使用密码学安全的随机数生成器(CSPRNG)生成 12 字节的 IV。这个 IV 不需要保密,可以和密文一起存储或传输。在我们的项目中,IV 会作为后续字谱替换的“种子”或参数之一,确保即使同一明文、同一密钥,每次加密产生的“中文句子”都不同,增加观察者分析的难度。
  3. 关联数据(AAD) :GCM 支持可选的关联数据,这部分数据会被认证但不被加密。我们可以利用这个特性,将一些不影响解密但有助于验证的上下文信息(比如加密时间、版本号)放进去,进一步提升安全性。

注意 :密钥(Key)的管理是安全的核心。在这个演示项目中,为了简化,密钥可能由用户输入或硬编码。但在真实生产环境中,密钥必须通过安全的密钥派生函数(如 PBKDF2, Argon2)从强密码生成,并妥善存储在安全的密钥库中,绝不能明文存储。

2.2 字谱替换:将二进制密文“翻译”成中文

这是项目中最具创意和挑战性的部分。AES-GCM 输出的密文和认证标签是一串二进制数据(通常表示为字节数组或十六进制字符串)。我们的目标是将这串“乱码”映射成一个由合法汉字构成的序列。

我将其称为“字谱替换”,灵感来源于密码学中的替换密码(Substitution Cipher),但规模和应用逻辑要复杂得多。

  1. 核心挑战 :二进制数据是任意的,而汉字是离散的字符集。我们需要建立一个双向、确定性的映射规则。

  2. 基本思路

    • 编码(加密后) :将密文(含Tag)的字节数组,通过特定算法,转换为一串索引号。每个索引号对应一个预定义“字谱”中的一个汉字。
    • 解码(解密前) :将收到的一串汉字,根据相同的“字谱”和算法,反向转换回字节数组,然后交给 AES-GCM 解密。
  3. 算法设计考量

    • 字谱规模 :常用汉字约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 垂直布局,包含以下几个核心组件:

  1. 文本输入框 ( TextArea ) :用于输入待加密的明文或待解密的“伪装中文”。设置 placeholder 属性提示用户。
  2. 密钥输入框 ( TextInput ) :用于输入加密密钥。为了安全,可以将其 type 属性设置为 InputType.Password ,显示为圆点。
  3. 操作按钮 ( Button ) :两个按钮,分别触发“加密”和“解密”操作。
  4. 结果显示框 ( Text ) :用于显示加密后生成的“中文句子”或解密后的原始明文。可以设置为多行、可滚动。
  5. 状态提示 ( 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. 模板填充法 :预置多个优美的古诗词或常见句子模板,其中包含一些占位符。将编码生成的汉字序列,按顺序填充到这些占位符中。例如,模板“ 1 2 3 4 5 6 。”,用6个生成的汉字替换 __1__ __6__ 。这种方法能快速生成结构工整的文本。
  2. 基于词频的链式选择 :建立一个汉字二元或三元词频表。编码时,在根据算法选定第一个汉字后,后续汉字的选择不再完全随机,而是根据前一个或两个字,从高频搭配中选择下一个字。这需要预先准备一个较大的语料库进行统计。虽然不能完全控制最终输出,但能显著增加文本的连贯性。
  3. 引入虚词和标点 :在编码输出的固定位置(如每3-5个实词后),随机插入“的”、“了”、“在”、“和”等虚词,以及逗号、句号。这能立刻打破机械感。

4.2 性能与安全考量

  1. 性能 :AES-GCM 的加解密是计算密集型操作,尤其是在移动设备上处理较长的文本时。务必在异步函数中执行,避免阻塞UI线程。字谱替换中的大整数运算(BigInt)也可能成为瓶颈,对于超长文本,可以考虑分块处理。
  2. 安全强化
    • 密钥派生 :绝对不要直接用用户输入的字符串作为密钥。必须使用 PBKDF2 Argon2 等密钥派生函数,并加入盐值(Salt)。
    • 盐值和IV的存储 :盐值可以固定或与用户身份绑定后存储在本地;IV 每次加密随机生成,需要和最终的“中文密文”一起存储或传输。一个简单的方法是将 IV(和 Salt)进行 Base64 编码后,作为“中文密文”的标题或注释附加在一起,但这样会降低隐蔽性。更隐蔽的做法是将这些元数据也编码进字谱序列的特定位置(比如开头几个字用特殊规则表示长度和IV),但这会大大增加算法复杂度。
    • 认证标签验证 :务必验证 GCM 的认证标签。这是防止密文被篡改的生命线。任何验证失败都必须立即中止,并给出明确的“认证失败”错误,而不是尝试输出破损的明文。

4.3 常见问题与调试实录

在开发过程中,我遇到了几个典型问题:

  1. 解密时认证失败(Authentication Failed)

    • 现象 :解密函数抛出异常,提示标签验证错误。
    • 排查
      • 首先检查加密和解密使用的密钥是否 完全一致 (包括大小写、空格)。
      • 检查 IV 是否被正确传递。加密生成的 IV 必须原封不动地用于解密。在我的实现中, IV encodeRobust 输入数据的一部分,所以只要编码/解码过程无损, IV 就不会丢。
      • 检查“中文密文”在传输或展示过程中是否被修改。多一个空格、少一个字符、繁体简体转换,都会导致解码出的二进制数据完全不同。因此,显示和复制“中文密文”时要格外小心,最好提供“一键复制”按钮,避免手动选取出错。
    • 解决 :在 AesGcmUtil.decrypt 方法中捕获特定错误,给用户更友好的提示,如“密文可能已被意外修改,请检查输入文本是否完整无误”。
  2. 字谱替换后解码出的字节数组长度不对

    • 现象 decodeRobust 得到的字节数组长度比原始的 encryptedBytes 短,导致解密失败。
    • 原因 BigInt 转换过程中,字节数组开头如果是 0x00 ,在转换成整数时会被忽略,导致信息丢失。
    • 解决 :这是“进制转换”类编码的通用问题。解决方法是在编码前,在数据最前面添加一个表示 原始数据长度 的固定字节(例如2字节,可表示最大长度为65535)。解码时,先还原出完整的大整数并转为字节数组,再根据头部的长度信息截取出正确的原始数据部分。
  3. 生成的“中文”过于生硬或包含生僻字

    • 现象 :输出文本像乱码,一眼就能看出不正常。
    • 解决
      • 优化字谱 :剔除非常用字和敏感字,优先选择构词能力强的汉字。可以参考《现代汉语常用字表》。
      • 引入后处理 :采用前面提到的“模板填充法”或“链式选择”,这是提升自然度最有效的手段。
      • 调整分组大小 :尝试以3个字节为一组进行编码,这样映射到的数字范围是 0-16777215,对5000字的字谱取模后分布更均匀,可能减少极端索引值(对应生僻字)的出现概率。
  4. 鸿蒙 API 兼容性问题

    • 现象 :在模拟器上运行正常,在部分真机上崩溃或返回错误。
    • 排查 :鸿蒙系统版本和 API 版本可能存在差异。特别是 cryptoFramework 这类系统级 API。
    • 解决
      • 仔细查阅官方文档,确认使用的 API 在目标设备的最低支持版本上是否可用。
      • 在代码中添加详细的 try-catch 和日志,定位具体出错的 API 调用。
      • 考虑准备一个备用的、纯 JS 实现的加密降级方案(虽然安全性可能稍弱),以增强应用的兼容性。

这个项目从构思到实现,最大的乐趣在于将严谨的密码学和富有创意的编码方案结合。AES-GCM 提供了军级的安全基础,而“字谱替换”则是在此基础上进行的一场文本伪装游戏。它不适合传输大量数据,但在某些需要将密码“写在显眼处”却又不想被人察觉的场景下,或许能派上意想不到的用场。在鸿蒙上实现,更是一次对 ArkTS 和鸿蒙安全 API 的良好探索。如果你也感兴趣,不妨从优化字谱、设计更妙的句子模板开始,打造属于你自己的“暗语生成器”。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐