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();
问题分析与修正方案
核心问题点
- sharedInfo错误:后端从加密数据开头读取的是JS侧生成的临时公钥(非压缩格式),但原JS代码用了服务器公钥作为sharedInfo,完全不符合后端逻辑。
- 密钥派生逻辑不匹配:后端的密钥派生流程是:ECDH计算的共享密钥(BigInteger转无符号字节数组)→ 拼接
[0x00,0x00,0x00,0x01]→ 拼接临时公钥 → SHA256哈希 → 前16字节为AES密钥,后16字节为IV。原JS代码用deriveKey直接生成AES密钥,逻辑完全偏离。 - 数据拼接格式错误:后端期望的加密数据结构是
[临时公钥(非压缩)][密文+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
相关产品推荐
相关产品推荐

