Skip to content

Commit 9a96c73

Browse files
authored
feat: 敏感信息加解密支持 (#15)
1. RsaCryptoUtil.rsaEncryptOAEP()/rsaDecryptOAEP() 2. Verifier提供接口getValidCertificate()方法
1 parent 8fe8e1b commit 9a96c73

File tree

6 files changed

+251
-34
lines changed

6 files changed

+251
-34
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,41 @@ HttpClient httpClient = builder.build();
119119
// 后面跟使用Apache HttpClient一样
120120
HttpResponse response = httpClient.execute(...);
121121
```
122-
123122
### 风险
124123

125124
因为不需要传入微信支付平台证书,AutoUpdateCertificatesVerifier 在首次更新证书时**不会验签**,也就无法确认应答身份,可能导致下载错误的证书。
126125

127126
但下载时会通过 **HTTPS****AES 对称加密**来保证证书安全,所以可以认为,在使用官方 JDK、且 APIv3 密钥不泄露的情况下,AutoUpdateCertificatesVerifier 是**安全**的。
128127

128+
## 敏感信息加解密
129+
130+
### 加密
131+
132+
使用` RsaCryptoUtil.encryptOAEP(String, X509Certificate)`进行公钥加密。示例代码如下。
133+
134+
```java
135+
// 建议从Verifier中获得微信支付平台证书,或使用预先下载到本地的平台证书文件中
136+
X509Certificate wechatpayCertificate = verifier.getValidCertificate();
137+
try {
138+
String ciphertext = RsaCryptoUtil.encryptOAEP(text, wechatpayCertificate);
139+
} catch (IllegalBlockSizeException e) {
140+
e.printStackTrace();
141+
}
142+
```
143+
144+
### 解密
145+
146+
使用`RsaCryptoUtil.decryptOAEP(String ciphertext, PrivateKey privateKey)`进行私钥解密。示例代码如下。
147+
148+
```java
149+
// 使用商户私钥解密
150+
try {
151+
String ciphertext = RsaCryptoUtil.decryptOAEP(text, merchantPrivateKey);
152+
} catch (BadPaddingException e) {
153+
e.printStackTrace();
154+
}
155+
```
156+
129157
## 常见问题
130158

131159
### 如何下载平台证书?

src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/AutoUpdateCertificatesVerifier.java

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.wechat.pay.contrib.apache.httpclient.Credentials;
66
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
77
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
8-
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
98
import java.io.ByteArrayInputStream;
109
import java.io.IOException;
1110
import java.security.GeneralSecurityException;
@@ -49,26 +48,12 @@ public class AutoUpdateCertificatesVerifier implements Verifier {
4948

5049
private ReentrantLock lock = new ReentrantLock();
5150

52-
//时间间隔枚举,支持一小时、六小时以及十二小时
53-
public enum TimeInterval {
54-
OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);
55-
56-
private int minutes;
57-
58-
TimeInterval(int minutes) {
59-
this.minutes = minutes;
60-
}
61-
62-
public int getMinutes() {
63-
return minutes;
64-
}
65-
}
66-
6751
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
6852
this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes());
6953
}
7054

71-
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, int minutesInterval) {
55+
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key,
56+
int minutesInterval) {
7257
this.credentials = credentials;
7358
this.apiV3Key = apiV3Key;
7459
this.minutesInterval = minutesInterval;
@@ -81,9 +66,15 @@ public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key,
8166
}
8267
}
8368

69+
@Override
70+
public X509Certificate getValidCertificate() {
71+
return verifier.getValidCertificate();
72+
}
73+
8474
@Override
8575
public boolean verify(String serialNumber, byte[] message, String signature) {
86-
if (instant == null || Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) {
76+
if (instant == null
77+
|| Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) {
8778
if (lock.tryLock()) {
8879
try {
8980
autoUpdateCert();
@@ -105,25 +96,32 @@ private void autoUpdateCert() throws IOException, GeneralSecurityException {
10596
.withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
10697
.build();
10798

108-
HttpGet httpGet = new HttpGet(CertDownloadPath);
109-
httpGet.addHeader("Accept", "application/json");
110-
111-
CloseableHttpResponse response = httpClient.execute(httpGet);
112-
int statusCode = response.getStatusLine().getStatusCode();
113-
String body = EntityUtils.toString(response.getEntity());
114-
if (statusCode == 200) {
115-
List<X509Certificate> newCertList = deserializeToCerts(apiV3Key, body);
116-
if (newCertList.isEmpty()) {
117-
log.warn("Cert list is empty");
118-
return;
99+
try {
100+
HttpGet httpGet = new HttpGet(CertDownloadPath);
101+
httpGet.addHeader("Accept", "application/json");
102+
103+
CloseableHttpResponse response = httpClient.execute(httpGet);
104+
try {
105+
int statusCode = response.getStatusLine().getStatusCode();
106+
String body = EntityUtils.toString(response.getEntity());
107+
if (statusCode == 200) {
108+
List<X509Certificate> newCertList = deserializeToCerts(apiV3Key, body);
109+
if (newCertList.isEmpty()) {
110+
log.warn("Cert list is empty");
111+
return;
112+
}
113+
this.verifier = new CertificatesVerifier(newCertList);
114+
} else {
115+
log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
116+
}
117+
} finally {
118+
response.close();
119119
}
120-
this.verifier = new CertificatesVerifier(newCertList);
121-
} else {
122-
log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
120+
} finally {
121+
httpClient.close();
123122
}
124123
}
125124

126-
127125
/**
128126
* 反序列化证书并解密
129127
*/
@@ -158,4 +156,20 @@ private List<X509Certificate> deserializeToCerts(byte[] apiV3Key, String body)
158156
}
159157
return newCertList;
160158
}
159+
160+
161+
//时间间隔枚举,支持一小时、六小时以及十二小时
162+
public enum TimeInterval {
163+
OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);
164+
165+
private int minutes;
166+
167+
TimeInterval(int minutes) {
168+
this.minutes = minutes;
169+
}
170+
171+
public int getMinutes() {
172+
return minutes;
173+
}
174+
}
161175
}

src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/CertificatesVerifier.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
import java.security.NoSuchAlgorithmException;
66
import java.security.Signature;
77
import java.security.SignatureException;
8+
import java.security.cert.CertificateExpiredException;
9+
import java.security.cert.CertificateNotYetValidException;
810
import java.security.cert.X509Certificate;
911
import java.util.Base64;
1012
import java.util.HashMap;
1113
import java.util.List;
14+
import java.util.NoSuchElementException;
1215

1316
public class CertificatesVerifier implements Verifier {
17+
1418
private final HashMap<BigInteger, X509Certificate> certificates = new HashMap<>();
1519

1620
public CertificatesVerifier(List<X509Certificate> list) {
@@ -40,4 +44,19 @@ public boolean verify(String serialNumber, byte[] message, String signature) {
4044
BigInteger val = new BigInteger(serialNumber, 16);
4145
return certificates.containsKey(val) && verify(certificates.get(val), message, signature);
4246
}
47+
48+
@Override
49+
public X509Certificate getValidCertificate() {
50+
for (X509Certificate x509Cert : certificates.values()) {
51+
try {
52+
x509Cert.checkValidity();
53+
54+
return x509Cert;
55+
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
56+
continue;
57+
}
58+
}
59+
60+
throw new NoSuchElementException("没有有效的微信支付平台证书");
61+
}
4362
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.wechat.pay.contrib.apache.httpclient.auth;
22

3+
import java.security.cert.X509Certificate;
4+
35
public interface Verifier {
6+
47
boolean verify(String serialNumber, byte[] message, String signature);
8+
9+
X509Certificate getValidCertificate();
510
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.wechat.pay.contrib.apache.httpclient.util;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.security.InvalidKeyException;
5+
import java.security.NoSuchAlgorithmException;
6+
import java.security.PrivateKey;
7+
import java.security.cert.X509Certificate;
8+
import java.util.Base64;
9+
import javax.crypto.BadPaddingException;
10+
import javax.crypto.Cipher;
11+
import javax.crypto.IllegalBlockSizeException;
12+
import javax.crypto.NoSuchPaddingException;
13+
14+
public class RsaCryptoUtil {
15+
16+
public static String encryptOAEP(String message, X509Certificate certificate)
17+
throws IllegalBlockSizeException {
18+
try {
19+
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
20+
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
21+
22+
byte[] data = message.getBytes(StandardCharsets.UTF_8);
23+
byte[] ciphertext = cipher.doFinal(data);
24+
return Base64.getEncoder().encodeToString(ciphertext);
25+
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
26+
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
27+
} catch (InvalidKeyException e) {
28+
throw new IllegalArgumentException("无效的证书", e);
29+
} catch (IllegalBlockSizeException | BadPaddingException e) {
30+
throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
31+
}
32+
}
33+
34+
public static String decryptOAEP(String ciphertext, PrivateKey privateKey)
35+
throws BadPaddingException {
36+
try {
37+
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
38+
cipher.init(Cipher.DECRYPT_MODE, privateKey);
39+
40+
byte[] data = Base64.getDecoder().decode(ciphertext);
41+
return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
42+
} catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
43+
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
44+
} catch (InvalidKeyException e) {
45+
throw new IllegalArgumentException("无效的私钥", e);
46+
} catch (BadPaddingException | IllegalBlockSizeException e) {
47+
throw new BadPaddingException("解密失败");
48+
}
49+
}
50+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.wechat.pay.contrib.apache.httpclient;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
6+
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
7+
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
8+
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
9+
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
10+
import com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil;
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.security.PrivateKey;
14+
import org.apache.http.HttpEntity;
15+
import org.apache.http.client.methods.CloseableHttpResponse;
16+
import org.apache.http.client.methods.HttpPost;
17+
import org.apache.http.entity.ContentType;
18+
import org.apache.http.entity.StringEntity;
19+
import org.apache.http.impl.client.CloseableHttpClient;
20+
import org.apache.http.util.EntityUtils;
21+
import org.junit.After;
22+
import org.junit.Before;
23+
import org.junit.Test;
24+
25+
public class RsaCryptoTest {
26+
27+
private static String mchId = ""; // 商户号
28+
private static String mchSerialNo = ""; // 商户证书序列号
29+
private static String apiV3Key = ""; // api密钥
30+
// 你的商户私钥
31+
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n"
32+
+ "-----END PRIVATE KEY-----\n";
33+
34+
private CloseableHttpClient httpClient;
35+
private AutoUpdateCertificatesVerifier verifier;
36+
37+
@Before
38+
public void setup() throws IOException {
39+
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
40+
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
41+
42+
//使用自动更新的签名验证器,不需要传入证书
43+
verifier = new AutoUpdateCertificatesVerifier(
44+
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
45+
apiV3Key.getBytes("utf-8"));
46+
47+
httpClient = WechatPayHttpClientBuilder.create()
48+
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
49+
.withValidator(new WechatPay2Validator(verifier))
50+
.build();
51+
}
52+
53+
@After
54+
public void after() throws IOException {
55+
httpClient.close();
56+
}
57+
58+
@Test
59+
public void encryptTest() throws Exception {
60+
String text = "helloworld";
61+
String ciphertext = RsaCryptoUtil.encryptOAEP(text, verifier.getValidCertificate());
62+
63+
System.out.println("ciphertext: " + ciphertext);
64+
}
65+
66+
@Test
67+
public void postEncryptDataTest() throws Exception {
68+
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/smartguide/guides");
69+
70+
String text = "helloworld";
71+
String ciphertext = RsaCryptoUtil.encryptOAEP(text, verifier.getValidCertificate());
72+
73+
String data = "{\n"
74+
+ " \"store_id\" : 1234,\n"
75+
+ " \"corpid\" : \"1234567890\",\n"
76+
+ " \"name\" : \"" + ciphertext + "\",\n"
77+
+ " \"mobile\" : \"" + ciphertext + "\",\n"
78+
+ " \"qr_code\" : \"https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx\",\n"
79+
+ " \"sub_mchid\" : \"1234567890\",\n"
80+
+ " \"avatar\" : \"logo\",\n"
81+
+ " \"userid\" : \"robert\"\n"
82+
+ "}";
83+
StringEntity reqEntity = new StringEntity(
84+
data, ContentType.create("application/json", "utf-8"));
85+
httpPost.setEntity(reqEntity);
86+
httpPost.addHeader("Accept", "application/json");
87+
httpPost.addHeader("Wechatpay-Serial", "5157F09EFDC096DE15EBE81A47057A7232F1B8E1");
88+
89+
CloseableHttpResponse response = httpClient.execute(httpPost);
90+
assertTrue(response.getStatusLine().getStatusCode() != 401);
91+
assertTrue(response.getStatusLine().getStatusCode() != 400);
92+
try {
93+
HttpEntity entity2 = response.getEntity();
94+
// do something useful with the response body
95+
// and ensure it is fully consumed
96+
EntityUtils.consume(entity2);
97+
} finally {
98+
response.close();
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)