必须在iformfile流读取完成后、写入s3前加密,使用aesgcm或aescryptoserviceprovider,密钥和nonce须安全存储于配置或key vault,s3 contentlength需设为加密后真实长度。

ASP.NET Core 中上传文件后立即加密再传 S3
直接在内存中加密,避免明文临时落盘。关键不是“能不能加”,而是“在哪一环加”——必须在 IFormFile 流读取完成后、写入 S3 前完成加密,否则等于没加。
- 用
AesGcm(.NET 5+)或AesCryptoServiceProvider(兼容旧版),别用已弃用的RijndaelManaged - 密钥和 nonce 必须随文件一起安全传递(比如存进数据库元数据),但 绝不能硬编码在代码里,推荐从
IConfiguration读取或使用 Azure Key Vault - S3 SDK 的
PutObjectRequest接收的是Stream,所以把加密后的MemoryStream直接传进去即可,无需保存本地文件 - 注意:加密流长度 ≠ 原始流长度(GCM 会加认证标签),S3 的
ContentLength必须设为加密后的真实字节数,否则上传失败或校验出错
Azure Blob Storage 上传前启用客户端加密(非服务端加密)
Azure SDK 支持客户端加密,但默认不开启;它和 S3 的“服务端加密(SSE)”不是一回事——这里说的是你控制密钥、在上传前就加密,Blob 存的永远是密文。
- 必须用
Azure.Storage.Blobs.Specialized.EncryptedBlobClient,普通BlobClient不行 - 密钥需封装为
KeyEncryptionKey实现类(如LocalKeyEncryptionKey),且KeyWrapAlgorithm推荐"A256KW" - 加密只对块 Blob 有效,Page Blob 和 Append Blob 不支持客户端加密
- 上传时若用
UploadAsync(Stream, ...),传入的必须是原始明文流;SDK 内部自动加密,不需要你手动调Encrypt() - 解密时也必须用同一个
EncryptedBlobClient,否则抛InvalidCiphertextException
常见错误:以为启用了 SSE 就等于文件被加密了
SSE(Server-Side Encryption)只是云厂商帮你用他们的密钥加密存储磁盘,你上传的仍是明文——中间链路、日志、代理缓存、甚至开发环境调试输出都可能暴露内容。
- AWS S3 的
ServerSideEncryption = ServerSideEncryption.Aes256或KmsKeyId配置,只影响落盘加密,不影响传输过程 - Azure 的
SetHttpHeaders里设ServerSideEncryption = "AES256"同理,它和客户端加密完全正交 - 如果你的合规要求是“数据离开应用进程即加密”,那 SSE 不满足;必须走客户端加密路径
- 混合使用时注意:先客户端加密 → 再开启 SSE,这样是双重防护;反过来(先 SSE 再客户端加密)无意义且浪费 CPU
密钥轮换与元数据设计的实际约束
加密不是加一次就完事。密钥过期、泄露、合规审计都会触发轮换,而轮换的前提是——你能准确知道每个 blob/object 是用哪一把密钥、哪个算法、哪个 nonce 加的。
- 必须在数据库或元数据中持久化记录:
encryption_key_id、encryption_algorithm、iv(或nonce)、tag_length(GCM 场景) - 不要把 nonce 拼进文件名或路径——Blob/S3 路径长度有限,且可能被日志采集;建议存进
Metadata字典(S3)或BlobHttpHeaders.Metadata(Azure) - .NET 的
AesGcm生成的 nonce 是 12 字节,必须原样保存,不能 base64 后截断或转大小写 - 轮换新密钥后,老文件解密仍要可用,所以解密逻辑得支持多密钥路由,不能写死一个
KeyVaultClient.GetSecret("current-key")
最麻烦的从来不是第一次加密,而是三年后审计时发现某批文件的 nonce 存成了字符串而非字节数组,或者密钥 ID 字段超长被 MySQL 自动截断了。











