4. 密钥存储安全:硬件安全元件(SE/TEE)、安全文件系统、密钥派生(KDF)、密钥包装与加密存储
密钥生成出来之后,放哪儿?这是个大问题。
很多人觉得,把密钥写死在代码里,或者存到一个配置文件里,不就完事了吗?我早年也这么干过,结果被安全审计怼得哑口无言。说白了,密钥存储是设备安全的第一道防线。密钥都守不住,后面所有密码算法都是白搭。
这一章,我们聊聊四种主流的密钥存储方案。从最硬的硬件安全元件,到纯软件的加密文件系统,再到密钥派生和包装技术。每种方案都有它的适用场景,也有它的坑。
4.1 硬件安全元件(SE)与可信执行环境(TEE)
先讲最硬的方案——硬件安全元件(SE)。
SE 是一个独立的芯片,有自己的 CPU、存储和加密引擎。密钥从生成到使用,全程在芯片内部完成,外界根本拿不到。我参与过一个智能电表项目,要求密钥必须防物理拆解。当时选的就是 SE,因为攻击者就算把芯片磨到只剩几微米,也读不到密钥。
核心要点:SE 提供的是物理级隔离。密钥永远不出芯片边界。
但 SE 也有缺点:贵,而且开发周期长。这时候,TEE 就是一个折中方案。
TEE 是在主处理器内部划出一个安全区域。它和普通操作系统(REE)是隔离的,但共享同一颗芯片。TEE 的典型实现有 ARM TrustZone、Intel SGX。我建议,如果设备对成本敏感,但又需要一定级别的安全保护,TEE 是首选。
| 特性 | SE(硬件安全元件) | TEE(可信执行环境) |
|---|---|---|
| 隔离级别 | 物理隔离(独立芯片) | 逻辑隔离(同芯片内) |
| 抗物理攻击 | 强(防探针、防拆解) | 中等(依赖芯片设计) |
| 成本 | 高(额外芯片和封装) | 低(集成在主SoC中) |
| 开发复杂度 | 高(需要专用驱动和协议) | 中等(有标准API如GlobalPlatform) |
| 典型场景 | 金融支付、身份认证 | DRM、移动支付、生物识别 |
注意:TEE 不是万能的。如果主芯片本身有硬件漏洞(比如熔断、幽灵),TEE 也可能被攻破。我曾经见过一个案例,攻击者通过电压毛刺攻击,绕过了 TEE 的内存隔离。所以,TEE 只能防软件攻击,对高级物理攻击还是得靠 SE。
4.2 安全文件系统
如果设备没有 SE 或 TEE,怎么办?那就得靠软件方案了。安全文件系统是常见的选择。
安全文件系统的核心思路是:对存储的密钥文件进行加密,同时加上完整性校验。常见的实现有 Linux 的 eCryptfs、dm-crypt,以及一些轻量级嵌入式文件系统。
我个人习惯,在 IoT 设备上使用 eCryptfs。它工作在 VFS 层,对上层应用透明。你写一个文件,它自动加密;你读一个文件,它自动解密。密钥可以绑定到设备的硬件特征(比如芯片唯一ID),这样即使把存储芯片拆下来,也解不开。
经验之谈:安全文件系统的密钥本身也需要保护。我一般把文件系统的主密钥用设备绑定的硬件密钥加密后存储。这样,攻击者即使拿到文件系统镜像,也找不到明文密钥。
但安全文件系统有个致命弱点:密钥在运行时是明文存在于内存中的。如果攻击者能拿到 root 权限,就能 dump 内存,拿到密钥。所以,安全文件系统只能防离线攻击,防不了在线攻击。
4.3 密钥派生(KDF)
很多时候,我们不需要存储一个完整的密钥。我们可以从一个低熵的秘密(比如用户密码)派生出一个高强度的密钥。这就是密钥派生函数(KDF)的用武之地。
KDF 的核心是:拉伸。把短密码变成长密钥,同时增加计算开销,让暴力破解变得不划算。
常用的 KDF 有:
- PBKDF2:经典方案,通过多次哈希迭代增加计算时间。我建议迭代次数至少 100000 次。
- bcrypt:内置 salt 和自适应成本因子,抗 GPU 攻击比 PBKDF2 好。
- scrypt:除了计算开销,还增加了内存开销,让 ASIC 攻击更难。
- Argon2:2015 年密码哈希竞赛冠军,目前最推荐。它有三个变体:Argon2d(抗 GPU)、Argon2i(抗侧信道)、Argon2id(混合)。
举个例子,用 Argon2id 从密码派生一个 256 位密钥:
// 伪代码示例
salt = random_bytes(16)
key = argon2id(
password: "user_password",
salt: salt,
time_cost: 3, // 迭代次数
memory_cost: 65536, // 64 MB 内存
parallelism: 4, // 并行线程数
key_length: 32 // 输出 256 位密钥
)
关键点:KDF 的输出是确定性的。同样的密码 + 同样的 salt = 同样的密钥。所以 salt 必须随机生成,并且和密文一起存储。我见过有人把 salt 写死在代码里,那跟没加 salt 一样。
4.4 密钥包装与加密存储
最后一种方案,也是最灵活的——密钥包装(Key Wrapping)。
密钥包装,说白了就是用另一个密钥(KEK,密钥加密密钥)来加密目标密钥。这样,我们只需要保护 KEK 的安全,其他密钥可以公开存储。
NIST 专门定义了 SP 800-38F 标准,推荐使用 AES-KW 算法。AES-KW 基于 AES 加密,输出长度只比输入多 8 字节(用于完整性校验)。
我一般这样设计密钥层次:
- 主密钥(MK):存储在 SE 或 TEE 中,永远不导出。
- 密钥加密密钥(KEK):由 MK 派生或解密得到,用于包装其他密钥。
- 工作密钥(WK):实际用于加解密数据的密钥,被 KEK 包装后存储在文件系统或数据库中。
这样做的好处是:更换工作密钥时,只需要重新包装,不需要动主密钥。而且,工作密钥可以定期轮换,不影响上层业务。
避坑指南:我曾经在一个项目中,直接用 AES-ECB 模式包装密钥。结果因为密钥块太短(16字节),出现了模式碰撞。后来改用 AES-KW,问题才解决。记住,密钥包装不是简单的 AES 加密,必须用专门的算法。
4.5 整体架构与决策流程
讲了这么多,到底怎么选?我画了一张图,帮你理清思路。
嗯,这张图基本覆盖了常见场景。但实际项目中,往往是组合使用。比如,用 SE 保护 KEK,然后用 KEK 包装工作密钥,工作密钥再通过 KDF 派生会话密钥。层次越多,安全性越高,但性能开销也越大。
我的建议:不要过度设计。如果设备只是传输温度数据,用安全文件系统就够了。如果是金融支付设备,必须上 SE。安全是成本、性能和风险的平衡。
4.6 常见陷阱与避坑指南
最后,分享几个我踩过的坑。
- 密钥硬编码:我见过有人把 AES 密钥直接写在 C 代码里。反编译一下,字符串就出来了。别这么干。
- 忽略密钥更新:密钥用十年不换?那跟没加密一样。建议定期轮换,至少一年一次。
- 混淆密钥用途:签名密钥和加密密钥用同一个?这是大忌。一旦一个被攻破,另一个也完蛋。每个密钥要有明确的用途标签。
- 忘记备份:密钥丢了,数据就废了。我曾经帮一个客户恢复数据,因为他们的 SE 坏了,密钥拿不出来。后来我们强制要求:密钥必须用 KEK 包装后备份到安全存储中。
重要:密钥备份和密钥存储是两回事。备份是为了恢复,存储是为了使用。备份的密钥必须用更强的密钥保护,而且备份介质要物理隔离。
好了,密钥存储就聊到这里。记住一句话:密钥在哪里,安全就在哪里。选对方案,比选对算法更重要。