BitzOrcas 使用 ASP.NET Core Data Protection 保护应用需要“日后解开”的短小机密,例如 AI Provider API Key、Website 线索敏感字段和实验分组标识。它不是密码哈希、TLS、数据库透明加密或云 KMS 的替代品。
保护链路
密文通常包含密钥标识和完整性信息。解密方必须拥有同一应用密钥环和相同 purpose 链;仅复制密文或 Redis 连接串不足以互通。
当前注册行为
API 在构建容器前调用 AddBitzOrcasDataProtection。连接串按 ConnectionStrings:redis(Aspire 注入)优先、Redis:ConnectionString 回退的顺序读取。
// 在组合根注册一次,业务模块只依赖 IDataProtectionProvider。builder.Services.AddBitzOrcasDataProtection(builder.Configuration);
// 每个业务用途创建稳定、独立的 purpose,避免跨域解密。var protector = provider.CreateProtector( "BitzOrcas.Website.Leads.SensitiveFields.v1");没有 Redis 连接串时调用框架默认 AddDataProtection()。当前源码注释把它称为内存密钥环,但实际默认仓库还受运行环境与框架配置影响;说明书把这个分支视为“非共享默认仓库”,不承诺重启或跨实例互通。
存在 Redis 连接串时,注册逻辑在 KeyManagementOptions 上安装 RedisXmlRepository,固定 key 为 bitzorcas:dataprotection:keys。它在运行时解析 IConnectionMultiplexer,用于兼容 Aspire 的延迟连接。
必须理解的回退边界
如果 Redis 连接串存在但容器中没有 IConnectionMultiplexer,配置器会直接返回并保留框架默认仓库,不会让 Host 因此崩溃。这种容错适合本地组合,却意味着“配置存在”不等于“生产密钥环已经共享”。
当前实现还没有给 Data Protection 设置显式 Application Name,也没有在应用层对 Redis 中的 XML key material 再做 KMS/HSM wrapping。跨不同部署共享同一 Redis 前缀会扩大解密边界;生产应通过独立 Redis 账号、网络、数据库或未来的应用名/前缀配置实现隔离。
选择正确的保护方式
| 数据 | 正确机制 | 原因 |
|---|---|---|
| 用户密码(传输) | RSA-OAEP-SHA256 客户端加密 + 服务端解密 | 抗 TLS 终止后 body 日志与重放;见 登录安全 |
| 用户密码(存储) | 专用慢哈希(BCrypt) | 不需要恢复明文 |
| API Key / SCIM token 的校验值 | SHA-256 hash + 固定时间比较 | 运行时只需验证 |
| 第三方 Provider secret | Data Protection 或外部 Secret 引用 | 业务需要取回使用 |
| JWT/Webhook 根签名密钥 | Secret Store / KMS / HSM | 独立轮换与访问控制 |
| 数据库/对象存储 | 平台静态加密 | 大规模持久数据保护 |
| 传输内容 | TLS | 防止链路窃听与篡改 |
Data Protection 适合应用拥有、体积小、必须恢复的值。不要用它批量加密大文件,也不要以可逆保护替代密码哈希。
Purpose 设计
Purpose 是密码学隔离的一部分,应包含产品、模块、用途和版本。例如:
// 写入端和读取端必须使用完全相同的 purpose 链。var protector = provider .CreateProtector("BitzOrcas.AIManage") .CreateProtector("ProviderApiKey") .CreateProtector("v1");
// Protect 的结果可以落库;日志中不得记录原值或解密结果。var protectedValue = protector.Protect(apiKey);修改 purpose 等价于切换解密域。需要升级格式时,应先支持读取旧版本并写入新版本,完成迁移后再删除旧读取路径;不能直接重命名常量。
密钥生命周期
新 key 会按框架策略产生,旧 key 必须保留以解开历史载荷。轮换不是删除旧 key,而是让新写入使用新 key,同时保留旧 key 的解密能力。只有确认所有依赖载荷已过期或重加密后,才可撤销历史 key。
Redis key ring 本身也是关键数据:
- 只允许应用工作负载账号读写指定前缀;
- Redis 链路启用认证和传输保护;
- 将 key ring 纳入备份、恢复和变更审计;
- 不在普通缓存清理脚本中删除该 key;
- 区分开发、测试、预生产和生产的密钥环。
滚动发布演练
发布前用两个不同进程或版本完成双向验证,而不是只在单元测试中用同一 provider round-trip。
旧实例 A Protect(v1) → 新实例 B Unprotect 成功新实例 B Protect(v1) → 旧实例 A Unprotect 成功重启 A/B → 两边仍能解开历史探针暂时阻断 Redis → 告警并确认既定故障策略恢复 Redis/密钥环 → 历史数据再次可读如果业务载荷长期存在,还要从数据库抽取一批历史密文做只读验证。探针值应是无业务意义的随机文本,不能把真实 Secret 暴露到测试日志。
故障处理
出现 CryptographicException 时先区分 purpose 不一致、key 丢失、部署使用了不同仓库、载荷损坏和应用版本格式不兼容。不要捕获异常后把密文当作明文继续使用,也不要自动生成新 Secret 掩盖数据丢失。
对于外部连接器,无法解密应把连接标记为不可用并告警;对于实验分组等低风险数据,可以按明确策略重新分配。降级行为必须由具体业务域决定,不能在通用保护层静默吞掉异常。
测试与观测
- 单元测试验证相同 purpose 可解、不同 purpose 不可解;
- 集成测试用真实 Redis 与两个 ServiceProvider 做交叉解密;
- 发布测试覆盖旧版本、新版本和重启;
- 指标记录解密失败次数和用途分类,但不记录密文或 Secret;
- Redis key ring 备份恢复有独立演练证据;
- 运维面能判断当前是否真正使用 Redis repository,而不是默认回退。
当前实现边界
当前注册具备共享 Redis repository 和安全回退,但没有开箱即用的 key-ring 健康探针、显式部署隔离名、KMS wrapping 或强制生产 fail-fast。这些能力属于生产加固项;在它们落地前,多实例验收必须依赖交叉解密和 Redis 运维控制,而不是把注册方法本身视作 GA 证据。
采用结论
单实例开发可接受默认仓库;任何多副本、滚动发布或长期密文场景都必须配置共享仓库并完成交叉解密。监管或高价值 Secret 场景还应把 KMS wrapping 与密钥访问审计列为上线前置条件。