From 9d5bb1aadd6c146179880d6a3a8d522a4d88a9af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:12:47 +0000 Subject: [PATCH 1/3] Initial plan From 96eaeb2caace4f3cfaa4ca26b876a66140aac94e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:27:04 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=8A=A0=E5=AF=86=E7=BD=91=E7=BB=9C=E9=80=9A=E9=81=93?= =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E5=A4=8D=20HMAC=20=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E3=80=81=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20AES=20=E5=B7=A5=E5=85=B7=E6=96=B9=E6=B3=95=E5=8F=8A?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/binarywang/WxJava/sessions/f3aba758-8b4a-479f-96bd-88ce00a9c176 Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../api/impl/WxMaInternetServiceImpl.java | 5 +- .../wx/miniapp/util/crypt/WxMaCryptUtils.java | 72 +++++++++++++++++++ .../util/crypt/WxMaCryptUtilsTest.java | 33 +++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java index 7da44ddaba..91d11795f3 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java @@ -9,6 +9,7 @@ import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; +import org.apache.commons.codec.binary.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -26,7 +27,7 @@ public class WxMaInternetServiceImpl implements WxMaInternetService { private String sha256(String data, String sessionKey) throws Exception { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); - SecretKeySpec secret_key = new SecretKeySpec(sessionKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(Base64.decodeBase64(sessionKey), "HmacSHA256"); sha256_HMAC.init(secret_key); byte[] array = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); @@ -57,7 +58,7 @@ public WxMaInternetResponse getUserEncryptKey(String openid, String sessionKey) private WxMaInternetResponse getWxMaInternetResponse(String url) throws WxErrorException { String responseContent = this.wxMaService.post(url, ""); WxMaInternetResponse response = WxMaGsonBuilder.create().fromJson(responseContent, WxMaInternetResponse.class); - if (response.getErrcode() == -1) { + if (response.getErrcode() != null && response.getErrcode() != 0) { throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp)); } return response; diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java index 2343634bfc..3c01e0bc7c 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java @@ -84,4 +84,76 @@ public static String decryptAnotherWay(String sessionKey, String encryptedData, } } + /** + * 使用用户加密 key 对数据进行 AES-128-CBC 解密(用于小程序加密网络通道). + * + *
+ * 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html + * encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码) + * iv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码) + *+ * + * @param encryptKey 用户加密 key(Base64 编码) + * @param hexIv 加密 iv(Hex 编码) + * @param encryptedData 加密数据(Base64 编码) + * @return 解密后的字符串 + */ + public static String decryptWithEncryptKey(String encryptKey, String hexIv, String encryptedData) { + try { + byte[] keyBytes = Base64.decodeBase64(encryptKey); + byte[] ivBytes = hexToBytes(hexIv); + byte[] dataBytes = Base64.decodeBase64(encryptedData); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(keyBytes, "AES"), + new IvParameterSpec(ivBytes)); + return new String(cipher.doFinal(dataBytes), UTF_8); + } catch (Exception e) { + throw new WxRuntimeException("AES解密失败!", e); + } + } + + /** + * 使用用户加密 key 对数据进行 AES-128-CBC 加密(用于小程序加密网络通道). + * + *
+ * 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html + * encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码) + * iv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码) + *+ * + * @param encryptKey 用户加密 key(Base64 编码) + * @param hexIv 加密 iv(Hex 编码) + * @param data 待加密的明文字符串 + * @return 加密后的数据(Base64 编码) + */ + public static String encryptWithEncryptKey(String encryptKey, String hexIv, String data) { + try { + byte[] keyBytes = Base64.decodeBase64(encryptKey); + byte[] ivBytes = hexToBytes(hexIv); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(keyBytes, "AES"), + new IvParameterSpec(ivBytes)); + return Base64.encodeBase64String(cipher.doFinal(data.getBytes(UTF_8))); + } catch (Exception e) { + throw new WxRuntimeException("AES加密失败!", e); + } + } + + /** + * 将 Hex 字符串转换为字节数组. + */ + private static byte[] hexToBytes(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + } diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java index 76b4e96743..74e40abd84 100644 --- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java +++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java @@ -32,4 +32,37 @@ public void testDecryptAnotherWay() { assertThat(WxMaCryptUtils.decrypt(sessionKey, encryptedData, ivStr)) .isEqualTo(WxMaCryptUtils.decryptAnotherWay(sessionKey, encryptedData, ivStr)); } + + /** + * 测试使用用户加密 key(来自小程序加密网络通道)进行加密和解密的对称性. + * encrypt_key 为 Base64 编码的 16 字节 AES-128 密钥,iv 为 Hex 编码的 16 字节初始向量。 + */ + @Test + public void testEncryptAndDecryptWithEncryptKey() { + // 模拟来自 getUserEncryptKey 接口的 encrypt_key(Base64)和 iv(Hex) + String encryptKey = "VI6BpyrK9XH4i4AIGe86tg=="; + String hexIv = "6003f73ec441c3866003f73ec441c386"; + String plainText = "{\"userId\":\"12345\",\"amount\":100}"; + + String encrypted = WxMaCryptUtils.encryptWithEncryptKey(encryptKey, hexIv, plainText); + assertThat(encrypted).isNotNull().isNotEmpty(); + + String decrypted = WxMaCryptUtils.decryptWithEncryptKey(encryptKey, hexIv, encrypted); + assertThat(decrypted).isEqualTo(plainText); + } + + /** + * 测试使用已知密文验证解密结果(加密网络通道). + */ + @Test + public void testDecryptWithEncryptKey() { + String encryptKey = "VI6BpyrK9XH4i4AIGe86tg=="; + String hexIv = "6003f73ec441c3866003f73ec441c386"; + String plainText = "hello miniprogram"; + + // 先加密再解密,验证对称性 + String encrypted = WxMaCryptUtils.encryptWithEncryptKey(encryptKey, hexIv, plainText); + String decrypted = WxMaCryptUtils.decryptWithEncryptKey(encryptKey, hexIv, encrypted); + assertThat(decrypted).isEqualTo(plainText); + } } From ee161b56f1691af4092c38ac04fed09b0c0f5631 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:30:17 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98=EF=BC=9AhexToBytes=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BE=93=E5=85=A5=E9=AA=8C=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E6=B5=8B=E8=AF=95=E4=BB=A3=E7=A0=81=E8=B4=A8?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/binarywang/WxJava/sessions/f3aba758-8b4a-479f-96bd-88ce00a9c176 Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../wx/miniapp/util/crypt/WxMaCryptUtils.java | 15 +++++++++++-- .../util/crypt/WxMaCryptUtilsTest.java | 22 +++++++++---------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java index 3c01e0bc7c..115daa1b36 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java @@ -145,13 +145,24 @@ public static String encryptWithEncryptKey(String encryptKey, String hexIv, Stri /** * 将 Hex 字符串转换为字节数组. + * + * @param hex Hex 字符串(长度必须为偶数,只包含 0-9 和 a-f/A-F 字符) + * @return 字节数组 + * @throws IllegalArgumentException 如果输入不是合法的 Hex 字符串 */ private static byte[] hexToBytes(String hex) { + if (hex == null || hex.length() % 2 != 0) { + throw new IllegalArgumentException("无效的十六进制字符串格式:长度必须为偶数"); + } int len = hex.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) - + Character.digit(hex.charAt(i + 1), 16)); + int high = Character.digit(hex.charAt(i), 16); + int low = Character.digit(hex.charAt(i + 1), 16); + if (high == -1 || low == -1) { + throw new IllegalArgumentException("无效的十六进制字符串格式:包含非法字符 '" + hex.charAt(high == -1 ? i : i + 1) + "'"); + } + data[i / 2] = (byte) ((high << 4) + low); } return data; } diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java index 74e40abd84..c4285d25f1 100644 --- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java +++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java @@ -14,6 +14,10 @@ * @author Binary Wang */ public class WxMaCryptUtilsTest { + // 模拟来自 getUserEncryptKey 接口返回的 encrypt_key(Base64)和 iv(Hex,32位即16字节) + private static final String ENCRYPT_KEY = "VI6BpyrK9XH4i4AIGe86tg=="; + private static final String HEX_IV = "6003f73ec441c3866003f73ec441c386"; + @Test public void testDecrypt() { String sessionKey = "7MG7jbTToVVRWRXVA885rg=="; @@ -39,30 +43,24 @@ public void testDecryptAnotherWay() { */ @Test public void testEncryptAndDecryptWithEncryptKey() { - // 模拟来自 getUserEncryptKey 接口的 encrypt_key(Base64)和 iv(Hex) - String encryptKey = "VI6BpyrK9XH4i4AIGe86tg=="; - String hexIv = "6003f73ec441c3866003f73ec441c386"; String plainText = "{\"userId\":\"12345\",\"amount\":100}"; - String encrypted = WxMaCryptUtils.encryptWithEncryptKey(encryptKey, hexIv, plainText); + String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText); assertThat(encrypted).isNotNull().isNotEmpty(); - String decrypted = WxMaCryptUtils.decryptWithEncryptKey(encryptKey, hexIv, encrypted); + String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted); assertThat(decrypted).isEqualTo(plainText); } /** - * 测试使用已知密文验证解密结果(加密网络通道). + * 测试加密网络通道的加解密对称性(不同明文). */ @Test - public void testDecryptWithEncryptKey() { - String encryptKey = "VI6BpyrK9XH4i4AIGe86tg=="; - String hexIv = "6003f73ec441c3866003f73ec441c386"; + public void testEncryptDecryptSymmetryWithEncryptKey() { String plainText = "hello miniprogram"; - // 先加密再解密,验证对称性 - String encrypted = WxMaCryptUtils.encryptWithEncryptKey(encryptKey, hexIv, plainText); - String decrypted = WxMaCryptUtils.decryptWithEncryptKey(encryptKey, hexIv, encrypted); + String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText); + String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted); assertThat(decrypted).isEqualTo(plainText); } }