跳转到主内容AEAD 最怕 nonce 用错 | 物联网民工

AEAD 最怕 nonce 用错

设备通信里经常会说“用 AES 加密一下”。这句话太粗了。

真正落地时要问的是:只加密内容,还是同时认证内容?是否能防篡改?是否防重放?nonce 从哪里来?设备断电重启后会不会复用同一个 nonce?

现代协议里更常用的是 AEAD,而不是裸加密。

AEAD = Authenticated Encryption with Associated Data
     = 加密 + 完整性认证 + 可选明文关联数据认证

常见 AEAD 算法包括:

  • AES-GCM
  • AES-CCM
  • ChaCha20-Poly1305

AEAD 很适合 IoT 和网络协议,但它有一个硬边界:同一把密钥下,nonce 不能复用。

对称加密解决的是共享密钥下的保密

对称加密使用同一把密钥加密和解密:

plaintext + key -> ciphertext
ciphertext + key -> plaintext

它适合设备和服务器已经共享会话密钥或设备密钥的场景。

但“加密”本身只解决机密性。攻击者看不懂内容,不等于攻击者不能篡改密文。

如果只用传统加密模式,不做认证,可能出现:

  • 密文被改,解密后得到被篡改的数据
  • padding oracle 这类协议攻击
  • 接收方无法确认数据是否完整
  • 错误被延迟到业务层才表现出来

所以现代设计里,通常不建议自己拼“加密 + 校验”。更稳的方式是使用 AEAD。

AEAD 同时保护密文和关联数据

AEAD 输入通常包括:

key
nonce
plaintext
AAD

输出:

ciphertext
tag

其中:

  • plaintext 会被加密
  • AAD 不加密,但会被认证
  • tag 用来验证密文和 AAD 是否被篡改

AAD 适合放协议头里不能加密但必须防篡改的字段,例如:

  • device_id
  • message_type
  • protocol_version
  • sequence_number
  • topic
  • header flags

接收方必须先验证 tag,再信任解密结果。tag 验证失败时,不应该把明文交给业务层。

nonce 不是密钥,但必须唯一

nonce 有时也叫 IV。它通常不需要保密,但必须满足算法要求。

对很多 AEAD 模式来说,最关键要求是:

同一把 key 下,nonce 不能重复

nonce 重复的后果不是“安全性稍微下降”,而可能是灾难性的。

以流加密或类流加密结构为例,同一 key 和 nonce 会生成同一段密钥流。两个明文用同一密钥流加密,攻击者可以从两个密文之间推出明文关系。

对 AES-GCM 这类模式,nonce 复用还可能破坏认证安全,使攻击者有机会伪造 tag。

所以 nonce 不是普通随机填充,也不是“随便给个时间戳”。它是协议状态的一部分。

设备断电会让 nonce 设计变难

服务器上维护单调计数器相对容易。嵌入式设备更麻烦:

  • 掉电可能回滚 RAM 里的计数器
  • flash 写计数器有磨损和掉电一致性问题
  • RTC 时间可能不准或被重置
  • 设备恢复出厂可能清掉状态
  • 多线程或中断并发发送可能抢同一个计数
  • 多个通信通道可能共用同一把 key

如果设备用 (key, counter) 生成 nonce,但 counter 只存在 RAM 里,重启后从 0 开始,就会在同一 key 下复用 nonce。

这类 bug 通常不会表现成“加密失败”,而是让安全性悄悄失效。

nonce 的常见设计方式

常见方式有几类。

第一,单调计数器:

nonce = fixed_prefix || counter

优点是不会碰撞,前提是 counter 不回滚、不并发重复。难点是持久化和掉电一致性。

第二,随机 nonce:

nonce = random(96 bits)

优点是无需持久计数,前提是随机数质量足够、碰撞概率可接受。难点是小设备早期启动熵不足,不能用伪随机凑数。

第三,设备唯一前缀加计数器:

nonce = device_or_session_prefix || message_counter

适合多设备或多会话,关键是 prefix 和 key 使用范围要一致,counter 仍要保证不回滚。

第四,会话密钥加会话内计数器:

long_term_key -> key exchange / KDF -> session_key
session_key + per-session counter -> AEAD nonce

这种方式常见于安全协议。长期密钥不直接用于大量数据加密,每次会话派生新 key,nonce 只需在会话内不重复。

AAD 不加密,但参与认证

AAD 很容易被误解成“附带数据,不重要”。

它不加密,所以攻击者能看到;但它参与 tag 计算,所以攻击者不能悄悄改。

典型用途是保护协议上下文:

AAD = device_id || message_type || sequence_number
plaintext = sensor_payload

这样接收方能确认:这份密文确实绑定到这个设备、这个消息类型和这个序号。

如果 sequence number 不进 AAD,攻击者可能不能解密 payload,但有机会重排或替换协议头。业务层看到的上下文就可能被污染。

AEAD 不自动防重放

AEAD 能发现密文和 AAD 被篡改,但不自动阻止攻击者重复发送旧报文。

如果攻击者录下一条合法报文:

nonce, AAD, ciphertext, tag

之后原样重放,tag 仍然有效。AEAD 没法仅靠自己知道“这条消息以前已经用过”。

防重放通常需要协议层状态:

  • 单调序号
  • 滑动窗口
  • 时间戳加窗口
  • 会话 ID
  • challenge/response
  • 服务端记录最近 nonce 或 counter

所以 nonce 唯一性和重放防护要一起设计。nonce 不重复是加密安全要求;拒绝旧消息是协议状态要求。

tag 验证失败不能继续处理

AEAD 解密时,tag 验证失败应当视为认证失败。

工程上要避免:

  • tag 失败后仍把部分明文交给业务层
  • 根据解密错误细节返回不同错误,形成 oracle
  • 重试时复用同一 nonce 发送不同明文
  • 记录过多敏感明文或密钥材料到日志
  • 把 tag 截断到过短长度

tag 是认证结果,不是普通校验和。验证失败说明密文、AAD、nonce 或 key 至少有一处不匹配,业务层不应该猜。

什么时候不用 AEAD

大多数新设计应该优先 AEAD,但也有例外:

  • 只做文件公开 checksum:hash 足够
  • 只做消息认证不加密:HMAC/CMAC 可能更合适
  • 协议已经规定了组合方式:遵守协议
  • 硬件只支持某种受限模式:要额外评估认证和 nonce 设计
  • 密钥包裹、磁盘加密等特殊场景:有专用模式和约束

不要为了“用 AES”而选择 AES-CBC 再自己拼 HMAC,除非你清楚顺序、padding、错误处理和兼容要求。新协议一般应使用成熟 AEAD。

现场排查顺序

遇到设备加密通信偶发失败、重启后异常、服务器报 tag 错,可以按这条链看:

使用的是不是 AEAD
-> key 的作用域是什么
-> nonce 是否在同一 key 下唯一
-> 设备重启后 nonce 是否回滚
-> 并发发送是否可能复用 counter
-> AAD 是否覆盖关键协议头
-> 接收端是否做重放检查
-> tag 验证失败后是否立即丢弃
-> 随机数是否在早期启动时可用

如果问题只在断电、恢复出厂、批量设备、长时间运行后出现,重点看 nonce 持久化、key 轮换和计数器边界。

真正要记住的边界

AEAD 是现代设备通信里非常好的默认选择,因为它把加密和认证放在一个清晰接口里。

但 AEAD 的安全不是“调用了 AES-GCM”就自动成立。它依赖 key、nonce、AAD、tag 和重放状态一起正确使用。

同一 key 下 nonce 不能复用,AAD 要覆盖不能被篡改的上下文,tag 失败不能继续处理,重放要由协议状态解决。对设备来说,最难的往往不是算法本身,而是断电、重启、并发和长期运行下,nonce 仍然不重复。