4. 设备影子敏感数据字段加密存储

设备影子敏感数据字段加密存储 学习卡片

为什么设备影子里的 Wi-Fi 密码必须加密

设备影子(Device Shadow)本质上是云端为每台设备保存的一份 JSON 状态文档,包含 desired、reported 和 delta 三部分。当用户通过 App 下发 Wi-Fi 配置时,密码会先写入 desired,设备上线后读取并回写 reported。问题在于:影子文档在云端以明文 JSON 存储,任何拥有 iot:GetThingShadow 权限的角色(运维、客服、日志系统)都能直接看到密码。本节的做法是:设备端用 KMS 生成的数据密钥对密码字段做 AES-256-GCM 加密,只把密文、IV 和认证标签写入影子,云端全程不接触明文,设备端用同一密钥解密读取。

加密字段的数据结构设计

不改变影子整体结构,只把 wifi.password 从明文字符串替换为一个加密信封对象,其余字段保持明文以便云端规则引擎和 App 正常读取 SSID、信道等信息:

{
  "state": {
    "desired": {
      "wifi": {
        "ssid": "Home-2.4G",
        "password": {
          "alg": "AES-256-GCM",
          "keyId": "alias/iot-shadow-wifi",
          "iv": "5f3a...(Base64,12字节)",
          "ct": "9c1d...(Base64 密文)",
          "tag": "a7b2...(Base64,16字节认证标签)"
        }
      }
    },
    "reported": { "wifi": { "ssid": "Home-2.4G", "password": { ...同上... } } }
  }
}

关键约束:iv 每次加密必须重新随机生成,绝不复用;keyId 用于标识密钥版本,便于后续轮换;tag 必须随密文一起存储,否则解密时无法验证完整性。

设备端加密上报与解密读取

设备端持有 KMS 数据密钥(通过 GenerateDataKey 获取明文密钥 + 密文密钥,明文密钥仅驻留内存)。上报前加密,读取后解密:

import os, json, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt_password(plain: str, data_key: bytes) -> dict:
    iv = os.urandom(12)                      # 每次加密重新生成
    aesgcm = AESGCM(data_key)
    ct_and_tag = aesgcm.encrypt(iv, plain.encode(), None)
    ct, tag = ct_and_tag[:-16], ct_and_tag[-16:]
    return {
        "alg": "AES-256-GCM",
        "keyId": "alias/iot-shadow-wifi",
        "iv": base64.b64encode(iv).decode(),
        "ct": base64.b64encode(ct).decode(),
        "tag": base64.b64encode(tag).decode(),
    }

def decrypt_password(env: dict, data_key: bytes) -> str:
    iv = base64.b64decode(env["iv"])
    ct = base64.b64decode(env["ct"])
    tag = base64.b64decode(env["tag"])
    aesgcm = AESGCM(data_key)
    return aesgcm.decrypt(iv, ct + tag, None).decode()

上报时把 encrypt_password() 的返回值填入 desired.wifi.password,通过 MQTT 发布到 $aws/things/{thingName}/shadow/update;设备读取影子时对 reported.wifi.password 调用 decrypt_password() 还原明文用于连接 Wi-Fi。

加密字段在影子中的流转关系

设备端 Wi-Fi 密码明文 "MyP@ssw0rd" AES-256-GCM 加密 iv 随机生成 输出 ct + tag 加密信封 alg / keyId / iv ct / tag 解密读取 还原明文连 Wi-Fi 云端设备影子 desired ssid: "Home-2.4G" password: {加密信封} reported ssid: "Home-2.4G" password: {加密信封} 云端仅见密文 无法还原明文 KMS 数据密钥(设备持有) MQTT over TLS 上报密文 设备读取影子 → 本地解密 KMS GenerateDataKey → 明文密钥 + 密文密钥

验证云端存储为密文

设备上报后,用具备 iot:GetThingShadow 权限的运维账号直接拉取影子文档,检查 desired.wifi.password 和 reported.wifi.password 是否均为加密信封对象,且 ct 字段中不含可读的密码字符:

aws iot-data get-thing-shadow \
  --thing-name "SmartLight-001" \
  --shadow-name "wifi-config" \
  --query "state.desired.wifi.password" output.json

# 预期输出(密文,非明文):
# {
#   "alg": "AES-256-GCM",
#   "keyId": "alias/iot-shadow-wifi",
#   "iv": "5f3a...",
#   "ct": "9c1d...",
#   "tag": "a7b2..."
# }

若 ct 中能直接看到 MyP@ssw0rd 或 Base64 解码后出现可读字符串,说明加密未生效,需检查设备端是否在写入影子前调用了 encrypt_password()。

验证设备端可解密读取

设备端从影子读取 reported.wifi.password 后调用 decrypt_password(),与原始明文比对。验证要点:

  • 解密成功:返回字符串与用户输入的 Wi-Fi 密码完全一致,设备用该密码成功关联 AP。
  • IV 唯一性:连续两次上报同一密码,两次的 iv 和 ct 必须不同,证明每次加密都重新生成 IV。
  • 篡改检测:手动修改影子中 ct 的任意一个字符后再解密,AES-GCM 应抛出认证失败异常(InvalidTag),证明完整性保护有效。
  • 密钥隔离:用另一台设备的密钥解密本设备密文,应失败,证明密钥与设备绑定。
KMS 数据密钥的明文部分只应在设备内存中使用,不要写入磁盘或日志。设备重启后可通过 KMS Decrypt 接口用密文密钥重新获取明文密钥,避免长期驻留。
不要对影子中所有字段统一加密。SSID、信道等字段若加密,云端规则引擎和 App 将无法直接读取,反而破坏影子作为“共享状态文档”的用途。只加密密码、Token、私钥等真正的敏感字段。