密钥严禁硬编码,应使用dotnet user-secrets(开发)或系统凭据管理(生产);必须稳定可复现,推荐rfc2898derivebytes派生或安全存储随机生成密钥;iv需每次随机生成并随密文保存;敏感数据须用span.clear()及时清零内存。

密钥不能硬编码在代码里,哪怕只是测试
硬编码密钥(比如写死在 const string key = "mysecret123...";)等于把保险柜钥匙焊在门把手上。编译后反编译工具几秒就能扒出来,dotPeek、ILSpy 都能直接看到字符串常量。哪怕用 XOR 混淆或 Base64 编码,也只算“防君子不防小人”,毫无安全意义。
实操建议:
- 开发阶段用
dotnet user-secrets存密钥:运行dotnet user-secrets set "Encryption:Key" "base64-encoded-32-byte-key",然后用IConfiguration读取,避免提交到 Git - 生产环境必须走操作系统级凭据管理:Windows 用
Windows DPAPI(ProtectedData.Protect),Linux/macOS 用libsecret或keyringCLI 工具配合进程级权限控制 - 绝对不要用
Environment.GetEnvironmentVariable直接读明文密钥——进程列表里ps aux或任务管理器一眼可见
别用 AES.Create().Key 直接生成密钥
AES.Create() 默认生成随机密钥没错,但问题在于:每次调用都不同。你加密一个文件,下次解密时再调用一次 AES.Create(),拿到的是全新密钥,必然失败。这不是“随机性好”,是“不可重现”。
实操建议:
- 密钥必须稳定可复现:用
Rfc2898DeriveBytes从口令派生(适合用户输入密码的场景),或用RandomNumberGenerator一次性生成并安全存储(适合服务端自动加解密) - 如果走派生路线,务必固定
iterations(至少 100_000)、用唯一盐值(如文件路径哈希),且盐值需和密文一起保存(不保密,但不可省略) - 密钥长度严格匹配算法要求:AES-256 要 32 字节,
Convert.FromBase64String后检查.Length == 32,否则CryptographicException报错时很难定位
IV 必须随密文一起保存,且不能复用
IV(初始化向量)不是密钥,但它的作用是让相同明文加密出不同密文。复用 IV + 相同密钥 = 破坏语义安全性,攻击者能通过密文比对推测内容模式。而如果 IV 不随密文保存,解密时就无法还原原始数据流。
实操建议:
- 每次加密前用
RandomNumberGenerator.Fill(iv)生成新 IV,然后把 IV(通常 16 字节)拼在密文前面,解密时先切出来 - 不要用时间戳、计数器或文件名哈希当 IV——这些可预测,等同于没用
- 注意 .NET 的
AesGcm等 AEAD 模式会额外需要Nonce,它和 IV 逻辑类似但长度/用法不同,别混用;老项目若还在用RijndaelManaged,赶紧换成Aes类型
密钥泄露风险最高点:内存未及时清零
.NET 的 string 和普通 byte[] 是托管对象,GC 不保证立即回收,密钥可能在内存中残留数秒甚至更久。调试器、内存转储(procdump)、甚至某些云平台的快照功能都可能捕获到。
实操建议:
- 敏感数据一律用
Span<byte></byte>或Memory<byte></byte>操作,加密完成后立刻调用Span<byte>.Clear()</byte> - 密钥变量声明为
ReadOnlySpan<byte></byte>,避免意外复制;若必须用数组,加密结束后手动循环赋零,并调用Array.Clear() - 避免在日志、异常消息、WPF 绑定源中暴露密钥相关变量名(比如
log.LogInformation($"Key length: {key.Length}")—— 这类日志上线前必须删掉)
真正麻烦的从来不是“怎么加密”,而是“密钥在哪生成、谁有权读、存多久、用完怎么销毁”。尤其当服务要长期运行、支持热更新或跨进程通信时,密钥生命周期管理比算法选择重要得多。









