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

JS端ECIES加密后C#端解密报GCM MAC校验失败求助

ECIES加密解密适配问题:JS加密后C#后端解密报“mac check in GCM failed”

尝试用JavaScript实现ECIES加密,传递给C#后端解密时触发“mac check in GCM failed”错误。后端代码由同事提供无法修改,只能调整JS代码适配,以下是问题代码及修正方案:

C#后端解密代码

public static string Decrypt(string privateKey, byte[] cliperText)
{
    
    X9ECParameters ecParams = ECNamedCurveTable.GetByName(_curveName);           
    PemReader pr = new(new StringReader(privateKey));
    AsymmetricCipherKeyPair keyPairServer = (AsymmetricCipherKeyPair) pr.ReadObject();
    ECPrivateKeyParameters serverECPrivateKeyParameters = (ECPrivateKeyParameters) keyPairServer.Private;

    int uncompressedPointKeyLengthBytes = 2 * (serverECPrivateKeyParameters.Parameters.N.BitLength + 8 - 1) / 8;
    byte[] sharedInfo = cliperText[0..uncompressedPointKeyLengthBytes];
    byte[] encryptedDataAndGcmTag = cliperText[uncompressedPointKeyLengthBytes..cliperText.Length];

    int keySizeLengthBytes = (sharedInfo.Length - 1) / 2;
    BigInteger x =  new(1, sharedInfo[1..(keySizeLengthBytes + 1)]);
    BigInteger y = new(1, sharedInfo[(keySizeLengthBytes + 1)..sharedInfo.Length]);

    ECDomainParameters domainParameters = new(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
    ECPublicKeyParameters pubkeyParam = new(ecParams.Curve.CreatePoint(x, y), domainParameters);

    ECDHCBasicAgreement keyAgreement = new();
    keyAgreement.Init(serverECPrivateKeyParameters);
    BigInteger symmetricKey = keyAgreement.CalculateAgreement(pubkeyParam);
    
    var counterData = new byte[] { 0x00, 0x00, 0x00, 0x01 };
    var preHashKey = symmetricKey.ToByteArrayUnsigned().Concat(counterData).Concat(sharedInfo).Select(i => i).ToArray();
    var hashedKey = SHA256.HashData(preHashKey);

    AeadParameters parameters = new(new KeyParameter(hashedKey[0..16]), 128, hashedKey[16..32], null);
    GcmBlockCipher cipher = new(new AesEngine());
    cipher.Init(false, parameters);
    byte[] plainBytes = new byte[cipher.GetOutputSize(encryptedDataAndGcmTag.Length)];

    // 此处触发Mac check错误
    cipher.DoFinal(plainBytes, cipher.ProcessBytes(encryptedDataAndGcmTag, 0, encryptedDataAndGcmTag.Length, plainBytes, 0));

    return Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());
}

string privateKeyServerASN1 = @"
-----BEGIN EC PRIVATE KEY-----
some private keys
-----END EC PRIVATE KEY-----
";

// JS加密生成的数据
var js = "BArRePhFlOuJkzMyydytWrFmlsy8sbAJBU5b/bxNimQ5hkwkz0RjK4dGVrdK9WinpDjjoJ5DnWcP4+zp2RvgSCfPBbSHC4ZaHBXuDKhxVbKc9Fk6Cp6aE/awVLUj4Tjyv/l2R8bY";

string decryptedDataAndroid = ECIES.Decrypt(privateKeyServerASN1, Convert.FromBase64String(js));

原JavaScript加密代码

async function performEncryption() {
  const publicKeyBase64 = "public key";
  
  const plaintextBytes = new TextEncoder().encode("Hello");

  // Convert the base64 public key to ArrayBuffer
  const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), c => c.charCodeAt(0)).buffer;

  // Import the public key
  const importedPublicKey = await crypto.subtle.importKey(
    "spki",
    publicKeyBytes,
    {
      name: "ECDH",
      namedCurve: "P-256",
    },
    true,
    []
  );

  // Generate an ephemeral EC key pair
  const ephemeralEcKeyPair = await crypto.subtle.generateKey(
    {
      name: "ECDH",
      namedCurve: "P-256",
    },
    true,
    ["deriveKey"]
  );

  // Use ECDH to generate a symmetric key
  const sharedSecret = await crypto.subtle.deriveKey(
    {
      name: "ECDH",
      public: importedPublicKey,
    },
    ephemeralEcKeyPair.privateKey,
    {
      name: "AES-GCM",
      length: 256,
    },
    true,
    ["encrypt", "decrypt"]
  );

  // Export the raw public key (x coordinate) as sharedInfo
  const rawPublicKey = await crypto.subtle.exportKey("raw", importedPublicKey);
  const sharedInfo = new Uint8Array(rawPublicKey);

  // Use SHA-256 to hash the shared secret
  const symmetricKey = await crypto.subtle.digest("SHA-256", new Uint8Array(await crypto.subtle.exportKey("raw", sharedSecret)));

  // Use the first 16 bytes as an AES-GCM key
  const aesGcmKey = await crypto.subtle.importKey(
    "raw",
    symmetricKey.slice(0, 16),
    {
      name: "AES-GCM",
      length: 256,
    },
    true,
    ["encrypt", "decrypt"]
  );

  // Use the second 16 bytes as the initialization vector (IV)
  const iv = symmetricKey.slice(16, 32);

  // Use AES/GCM/NoPadding to encrypt the plaintext and generate a 16-byte GCM tag
  const encryptedData = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: iv,
    },
    aesGcmKey,
    plaintextBytes
  );

  // Concatenate sharedInfo, IV, and encrypted data
  const combinedData = concatBuffers(concatBuffers(sharedInfo, iv), encryptedData);

    console.log("combinedData", combinedData)

  // Convert combinedData to base64
  const base64CombinedData = btoa(String.fromCharCode.apply(null, new Uint8Array(combinedData)));

  console.log("Encrypted Data (Base64):", base64CombinedData);
}

// Utility function to concatenate two ArrayBuffers
function concatBuffers(buffer1, buffer2) {
  const result = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
  result.set(new Uint8Array(buffer1), 0);
  result.set(new Uint8Array(buffer2), buffer1.byteLength);
  return result;
}

performEncryption();

问题分析与修正方案

核心问题点

  1. sharedInfo错误:后端从加密数据开头读取的是JS侧生成的临时公钥(非压缩格式),但原JS代码用了服务器公钥作为sharedInfo,完全不符合后端逻辑。
  2. 密钥派生逻辑不匹配:后端的密钥派生流程是:ECDH计算的共享密钥(BigInteger转无符号字节数组)→ 拼接[0x00,0x00,0x00,0x01] → 拼接临时公钥 → SHA256哈希 → 前16字节为AES密钥,后16字节为IV。原JS代码用deriveKey直接生成AES密钥,逻辑完全偏离。
  3. 数据拼接格式错误:后端期望的加密数据结构是[临时公钥(非压缩)][密文+GCM Tag],原JS代码多拼接了IV,导致后端解析的密文和Tag完全错误。

修正后的JavaScript代码

async function performEncryption() {
  const publicKeyBase64 = "public key"; // 替换为实际的服务器公钥(SPKI格式Base64)
  const plaintextBytes = new TextEncoder().encode("Hello");

  // 1. 导入服务器公钥
  const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), c => c.charCodeAt(0)).buffer;
  const importedPublicKey = await crypto.subtle.importKey(
    "spki",
    publicKeyBytes,
    { name: "ECDH", namedCurve: "P-256" },
    true,
    []
  );

  // 2. 生成临时EC密钥对
  const ephemeralEcKeyPair = await crypto.subtle.generateKey(
    { name: "ECDH", namedCurve: "P-256" },
    true,
    ["deriveBits"]
  );

  // 3. 导出临时公钥并转为非压缩格式(后端需要的sharedInfo)
  const rawEphemeralPubKey = await crypto.subtle.exportKey("raw", ephemeralEcKeyPair.publicKey);
  // raw格式是x+y共64字节,非压缩格式需要前缀0x04,所以拼接成65字节
  const sharedInfo = new Uint8Array(65);
  sharedInfo[0] = 0x04;
  sharedInfo.set(new Uint8Array(rawEphemeralPubKey), 1);

  // 4. 执行ECDH计算共享密钥(原始比特位)
  const sharedSecretBits = await crypto.subtle.deriveBits(
    { name: "ECDH", public: importedPublicKey },
    ephemeralEcKeyPair.privateKey,
    256 // P-256的密钥长度是256位
  );

  // 5. 按照后端逻辑派生AES密钥和IV
  const counterData = new Uint8Array([0x00, 0x00, 0x00, 0x01]);
  // 拼接:共享密钥字节 + counterData + sharedInfo
  const preHashData = new Uint8Array(
    sharedSecretBits.byteLength + counterData.byteLength + sharedInfo.byteLength
  );
  preHashData.set(new Uint8Array(sharedSecretBits), 0);
  preHashData.set(counterData, sharedSecretBits.byteLength);
  preHashData.set(sharedInfo, sharedSecretBits.byteLength + counterData.byteLength);

  // SHA256哈希得到32字节数据
  const hashedKey = await crypto.subtle.digest("SHA-256", preHashData);
  const aesKeyBytes = hashedKey.slice(0, 16); // 前16字节为AES密钥
  const ivBytes = hashedKey.slice(16, 32);    // 后16字节为IV

  // 6. 导入AES-GCM密钥
  const aesGcmKey = await crypto.subtle.importKey(
    "raw",
    aesKeyBytes,
    { name: "AES-GCM", length: 128 }, // 这里用128位密钥,因为取的是前16字节
    true,
    ["encrypt"]
  );

  // 7. AES-GCM加密(密文+Tag会自动拼接)
  const encryptedDataWithTag = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv: ivBytes },
    aesGcmKey,
    plaintextBytes
  );

  // 8. 拼接最终数据:sharedInfo(临时公钥非压缩格式) + 密文+Tag
  const combinedData = new Uint8Array(sharedInfo.length + encryptedDataWithTag.byteLength);
  combinedData.set(sharedInfo, 0);
  combinedData.set(new Uint8Array(encryptedDataWithTag), sharedInfo.length);

  // 转Base64输出
  const base64CombinedData = btoa(String.fromCharCode.apply(null, combinedData));
  console.log("Encrypted Data (Base64):", base64CombinedData);
}

performEncryption();

关键修正说明

  • 临时公钥转为非压缩格式(前缀0x04+x坐标+y坐标,共65字节),对应后端sharedInfo的解析逻辑。
  • 严格按照后端的密钥派生流程实现:ECDH共享密钥→拼接固定计数器→拼接临时公钥→SHA256哈希→拆分AES密钥和IV。
  • 最终数据结构严格匹配后端预期:[临时公钥非压缩格式][密文+GCM Tag],不再额外拼接IV。

内容的提问来源于stack exchange,提问作者pakito

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:02:04