.NET Framework与JavaScript端到端加密实现问题:客户端私钥解密失败
.NET Framework与JavaScript端到端加密实现问题:客户端私钥解密失败
看起来你在跨端加密的实现上卡壳了,我帮你梳理下代码里的几个关键问题,这些应该就是导致客户端解密失败的核心原因:
1. PBKDF2参数逻辑混乱(最关键)
你在服务器端和客户端对PBKDF2的用法完全不匹配:
- 服务器端把
request.Salt既当密码又转成saltBytes当盐,逻辑自相矛盾; - 客户端把
salt同时作为密钥材料和PBKDF2的盐,和服务器的密钥推导逻辑完全脱节。
更合理的逻辑是:用设备号作为PBKDF2的固定密码,客户端生成的随机salt作为盐,这样两端用相同的密码+盐就能推导出一致的AES密钥。
2. 迭代次数不一致
服务器端用了10000次迭代,客户端却用了100000次,这会直接导致生成的AES密钥完全不同,解密自然失败。
3. 私钥导出格式不兼容
服务器端用privateKey.ExportCspBlob(true)导出的是Windows特定的CSP二进制格式,JavaScript的Web Crypto API无法识别这种格式,应该导出标准的PKCS#8格式私钥,才能实现跨平台兼容。
4. 客户端解密后处理错误
客户端解密后用TextDecoder把二进制私钥转成字符串,会直接破坏二进制数据的完整性,应该直接用Web Crypto API把解密后的二进制数据导入成CryptoKey对象,供后续加密使用。
修改后的代码实现
服务器端(C# .NET Framework)
using System; using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Web.Http; [HttpPost] [Route("api/device/certificate")] public IHttpActionResult GenerateDeviceCertificate([FromBody] CertificateRequestModel request) { try { // 创建带私钥的自签名证书 var subjectName = $"CN={request.DeviceNumber}"; using var rsa = RSA.Create(2048); var certRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 生成自签名证书,有效期1年 var cert = certRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); // 导出跨平台兼容的PKCS#8格式私钥 var privateKeyBytes = rsa.ExportPkcs8PrivateKey(); // 用PBKDF2推导AES密钥:设备号为密码,客户端salt为盐 byte[] saltBytes = Encoding.UTF8.GetBytes(request.Salt); byte[] passwordBytes = Encoding.UTF8.GetBytes(request.DeviceNumber); using var keyDerivation = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 10000, HashAlgorithmName.SHA256); using var aes = Aes.Create(); aes.Key = keyDerivation.GetBytes(32); // 256位AES密钥 aes.GenerateIV(); // 加密私钥 byte[] encryptedPrivateKey; using (var encryptor = aes.CreateEncryptor()) using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(privateKeyBytes, 0, privateKeyBytes.Length); } encryptedPrivateKey = ms.ToArray(); } // 将公钥保存到数据库(用于后续解密客户端payload) var publicKeyBytes = rsa.ExportSubjectPublicKeyInfo(); // 这里添加保存publicKeyBytes到数据库的逻辑,关联request.DeviceNumber return Ok(new { success = true, encryptedPrivateKey = Convert.ToBase64String(encryptedPrivateKey), iv = Convert.ToBase64String(aes.IV) }); } catch (Exception ex) { return Ok(new { success = false, error = ex.Message }); } }
客户端(JavaScript)
let _userDevice; // 你的设备号变量 let _urlBase; // 你的服务器地址变量 let _privateKey; // 存储导入后的CryptoKey对象 async function requestKeyFromServer() { // 生成16字节随机salt并转成十六进制字符串 const saltArray = crypto.getRandomValues(new Uint8Array(16)); const salt = Array.from(saltArray) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); const certificateRequest = { deviceNumber: _userDevice, salt: salt }; try { const response = await fetch(`${_urlBase}api/device/certificate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(certificateRequest) }); const result = await response.json(); if (result.success) { // 解密并导入私钥为CryptoKey对象 _privateKey = await decryptAndImportPrivateKey( result.encryptedPrivateKey, _userDevice, salt, result.iv ); console.log("私钥导入成功"); } else { throw new Error(result.error || 'Failed to generate certificate'); } } catch (error) { console.error('Certificate generation failed:', error); throw error; } } async function decryptAndImportPrivateKey(encryptedPrivateKeyB64, password, salt, ivB64) { // 解码Base64格式的数据 const encryptedData = Uint8Array.from(atob(encryptedPrivateKeyB64), c => c.charCodeAt(0)); const ivBytes = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const saltBytes = new TextEncoder().encode(salt); const passwordBytes = new TextEncoder().encode(password); // 推导AES密钥(和服务器参数完全一致) const keyMaterial = await crypto.subtle.importKey( "raw", passwordBytes, { name: "PBKDF2" }, false, ["deriveKey"] ); const aesKey = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: saltBytes, iterations: 10000, hash: "SHA-256" }, keyMaterial, { name: "AES-CBC", length: 256 }, true, ["decrypt"] ); // 解密PKCS#8格式的私钥 const decryptedPkcs8 = await crypto.subtle.decrypt( { name: "AES-CBC", iv: ivBytes }, aesKey, encryptedData ); // 导入PKCS#8私钥为RSA CryptoKey对象(用于后续加密payload) return await crypto.subtle.importKey( "pkcs8", decryptedPkcs8, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"] ); } // 示例:用私钥加密payload并发送给服务器 async function encryptAndSendPayload(payload) { if (!_privateKey) throw new Error("私钥未初始化"); const payloadBytes = new TextEncoder().encode(JSON.stringify(payload)); const encryptedPayload = await crypto.subtle.encrypt( { name: "RSA-OAEP" }, _privateKey, payloadBytes ); // 将加密后的二进制数据转成Base64格式发送 const encryptedPayloadB64 = btoa(String.fromCharCode(...new Uint8Array(encryptedPayload))); // 发送请求到服务器,服务器用保存的公钥解密 const response = await fetch(`${_urlBase}api/device/processPayload`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceNumber: _userDevice, encryptedPayload: encryptedPayloadB64 }) }); return await response.json(); }
额外说明
- 服务器端解密客户端payload时,需要从数据库取出对应的公钥,用
RSA.ImportSubjectPublicKeyInfo()导入后,再用RSA-OAEP算法解密; - 所有加密算法参数必须严格一致:AES模式(CBC)、密钥长度(256)、PBKDF2哈希算法(SHA256)、迭代次数,任何参数不匹配都会导致解密失败;
- 生产环境建议增加设备号验证机制,防止恶意请求伪造证书。
备注:内容来源于stack exchange,提问作者Wolf
相关产品推荐
相关产品推荐

