wechatpay-apiv3 / wechatpay-java

微信支付 APIv3 的官方 Java Library
Apache License 2.0
870 stars 195 forks source link

使用sdk下载的平台证书加密敏感信息后接口报错 平台私钥解密失败 #234

Closed LHX980818 closed 9 months ago

LHX980818 commented 9 months ago

错误描述

首先,sdk提供了下载平台证书的服务,却没有返回证书的序列号,加密敏感信息时需要用到证书序列号。没办法只能手工调用获取平台证书的接口,对返回信息进行解密后拿到平台证书公钥对敏感信息解密并在请求头中设置了对应的证书序列号。提交接口时返回 平台私钥解密失败。

重现bug的步骤

1.手动调用获取平台证书接口获取平台证书 2.获取到平台证书序列号 3.将响应密文解密获取到平台证书 4.使用平台证书公钥对敏感信息(银行卡号)加密并拼接到请求地址(获取对私银行卡号开户银行API) 5.在请求头中附加加密使用的平台证书序列号 6.调用接口 (获取对私银行卡号开户银行API) 7.接口返回 平台私钥解密失败

预期行为

预期微信后台对加密后的数据正常解密并返回相应的开户行

导致错误的代码片段

// 获取平台证书序列号及证书
HttpClient httpClient = new DefaultHttpClientBuilder().config(entCertificateConfig).build();
        String url = "https://api.mch.weixin.qq.com/v3/certificates";
        com.wechat.pay.java.core.http.HttpHeaders headers = new com.wechat.pay.java.core.http.HttpHeaders();
        headers.addHeader(Constant.ACCEPT, " */*");
        headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue());
        HttpRequest httpRequest = new HttpRequest.Builder()
                .headers(headers)
                .httpMethod(HttpMethod.GET)
                .url(url)
                .build();
        HttpResponse<DownloadCertificateResponse> httpResponse =
                httpClient.execute(httpRequest, DownloadCertificateResponse.class);

        Data data = httpResponse.getServiceResponse().getData().get(0);
        wxPlatCertificateSer = data.getSerialNo();

        EncryptCertificate encryptCertificate = data.getEncryptCertificate();
        AeadCipher aeadCipher = new AeadAesCipher(entWxPayConfig.getApiV3Key().getBytes());

        String decryptCertificate = aeadCipher.decrypt(
                encryptCertificate.getAssociatedData().getBytes(StandardCharsets.UTF_8),
                encryptCertificate.getNonce().getBytes(StandardCharsets.UTF_8),
                Base64.getDecoder().decode(encryptCertificate.getCiphertext()));

        wxPlatCertificate = PemUtil.loadX509FromString(decryptCertificate);

// 加密敏感数据
public static String rsaEncryptOAEP(String message, X509Certificate certificate)
            throws IllegalBlockSizeException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
            byte[] data = message.getBytes(StandardCharsets.UTF_8);
            byte[] cipherdata = cipher.doFinal(data);
            return Base64.getEncoder().encodeToString(cipherdata);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
        } catch (InvalidKeyException e) {
            throw new IllegalArgumentException("无效的证书", e);
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
        }
    }

操作系统

windows10

Java 版本

java 8

wechatpay-java 版本

0.2.7

其他信息

No response

xy-peng commented 9 months ago

可以从 Config.createEncryptor() 获得一个加密器,加密器也有方法获得证书序列号。

目前是在常见问题中有提到,我提前到加解密的章节中补充说明下。

LHX980818 commented 9 months ago

可以从 Config.createEncryptor() 获得一个加密器,加密器也有方法获得证书序列号。

目前是在常见问题中有提到,我提前到加解密的章节中补充说明下。

@xy-peng 我换成了你说的方法,接口仍然返回 平台私钥解密失败。 问题的根源是微信端无法解密我加密的数据

public DepositBankResponse searchBanksByAccount(String accountNumber) {
    HttpClient httpClient = new DefaultHttpClientBuilder().config(entCertificateConfig).build();

    PrivacyEncryptor encryptor = entCertificateConfig.createEncryptor();
    String encryptNumber = encryptor.encrypt(accountNumber);
    String url = "https://api.mch.weixin.qq.com/v3/capital/capitallhh/banks/search-banks-by-bank-account?account_number=" + encryptNumber;

    com.wechat.pay.java.core.http.HttpHeaders headers = new com.wechat.pay.java.core.http.HttpHeaders();
    headers.addHeader(Constant.ACCEPT, com.wechat.pay.java.core.http.MediaType.APPLICATION_JSON.getValue());
    headers.addHeader("Wechatpay-Serial", encryptor.getWechatpaySerial());

    HttpRequest httpRequest = new HttpRequest.Builder()
            .headers(headers)
            .httpMethod(HttpMethod.GET)
            .url(url)
            .build();
    HttpResponse<DepositBankResponse> response;

    try {
        response = httpClient.execute(httpRequest, DepositBankResponse.class);
    } catch (com.wechat.pay.java.core.exception.ServiceException e) {
        LOGGER.error("根据银行账户查询开户行失败:" + e.getResponseBody());
        throw new ServiceException(e.getErrorMessage());
    }

    return response.getServiceResponse();
}
xy-peng commented 9 months ago

麻烦提供下商户号或者应答的 request-id ?

LHX980818 commented 9 months ago

麻烦提供下商户号或者应答的 request-id ?

@xy-peng 1652310197 Request-ID -> 08A29798A90610BF0618A6E1EE5C20D81B289FFD01-269546542

xy-peng commented 9 months ago
PrivacyEncryptor encryptor = entCertificateConfig.createEncryptor();
    String encryptNumber = encryptor.encrypt(accountNumber);
    String url = "https://api.mch.weixin.qq.com/v3/capital/capitallhh/banks/search-banks-by-bank-account?account_number=" + encryptNumber;

你试试对 encryptNumber 做一次 urlencode,base64编码不是 url safe 的

LHX980818 commented 9 months ago
PrivacyEncryptor encryptor = entCertificateConfig.createEncryptor();
    String encryptNumber = encryptor.encrypt(accountNumber);
    String url = "https://api.mch.weixin.qq.com/v3/capital/capitallhh/banks/search-banks-by-bank-account?account_number=" + encryptNumber;

你试试对 encryptNumber 做一次 urlencode,base64编码不是 url safe 的

可以了,感谢。但是在官方文档中的请求示例并没有做urlencode,也没有进行说明,容易造成误导,希望可以改进。

image

xy-peng commented 9 months ago

文档的问题已经交由负责相关 API 的同事了