Certificate and decrypting the callback message
Update Time:2024.09.18To ensure security, WeChat Pay uses AES-256-GCM encryption for key information in the callback notification and platform certificate download API. This chapter details the format of encrypted messages and how to decrypt them.
1. Format of Encrypted Messages
AES-AES-GCM
is an Authenticated Encryption algorithm of the NIST standard and an encryption mode that can guarantee the confidentiality, integrity and authenticity of data.It is most widely used in TLS.
The encryption key used in the certificate and callback messages is the API V3 key.
We use a separate JSON object to represent encrypted data. For ease of reading, the example is Pretty formatted and comments are added.
1{ 2 "original_type": "transaction", //Object type before encryption 3 "algorithm": "AEAD_AES_256_GCM", // Encryption algorithm 4 // Base64 encoded ciphertext 5 "ciphertext": "...", 6 // Random string initialization vector used for encryption 7 "nonce": "...", 8 // Additional data package (can be left blank) 9 "associated_data": "" 10} 11
|
2. Decryption
Please refer to RFC 5116 for details of the algorithm API.
Most programming languages (newer versions) support AEAD_AES_256_GCM
. Developers can refer to the following examples to learn how to use your programming language for decryption.
JAVA
1package com.wechat.v3; 2 3import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4import com.fasterxml.jackson.annotation.JsonProperty; 5import com.wechat.pay.contrib.apache.httpclient.exception.ParseException; 6import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; 7 8import java.nio.charset.StandardCharsets; 9import java.security.GeneralSecurityException; 10 11 12class DecodeCipher { 13 14 private static final String apiV3Key = ""; 15 16 private void setDecryptData(Notification notification) throws ParseException { 17 18 Notification.Resource resource = notification.getResource(); 19 String getAssociateddData = ""; 20 if (resource.getAssociatedData() != null) { 21 getAssociateddData = resource.getAssociatedData(); 22 } 23 byte[] associatedData = getAssociateddData.getBytes(StandardCharsets.UTF_8); 24 byte[] nonce = resource.getNonce().getBytes(StandardCharsets.UTF_8); 25 String ciphertext = resource.getCiphertext(); 26 AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)); 27 String decryptData; 28 try { 29 decryptData = aesUtil.decryptToString(associatedData, nonce, ciphertext); 30 } catch (GeneralSecurityException e) { 31 throw new ParseException("AES解密失败,resource:" + resource.toString(), e); 32 } 33 notification.setDecryptData(decryptData); 34 } 35 36 37 @JsonIgnoreProperties(ignoreUnknown = true) 38 public class Notification { 39 40 @JsonProperty("id") 41 private String id; 42 @JsonProperty("create_time") 43 private String createTime; 44 @JsonProperty("event_type") 45 private String eventType; 46 @JsonProperty("resource_type") 47 private String resourceType; 48 @JsonProperty("summary") 49 private String summary; 50 @JsonProperty("resource") 51 private Resource resource; 52 private String decryptData; 53 54 @Override 55 public String toString() { 56 return "Notification{" + 57 "id='" + id + '\'' + 58 ", createTime='" + createTime + '\'' + 59 ", eventType='" + eventType + '\'' + 60 ", resourceType='" + resourceType + '\'' + 61 ", decryptData='" + decryptData + '\'' + 62 ", summary='" + summary + '\'' + 63 ", resource=" + resource + 64 '}'; 65 } 66 67 public String getId() { 68 return id; 69 } 70 71 public String getCreateTime() { 72 return createTime; 73 } 74 75 public String getEventType() { 76 return eventType; 77 } 78 79 public String getDecryptData() { 80 return decryptData; 81 } 82 83 public String getSummary() { 84 return summary; 85 } 86 87 public String getResourceType() { 88 return resourceType; 89 } 90 91 public Resource getResource() { 92 return resource; 93 } 94 95 public void setDecryptData(String decryptData) { 96 this.decryptData = decryptData; 97 } 98 99 @JsonIgnoreProperties(ignoreUnknown = true) 100 public class Resource { 101 102 @JsonProperty("algorithm") 103 private String algorithm; 104 @JsonProperty("ciphertext") 105 private String ciphertext; 106 @JsonProperty("associated_data") 107 private String associatedData; 108 @JsonProperty("nonce") 109 private String nonce; 110 @JsonProperty("original_type") 111 private String originalType; 112 113 public String getAlgorithm() { 114 return algorithm; 115 } 116 117 public String getCiphertext() { 118 return ciphertext; 119 } 120 121 public String getAssociatedData() { 122 return associatedData; 123 } 124 125 public String getNonce() { 126 return nonce; 127 } 128 129 public String getOriginalType() { 130 return originalType; 131 } 132 133 @Override 134 public String toString() { 135 return "Resource{" + 136 "algorithm='" + algorithm + '\'' + 137 ", ciphertext='" + ciphertext + '\'' + 138 ", associatedData='" + associatedData + '\'' + 139 ", nonce='" + nonce + '\'' + 140 ", originalType='" + originalType + '\'' + 141 '}'; 142 } 143 } 144 145 } 146}
PHP
1<?php 2require_once('vendor/autoload.php'); 3 4use WeChatPay\Crypto\Rsa; 5use WeChatPay\Crypto\AesGcm; 6use WeChatPay\Formatter; 7 8$inWechatpaySignature = '';// Get this value from the header in response 9$inWechatpayTimestamp = '';// Get this value from the header in response 10$inWechatpaySerial = '';// Get this value from the header in response 11$inWechatpayNonce = '';// Get this value from the header in response 12$inBody = '';// Get this value from the body in response 13$apiv3Key = ''; 14$platformPublicKeyInstance = Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem', Rsa::KEY_TYPE_PUBLIC); 15 16$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp); 17$verifiedStatus = Rsa::verify( 18 Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody), 19 $inWechatpaySignature, 20 $platformPublicKeyInstance 21); 22if ($timeOffsetStatus && $verifiedStatus) { 23 $inBodyArray = (array)json_decode($inBody, true); 24 ['resource' => [ 25 'ciphertext' => $ciphertext, 26 'nonce' => $nonce, 27 'associated_data' => $aad 28 ]] = $inBodyArray; 29 $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad); 30 $inBodyResourceArray = (array)json_decode($inBodyResource, true); 31}
GO
1package wechatpay 2 3import ( 4 "crypto/aes" 5 "crypto/cipher" 6 "encoding/base64" 7) 8 9// DecryptAES256GCM 使用 AEAD_AES_256_GCM 算法进行解密 10// 11// 你可以使用此算法完成微信支付平台证书和回调报文解密,详见: 12// https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/zheng-shu-he-hui-tiao-bao-wen-jie-mi 13func DecryptAES256GCM(aesKey, associatedData, nonce, ciphertext string) (plaintext string, err error) { 14 decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext) 15 if err != nil { 16 return "", err 17 } 18 c, err := aes.NewCipher([]byte(aesKey)) 19 if err != nil { 20 return "", err 21 } 22 gcm, err := cipher.NewGCM(c) 23 if err != nil { 24 return "", err 25 } 26 dataBytes, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData)) 27 if err != nil { 28 return "", err 29 } 30 return string(dataBytes), nil 31}