如何解密回调报文和平台证书

更新时间:2024.11.29

为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密,商户收到报文后,要解密出明文,APIv3密钥是解密时使用的对称密钥。本章节详细介绍了加密报文的格式,以及如何进行解密。

我们提供了微信支付API v3官方SDK(目前包含JavaPHPGo三种语言版本),使用官方 SDK 调用微信支付接口,无需关心签名生成和验证,接入更方便。

1. 加密报文格式

AES-GCM是一种NIST标准的认证加密算法, 是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。它最广泛的应用是在TLS中。

证书和回调报文使用的加密密钥为APIv3密钥,请参考什么是APIv3密钥?如何获取APIv3密钥

对于加密的数据,我们使用了一个独立的JSON对象来表示。为了方便阅读,示例做了Pretty格式化,并加入了注释。

1{
2  "original_type": "transaction", // 加密前的对象类型
3  "algorithm": "AEAD_AES_256_GCM", // 加密算法
4  // Base64编码后的密文
5  "ciphertext": "...",
6  // 加密使用的随机串初始化向量)
7  "nonce": "...",
8  // 附加数据包(可能为空)
9  "associated_data": ""
10}

注意:

加密的随机串,跟签名时使用的随机串没有任何关系,是不一样的。

2. 解密

算法接口的细节,可以参考rfc5116

大部分编程语言(较新版本)都支持了AEAD_AES_256_GCM。开发者可以参考下列的示例,了解如何使用您的编程语言实现解密。

JAVA:

1import java.io.IOException;
2import java.security.GeneralSecurityException;
3import java.security.InvalidAlgorithmParameterException;
4import java.security.InvalidKeyException;
5import java.security.NoSuchAlgorithmException;
6import java.util.Base64;
7import javax.crypto.Cipher;
8import javax.crypto.NoSuchPaddingException;
9import javax.crypto.spec.GCMParameterSpec;
10import javax.crypto.spec.SecretKeySpec;
11public class AesUtil {
12  static final int KEY_LENGTH_BYTE = 32;
13  static final int TAG_LENGTH_BIT = 128;
14  private final byte[] aesKey;
15  public AesUtil(byte[] key) {
16    if (key.length != KEY_LENGTH_BYTE) {
17      throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
18    }
19      this.aesKey = key;
20  }
21  public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
22  throws GeneralSecurityException, IOException {
23    try {
24      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
25      SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
26      GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
27      cipher.init(Cipher.DECRYPT_MODE, key, spec);
28      cipher.updateAAD(associatedData);
29      return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
30    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
31      throw new IllegalStateException(e);
32    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
33      throw new IllegalArgumentException(e);
34    }
35  }
36}

PHP:

1class AesUtil {
2 /**
3  * AES key
4  *
5  * @var string
6  */
7 private $aesKey;
8 const KEY_LENGTH_BYTE = 32;
9 const AUTH_TAG_LENGTH_BYTE = 16;
10 /**
11  * Constructor
12  */
13 public
14 function __construct($aesKey) {
15  if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
16   throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
17  }
18  $this - > aesKey = $aesKey;
19 }
20 /**
21  * Decrypt AEAD_AES_256_GCM ciphertext
22  *
23  * @param string    $associatedData     AES GCM additional authentication data
24  * @param string    $nonceStr           AES GCM nonce
25  * @param string    $ciphertext         AES GCM cipher text
26  *
27  * @return string|bool      Decrypted string on success or FALSE on failure
28  */
29 public
30 function decryptToString($associatedData, $nonceStr, $ciphertext) {
31  $ciphertext = \base64_decode($ciphertext);
32  if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
33   return false;
34  }
35  // ext-sodium (default installed on >= PHP 7.2)
36  if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
37   return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this - > aesKey);
38  }
39  // ext-libsodium (need install libsodium-php 1.x via pecl)
40  if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
41   return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this - > aesKey);
42  }
43  // openssl (PHP >= 7.1 support AEAD)
44  if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
45   $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
46   $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
47   return \openssl_decrypt($ctext, 'aes-256-gcm', $this - > aesKey, \OPENSSL_RAW_DATA, $nonceStr,
48    $authTag, $associatedData);
49  }
50  throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
51 }
52}

aspnet:

1public class AesGcm
2{
3    private static string ALGORITHM = "AES/GCM/NoPadding";
4    private static int TAG_LENGTH_BIT = 128;
5    private static int NONCE_LENGTH_BYTE = 12;
6    private static string AES_KEY = "yourkeyhere";
7    public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
8    {
9        GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
10        AeadParameters aeadParameters = new AeadParameters(
11            new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)), 
12            128, 
13            Encoding.UTF8.GetBytes(nonce), 
14            Encoding.UTF8.GetBytes(associatedData));
15        gcmBlockCipher.Init(false, aeadParameters);
16        byte[] data = Convert.FromBase64String(ciphertext);
17        byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
18        int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
19        gcmBlockCipher.DoFinal(plaintext, length);
20        return Encoding.UTF8.GetString(plaintext);
21    }
22}

Python:

1from cryptography.hazmat.primitives.ciphers.aead import AESGCM
2import base64
3def decrypt(nonce, ciphertext, associated_data):
4    key = "Your32Apiv3Key"
5    key_bytes = str.encode(key)
6    nonce_bytes = str.encode(nonce)
7    ad_bytes = str.encode(associated_data)
8    data = base64.b64decode(ciphertext)
9    aesgcm = AESGCM(key_bytes)
10    return aesgcm.decrypt(nonce_bytes, data, ad_bytes)

 

 

更多技术问题
技术咨询
反馈
咨询
目录
置顶