如何解密回调报文和平台证书
更新时间:2024.11.29为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密,商户收到报文后,要解密出明文,APIv3密钥是解密时使用的对称密钥。本章节详细介绍了加密报文的格式,以及如何进行解密。
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)
文档是否有帮助