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。
加密字段在影子中的流转关系
验证云端存储为密文
设备上报后,用具备 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),证明完整性保护有效。 - 密钥隔离:用另一台设备的密钥解密本设备密文,应失败,证明密钥与设备绑定。
Decrypt 接口用密文密钥重新获取明文密钥,避免长期驻留。