You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 10:14:32