证书和回调报文解密

更新时间:2024.10.30

为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。


1. 加密报文格式

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

证书和回调报文使用的加密密钥为APIv3密钥。

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

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

注意

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

2. 解密

算法接口的细节,可以参考RFC 5116。

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

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}

 

About  WeChat  Pay

Powered By Tencent & Tenpay Copyright©

2005-2025 Tenpay All Rights Reserved.

Contact Us
Wechat Pay Global

WeChat Pay Global

Contact Us

Customer Service Tel

+86 571 95017

9:00-18:00 Monday-Friday GMT+8

Business Development

wxpayglobal@tencent.com

Developer Support

wepayTS@tencent.com

Wechat Pay Global

About Tenpay
Powered By Tencent & Tenpay Copyright© 2005-2025 Tenpay All Rights Reserved.