AES-GCM 同密钥下 IV 不可复用封面

HarmonyOS 7 ECIES 示例能解密,连续发两条消息却有风险?AES-GCM 的 IV 不能复用

华为在 API 26.0.0 的 ECIES 开发指导里给出了一次完整的演示:ECC 密钥协商,X963KDF 导出材料,再用 AES-GCM 加密和解密一条消息。演示一次能够解密,不等于把相同参数原封不动放进消息循环也安全。

我在把示例改成“同一会话连续加密多条消息”时,最先检查的不是加密函数是否返回成功,而是每次调用的 key + IV 是否重复。NIST SP 800-38D 对 GCM 明确要求:同一密钥下的 IV 必须满足唯一性约束。本文不把官网的一次性演示说成错误;风险发生在我们把示例中的派生 IV 固定下来,又在同一派生密钥下反复加密时。

适用边界:HarmonyOS 的 ECIES 指导标注从 API 26.0.0 开始。本文在本机运行了 Node.js AES-GCM 实验,验证的是 GCM 的通用密码学约束,不是声称在 API 26 设备上编译或跑通了 CryptoArchitectureKit。生产密钥生命周期和 IV 分配必须再经过安全评审。

先看官方示例里这两个值从哪里来

官方流程可读成四段:双方生成 ECC 密钥对,通过 createKeyAgreement('ECC256') 取得相同的共享材料;createKdf('X963KDF|SHA256') 将材料导成 32 字节;示例取前 16 字节作 GCM IV,后 16 字节作 AES-128 密钥;加密后把 GCM 认证标签交给解密端。

对于一对新生成密钥只加密这一条消息的演示,流程能够展示协商、导出、加密和认证的连接关系。但如果应用保持同一对参与协商的密钥,KDF 的输入与 info 也不变,再次从同一结果取 IV 和 AES 密钥,第二条消息将得到相同的 key + IV。GCM 不能这样用。实际工程里要把“密钥协商/派生”和“每条消息的 IV 分配”分开设计。

同一密钥下的 IV 复用与独立分配对比图

案例一:为什么两条消息不能共用 IV

下面的最小实验只使用 Node.js 标准库,方便任何开发机复现。它故意在同一 AES-128 密钥下复用一个 12 字节 IV,分别加密两段等长文本。注意:这段 badIv 仅用于演示禁用模式,不是可用的生产代码。

import assert from 'node:assert/strict';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

function seal(key, iv, plain) {
  const cipher = createCipheriv('aes-128-gcm', key, iv);
  const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
  return { ciphertext, tag: cipher.getAuthTag(), iv };
}

function open(key, packet) {
  const decipher = createDecipheriv('aes-128-gcm', key, packet.iv);
  decipher.setAuthTag(packet.tag);
  return Buffer.concat([decipher.update(packet.ciphertext), decipher.final()]);
}

function xor(a, b) {
  assert.equal(a.length, b.length);
  return Buffer.from(a.map((byte, i) => byte ^ b[i]));
}

const key = randomBytes(16);
const badIv = randomBytes(12);
const a = Buffer.from('order=000001;paid=Y');
const b = Buffer.from('order=000002;paid=N');
const badA = seal(key, badIv, a);
const badB = seal(key, badIv, b);
assert.deepEqual(xor(badA.ciphertext, badB.ciphertext), xor(a, b));
console.log('case 1: reused key+IV exposes plaintext XOR relationship');

const goodA = seal(key, randomBytes(12), a);
const goodB = seal(key, randomBytes(12), b);
assert.notDeepEqual(goodA.iv, goodB.iv);
assert.deepEqual(open(key, goodA), a);
assert.deepEqual(open(key, goodB), b);
console.log('case 2: distinct IVs decrypt independently');

const tampered = { ...goodA, ciphertext: Buffer.from(goodA.ciphertext) };
tampered.ciphertext[0] ^= 1;
assert.throws(() => open(key, tampered));
console.log('case 3: ciphertext tampering fails authentication');

保存为 gcm-iv-check.mjs,运行 node gcm-iv-check.mjs。第一行说明两段密文暴露了对应明文的异或关系;它不意味着仅凭这一步就恢复了全部明文,但已足够证明这组参数不可重复使用。后两行验证每条消息用不同 IV 可以分别解密,以及篡改密文会在认证阶段失败。

案例二:把一次性演示改成多消息格式

和 HarmonyOS API 对齐时,先保留官方的 ECC 协商与 X963KDF 派生步骤,再单独为每次 AES-GCM 加密调用分配 IV。下面只写参数管理的设计草图,避免复制整篇官方示例。它没有在 API 26 设备上编译验证,不应直接复制进生产工程:

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

interface EncryptedPacket {
  iv: cryptoFramework.DataBlob;
  aad: cryptoFramework.DataBlob;
  ciphertext: cryptoFramework.DataBlob;
  tag: cryptoFramework.DataBlob;
}

async function encryptOne(
  symKey: cryptoFramework.SymKey,
  plain: cryptoFramework.DataBlob
): Promise<EncryptedPacket> {
  const random: cryptoFramework.Random = cryptoFramework.createRandom();
  const params: cryptoFramework.GcmParamsSpec = {
    algName: 'GcmParamsSpec',
    iv: random.generateRandomSync(12),
    aad: { data: new Uint8Array(0) },
    authTag: { data: new Uint8Array(16) }
  };
  const cipher = cryptoFramework.createCipher('AES128|GCM');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, params);
  const ciphertext: cryptoFramework.DataBlob = await cipher.update(plain);
  params.authTag = await cipher.doFinal(null);
  return { iv: params.iv, aad: params.aad, ciphertext, tag: params.authTag };
}

每条消息的 iv、aad、ciphertext、tag 都是这条消息自己的;解密方要用相同的 IV/AAD/tag 做认证解密,而不能重新随机生成或拿上一条消息的值。还需在协议中提供让接收方重建相同共享密钥的公钥信息。上述代码只演示每条消息的 GCM 参数管理,不包含完整密钥交换、网络传输和持久化协议,也没有处理 update/doFinal 在不同分段加密场景下的输出拼接;真正接入时要依据目标 API 26 SDK 的类型检查和端到端用例补齐。

这里选 12 字节 IV 是 GCM 常见的 96 位长度,但“调用随机函数”并不自动等于在无限条消息上有绝对唯一性。要明确同一密钥的消息数量上限、并发分配和重启后的策略;做不到时应缩短密钥生命周期或设计可证明不重复的 IV 分配方式。不要把 IV 当秘密;需要保护的是密钥,IV 需要的是同一密钥下不复用。

解密失败该查什么

把 GCM 解密失败粗暴归因于“密钥不一致”会走弯路。我的排查顺序是:

  1. 先对比双方协商曲线、KDF 算法、info 与导出长度是否一致。
  2. 再对比该条消息的 IV、AAD、密文和认证标签是否被完整传输;标签不是重新生成的随机数。
  3. 若仍失败,检查是否在编码、分片、持久化或跨设备同步时改变了字节序列。
  4. 不要在认证失败后向业务层交付“已经解出的部分明文”;直接拒绝该消息并保留非敏感排障信息。

本机三个实验验证了 GCM 行为;API 26 的 cryptoFramework 实际编译、12 字节 IV 支持情况、不同设备互通和异常码需要在具备对应 SDK 的工程中继续验收。没有这些结果前,这段接口连接只能作为设计草图,不能贴上“量产可用”的标签。

记住这一条边界

HarmonyOS 7 的 ECIES 指导很适合学习“ECC 协商 + X963KDF + AES-GCM”如何接起来;一旦从一次性演示变成多条消息,最先补的是每次加密调用的 key/IV 唯一性管理与可传输的数据封装。能解密只是功能验收,重复消息、并发、重启和篡改测试才是安全验收的起点。

参考资料(核对日期:2026-09-18):

Logo

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

更多推荐