第4章 密钥分层体系:主密钥(KEK)、工作密钥(DEK)、密钥派生函数与轮换策略
4.1 组件边界
密钥分层组件位于应用层与底层安全硬件之间,负责定义和管理密钥的生成、派生、加密存储、使用与轮换。其核心边界如下:
- 上层接口:通过密钥 ID(UUID)对外提供加密/解密操作,不暴露裸密钥材料。
- 下层依赖:Android KeyStore / TEE / StrongBox 提供根信任锚点(Root of Trust)。
- 数据边界:仅接触加密后的密钥材料(KEK 加密 DEK)和派生中间数据,不读取用户明文数据。
- 生命周期边界:管理密钥从创建、激活、冻结到销毁的完整状态机。
4.2 系统分层
| 层级 | 角色 | 存储位置 | 示例 |
|---|---|---|---|
| 密钥存储层 | 根信任锚点 | Android KeyStore / TEE | 主密钥 KEK(硬件绑定时不可导出) |
| 密钥管理层 | 派生、加密、缓存 DEK | 应用私有文件 / EncryptedSharedPreferences | DEK 被 KEK 加密后持久化 |
| 密码学使用层 | 执行加解密 | 应用堆内存(零化策略) | 使用内存中的 DEK 调用 AES-GCM |
4.3 数据流
- 加密流程:应用请求加密数据 → 密钥管理组件通过 ID 查找被 KEK 加密的 DEK → 用 TEE 内的 KEK 解密 DEK → 使用 DEK 在内存中执行 AES-GCM 加密 → 返回密文并零化 DEK。
- 解密流程:同上逆向,需验证 GCM 认证标签。
- 派生流程:用户输入口令 → 调用 scrypt/PBKDF2 生成 KEK → 存储 KEK 至 KeyStore 或直接用作根密钥。
- 轮换流程:标记旧 DEK 为“过期” → 为新数据使用新 DEK → 后台任务用新 DEK 重新加密旧密文 → 删除旧 DEK 密文。
4.4 信任链路
信任边界从 Boot ROM → TEE → Android KeyStore → 密钥管理库 → 应用进程。每一层通过签名验证和隔离机制递进信任,其中 KEK 由硬件安全模块保护,DEK 仅以加密形态存在于应用文件系统中。用户口令派生时,派生函数在应用空间执行,结果受 OS 内存隔离保护。
4.5 密钥派生函数选型权衡
| 属性 | PBKDF2 | scrypt |
|---|---|---|
| 迭代轮数 | 可配置(推荐 100k+) | 可配置 |
| 内存消耗 | 低(固定) | 高(可配置,推荐 64MiB) |
| GPU/ASIC 抗性 | 较弱 | 较强(内存硬) |
| Android 兼容性 | 普遍支持(API 1+) | 需 API 26+ 或引入 Bouncy Castle |
| 推荐场景 | 低算力设备、快速原型 | 高安全要求、用户口令派生 KEK |
建议:对于用户口令派生,优先使用 scrypt(内存成本 ≥ 64MiB,CPU 成本 ≥ 2^14);若需兼容低版本,退而使用 PBKDF2 且迭代次数 ≥ 600k。
4.6 风险点
- KEK 泄露:所有 DEK 均可被解密,导致数据批量暴露。需依赖 KeyStore 硬件隔离。
- 派生函数参数弱化:迭代次数过低、内存成本过小,可被暴力破解。
- 轮换期间数据不一致:若未保留旧 DEK 直至所有密文完成重加密,会导致数据不可解密。
- 密钥存储未加密:DEK 若直接以明文写入 SharedPreferences,则任意有读权限的进程均可窃取。
4.7 落地步骤
- 初始化根密钥
使用 Android KeyStore 生成 256 位 AES 密钥作为 KEK,且设置KeyGenParameterSpec.Builder.setIsStrongBoxBacked(true)并禁用导出。 - 实现 DEK 生成与加密存储
每次为新数据集生成随机 AES-256-GCM 密钥作为 DEK,用 KEK 加密后存储到 JSON 文件或 EncryptedSharedPreferences。 - 集成密钥派生函数
对于用户口令场景,使用 scrypt(可通过javax.crypto.SecretKeyFactory或第三方库)生成固定位数的 KEK,再注入 KeyStore 或直接作为加密密钥。 - 实现密钥轮换
设计双缓冲策略:保留当前 DEK 和上一个版本的 DEK;轮换时生成新 DEK,标记旧 DEK 为“待回收”,所有写操作使用新 DEK,后台任务逐步重加密旧数据。 - 集成测试与回归
验证:派生耗时 ≥ 200ms;轮换期间并发读写不丢数据;KEK 不可通过KeyStore.getEntry泄露;DEK 密文完整性标签校验通过。
// 示例:在 Android KeyStore 中生成不可导出的 KEK
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"master_key_v1",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true)
.build();
KeyGenerator kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
kg.init(spec);
SecretKey kek = kg.generateKey();
4.8 验收标准
| 检查项 | 预期结果 |
|---|---|
| KEK 导出测试 | 调用 KeyStore.getKey() 返回 null 或 UnrecoverableKeyException |
| DEK 加密存储完整性 | 篡改 DEK 密文后解密失败(GCM 认证标签校验) |
| 派生函数延迟 | scrypt (N=16384, r=8, p=1) 平均执行时间 ≥ 200ms |
| 轮换并发正确性 | 轮换过程中新数据使用新 DEK,旧数据仍可用旧 DEK 解密,重加密完成后旧 DEK 被安全删除 |
| 内存零化 | DEK 使用后显式调用 Arrays.fill(sek, (byte)0) 或使用 SecureZero |
注意:密钥轮换时,必须保留旧 DEK 直到引用该 DEK 的所有密文都被重新加密或过期。建议使用“延迟删除”策略,在后台线程中逐步迁移。