做微信商圈, 支付即积分,当用户使用微信支付后,微信会把支付信息回调到业务系统,业务系统按照预订的规则对会员进行积分,但就是一个微信商圈回调信息, 进行解密却报错Tag mismatch. 解密微信报错Tag mismath的报错信息如下.

Connected to the target VM, address: '127.0.0.1:50803', transport: 'socket'
Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch!
	at com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:620)
	at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116)
	at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
	at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
	at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
	at javax.crypto.Cipher.doFinal(Cipher.java:2168)
	at com.example.demo.index.AesUtil.decryptToString(AesUtil.java:38)
	at com.example.demo.index.TestWechatHuangting.main(TestWechatHuangting.java:49)
Disconnected from the target VM, address: '127.0.0.1:50803', transport: 'socket'

翻遍全网, 给出了让人以为一定可行的几个方法. 首先是微信官方给的解决方法

官方:

官方给大家发了文档

参考这个排查文档,您这边仔细检查下:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_2.shtml

官方的声音简直是掷地有声:

 

使用Java解密时,抛出异常AEADBadTagException: Tag mismatch!

加密使用的AES-GCM包含了Galois Message Authentication Code (GMAC)的消息认证机制。解密时会对数据的完整性进行校验。出现tag mismatch异常,表示解密时的消息认证失败。通常有三种可能:

  • 使用了错误的API v3密钥,如使用了其他商户号的密钥,或者使用了APIv2的APIKey。
  • 密文不正确。请检查提交解密的密文和收到的密文。注意报文中的密文经过了Base64编码。
  • 解密时接口遗漏传入附加数据(associated_data)

所以, 我们按图索骥, 三种可能一一排查. 果然......., 都被我们排查掉了.

上代码:

   public static void main(String[] args) throws GeneralSecurityException, IOException {

        /*
        *
        *  这一段是刚才回调回来的
        *
        *   { "summary" : "支付成功",
"create_time" : "2022-03-23T16:16:50+08:00",
"resource" :{ "ciphertext" : "/w/WqKvC7S1+OLqAIWld/IGqFkHnT5gTZjW3+6BfArL6k47DYB+As/ObssxdT3qI16KX4JfXx7O35l4BId9JryoUyD1B+cLVJkiT/Rg5Dsk0P1IVzAX1hlx/sIM+B1HWx3xj4U8hNhHd8Ckitv+Hes7CNw2ysA/QJ0bLgG3d1DTwV1THpuVB5x/lPHdeAi/CAs0WHoyqaXOEVaD+4G3/87T22wUz9BEQbi5+XNd8idOGADyDzRU1iKChMTPanSvfBiDFyXNZjhsHmmdmgfpIDE4jTn6cO3ZbrW1dGkUmBbGjucJbIBPXCCLdDmhgJ4eZwg0h5W93vvcdjJnakx872HlkKNhRdOnLSfkSgbHOf55woqZyB9cnT0s1P0bemCknyDDT",
"nonce" : "eciTiCgUlhiP",
"associated_data" : "mall_transaction",
"original_type" : "mall_transaction",
"algorithm" : "AEAD_AES_256_GCM" },
"resource_type" : "encrypt-resource",
"event_type" : "MALL_TRANSACTION.SUCCESS",
"id" : "2507da91-0b81-551a-912c-e2dcf8bf8ba1"}
*
        *
        *
        *
        * */

        //某商场广场

        //密钥
        String key = "api3key密钥";

        AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
        String associatedData = "mall_transaction";

        String nonce = "eciTiCgUlhiP";

        String ciphertext = "/w/WqKvC7S1+OLqAIWld/IGqFkHnT5gTZjW3+6BfArL6k47DYB+As/ObssxdT3qI16KX4JfXx7O35l4BId9JryoUyD1B+cLVJkiT/Rg5Dsk0P1IVzAX1hlx/sIM+B1HWx3xj4U8hNhHd8Ckitv+Hes7CNw2ysA/QJ0bLgG3d1DTwV1THpuVB5x/lPHdeAi/CAs0WHoyqaXOEVaD+4G3/87T22wUz9BEQbi5+XNd8idOGADyDzRU1iKChMTPanSvfBiDFyXNZjhsHmmdmgfpIDE4jTn6cO3ZbrW1dGkUmBbGjucJbIBPXCCLdDmhgJ4eZwg0h5W93vvcdjJnakx872HlkKNhRdOnLSfkSgbHOf55woqZyB9cnT0s1P0bemCknyDDT";

        String salesjson = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);

        System.out.println(salesjson);
    }

就是这个feel, 我们的代码一点问题都没有, 上面只是提取出来的demo, aesUtil, 就是用的官方给的,我的怀疑精神顿时起来了, 难道我遇到的难得一见面的官方出错?

package com.example.demo.index;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class AesUtil {

    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
            throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

最后检查, AESKEY没错, 字符集也没错, 数据也没少,究竟是什么原因?

我把这个demo, 用另一个商场的生产环境的回调报文,以及那个商场的aeskey进行解密, 结果,一点问题都没有,用户的支付信息一览无余.

{"mchid":"157保密9751","merchant_name":"保密 MALL","shop_name":"保密扒","shop_number":"11保密N2","openid":"oLb更要保密xv3bWQ","appid":"wx1d保密b","time_end":"2022-03-23T15:12:05+08:00","amount":3500,"transaction_id":"420000保密4356"}
Disconnected from the target VM, address: '127.0.0.1:52254', transport: 'socket'

所以, 最后, 我们把锅扔给了腾讯的微信技术小兄弟.但是并没有得到想要的回复.直到我们的队友喊出来那一句"我们着急上线,就卡在这个问题上了". 终于有了回响. 

原来, 这个商场的微信商圈, 之前是启用的服务商模式, AesKey的密钥, 是在原服务商那边配置的. 所以, 再怎么修改自己小程序的密钥, 也不能解密, 因为, 加密的时候, 使用的是原服务商的密钥. 那个服务商是广州的某家公司.

最后终于真相大白,还是在密钥问题上.微信商圈回调解密报错Tag mismath的这个问题得意解决.