C#加密PHI/PII数据,无CryptoJS的JavaScript解密方案咨询
针对你的场景——C#后端加密PHI/PII,前端JS解密,不能用CryptoJS,还要在SSL之上再加一层加密,我整理了几个基于原生库的可行方案,兼顾安全性和实用性:
方案1:AES-GCM混合加密(首推,安全+高性能)
AES-GCM是带认证的对称加密算法,既能保证数据机密性,又能防止篡改,性能也比非对称加密高很多。核心思路是:后端用AES加密数据,同时用RSA加密AES密钥(避免密钥明文传输),前端解密AES密钥后再解密数据。
C#后端加密代码
using System; using System.Security.Cryptography; using System.Text; public class SecureDataEncryption { // 生成256位AES密钥(安全随机) public static byte[] GenerateAesKey() => RandomNumberGenerator.GetBytes(32); // AES-GCM加密数据 public static (string CipherText, string Iv, string AuthTag) EncryptWithAesGcm(string plainText, byte[] aesKey) { using var aesGcm = new AesGcm(aesKey); var plainBytes = Encoding.UTF8.GetBytes(plainText); var iv = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize); // GCM的IV(Nonce) var cipherBytes = new byte[plainBytes.Length]; var authTag = new byte[AesGcm.TagByteSizes.MaxSize]; // 认证标签,用于验证数据完整性 aesGcm.Encrypt(iv, plainBytes, cipherBytes, authTag); return ( Convert.ToBase64String(cipherBytes), Convert.ToBase64String(iv), Convert.ToBase64String(authTag) ); } // 用RSA公钥加密AES密钥(公钥由前端生成并传给后端) public static string EncryptAesKeyWithRsa(byte[] aesKey, string rsaPublicKeyBase64) { var publicKeyBytes = Convert.FromBase64String(rsaPublicKeyBase64); using var rsa = RSA.Create(); rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); // 使用OAEP填充,比PKCS#1更安全 var encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256); return Convert.ToBase64String(encryptedKey); } }
前端JS解密代码(用原生Web Crypto API)
// 前端生成RSA密钥对(只在需要时生成,私钥不导出、不传输) async function generateRsaKeyPair() { return crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, // 私钥不可提取,更安全 ["encrypt", "decrypt"] ); } // 导出公钥给后端 async function exportRsaPublicKey(keyPair) { const publicKeyBytes = await crypto.subtle.exportKey("spki", keyPair.publicKey); return btoa(String.fromCharCode(...new Uint8Array(publicKeyBytes))); } // 解密RSA加密的AES密钥 async function decryptAesKeyWithRsa(encryptedAesKeyBase64, privateKey) { const encryptedKey = Uint8Array.from(atob(encryptedAesKeyBase64), c => c.charCodeAt(0)); return crypto.subtle.decrypt( { name: "RSA-OAEP" }, privateKey, encryptedKey ); } // AES-GCM解密数据 async function decryptAesGcm(cipherTextBase64, ivBase64, authTagBase64, aesKey) { const cipherText = Uint8Array.from(atob(cipherTextBase64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0)); const authTag = Uint8Array.from(atob(authTagBase64), c => c.charCodeAt(0)); const cryptoKey = await crypto.subtle.importKey( "raw", aesKey, { name: "AES-GCM" }, false, ["decrypt"] ); const decryptedBytes = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tag: authTag }, cryptoKey, cipherText ); return new TextDecoder().decode(decryptedBytes); } // 完整解密流程示例 async function decryptPhiData(encryptedData) { // 假设前端已经生成并保存了RSA密钥对,这里从内存中获取 const rsaKeyPair = await generateRsaKeyPair(); // 解密AES密钥 const aesKey = await decryptAesKeyWithRsa(encryptedData.EncryptedAesKey, rsaKeyPair.privateKey); // 解密PHI数据 return decryptAesGcm(encryptedData.CipherText, encryptedData.Iv, encryptedData.AuthTag, aesKey); }
方案2:RSA直接加密(仅适合小体量数据)
如果你的PHI/PII是小字段(比如身份证号、手机号,单条数据小于190字节左右,取决于RSA密钥长度),可以直接用RSA加密。但注意RSA性能低,不适合大体积数据。
C#后端加密(用前端传的公钥)
public static string EncryptSmallDataWithRsa(string plainText, string rsaPublicKeyBase64) { var publicKeyBytes = Convert.FromBase64String(rsaPublicKeyBase64); using var rsa = RSA.Create(); rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); var plainBytes = Encoding.UTF8.GetBytes(plainText); var cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256); return Convert.ToBase64String(cipherBytes); }
前端JS解密
async function decryptSmallRsaData(encryptedDataBase64, privateKey) { const encryptedBytes = Uint8Array.from(atob(encryptedDataBase64), c => c.charCodeAt(0)); const decryptedBytes = await crypto.subtle.decrypt( { name: "RSA-OAEP" }, privateKey, encryptedBytes ); return new TextDecoder().decode(decryptedBytes); }
方案3:PBKDF2密钥派生(基于用户密码的场景)
如果你的系统有用户登录密码,可以用PBKDF2从密码派生相同的密钥,两端用相同的盐、迭代次数和哈希算法。适合需要用户密码才能解密数据的场景。
C#后端加密
public static (string CipherText, string Iv, string AuthTag, string Salt) EncryptWithPassword(string plainText, string password) { var salt = RandomNumberGenerator.GetBytes(16); // 随机盐,每次加密都不同 using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256); var aesKey = pbkdf2.GetBytes(32); var (cipherText, iv, authTag) = EncryptWithAesGcm(plainText, aesKey); return (cipherText, iv, authTag, Convert.ToBase64String(salt)); }
前端JS解密
async function deriveKeyFromPassword(password, saltBase64) { const salt = Uint8Array.from(atob(saltBase64), c => c.charCodeAt(0)); const passwordBytes = new TextEncoder().encode(password); const keyMaterial = await crypto.subtle.importKey( "raw", passwordBytes, { name: "PBKDF2" }, false, ["deriveKey"] ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["decrypt"] ); } async function decryptWithPassword(cipherTextBase64, ivBase64, authTagBase64, saltBase64, password) { const key = await deriveKeyFromPassword(password, saltBase64); const cipherText = Uint8Array.from(atob(cipherTextBase64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0)); const authTag = Uint8Array.from(atob(authTagBase64), c => c.charCodeAt(0)); const decryptedBytes = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tag: authTag }, key, cipherText ); return new TextDecoder().decode(decryptedBytes); }
关键安全注意事项
- 绝对不要硬编码密钥:C#后端用环境变量或安全密钥管理服务存储密钥;前端不要将敏感密钥存在本地存储(用内存暂存即可)。
- 优先用AES-GCM:避免使用CBC模式,因为CBC需要额外的padding,容易引发padding oracle攻击,而GCM自带认证和完整性校验。
- RSA密钥对管理:前端生成的RSA私钥设置为
不可提取(extractable: false),防止被恶意脚本窃取。 - 编码一致性:两端都用UTF-8编码字符串,Base64传输字节数据,避免乱码。
内容的提问来源于stack exchange,提问作者Dean.DePue
相关产品推荐
相关产品推荐

