Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class WxMaInternetUserKeyInfo implements Serializable {
private Long expireIn;

/**
* 加密iv
* 加密iv(Hex 编码,通常为 32 位十六进制字符,解码后为 16 字节,用于 AES-128-CBC)
*/
private String iv;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,103 @@ public static String decryptAnotherWay(String sessionKey, String encryptedData,
}
}

/**
* 使用用户加密 key 对数据进行 AES-128-CBC 解密(用于小程序加密网络通道).
*
* <pre>
* 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html
* encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码,解码后须为 16 字节)
* hexIv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码,须为 32 位十六进制字符,解码后为 16 字节)
* </pre>
*
* @param encryptKey 用户加密 key(Base64 编码,解码后须为 16 字节)
* @param hexIv 加密 iv(Hex 编码,须为 32 位十六进制字符)
* @param encryptedData 加密数据(Base64 编码)
* @return 解密后的字符串
* @throws IllegalArgumentException 如果 encryptKey 解码后不为 16 字节,或 hexIv 格式非法/解码后不为 16 字节
*/
public static String decryptWithEncryptKey(String encryptKey, String hexIv, String encryptedData) {
byte[] keyBytes = Base64.decodeBase64(encryptKey);
if (keyBytes.length != 16) {
throw new IllegalArgumentException(
"encryptKey 解码后必须为 16 字节(AES-128),实际为 " + keyBytes.length + " 字节");
}
byte[] ivBytes = hexToBytes(hexIv);
if (ivBytes.length != 16) {
throw new IllegalArgumentException(
"hexIv 解码后必须为 16 字节(AES-128-CBC),实际为 " + ivBytes.length + " 字节(需 32 位 Hex 字符串)");
}
byte[] dataBytes = Base64.decodeBase64(encryptedData);
try {
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) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decryptWithEncryptKey catches all Exception and wraps it into WxRuntimeException, which means invalid hexIv (from hexToBytes’s IllegalArgumentException) won’t be observable as IllegalArgumentException by callers and the specific validation message is lost; is that intended given the Javadoc contract on hexToBytes? Other locations where this applies: weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java:141.

Severity: medium

Other Locations
  • weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java:141

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

throw new WxRuntimeException("AES解密失败!", e);
}
Comment on lines +102 to +122
}

/**
* 使用用户加密 key 对数据进行 AES-128-CBC 加密(用于小程序加密网络通道).
*
* <pre>
* 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html
* encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码,解码后须为 16 字节)
* hexIv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码,须为 32 位十六进制字符,解码后为 16 字节)
* </pre>
*
* @param encryptKey 用户加密 key(Base64 编码,解码后须为 16 字节)
* @param hexIv 加密 iv(Hex 编码,须为 32 位十六进制字符)
* @param data 待加密的明文字符串
* @return 加密后的数据(Base64 编码)
* @throws IllegalArgumentException 如果 encryptKey 解码后不为 16 字节,或 hexIv 格式非法/解码后不为 16 字节
*/
public static String encryptWithEncryptKey(String encryptKey, String hexIv, String data) {
byte[] keyBytes = Base64.decodeBase64(encryptKey);
if (keyBytes.length != 16) {
throw new IllegalArgumentException(
"encryptKey 解码后必须为 16 字节(AES-128),实际为 " + keyBytes.length + " 字节");
}
byte[] ivBytes = hexToBytes(hexIv);
if (ivBytes.length != 16) {
throw new IllegalArgumentException(
"hexIv 解码后必须为 16 字节(AES-128-CBC),实际为 " + ivBytes.length + " 字节(需 32 位 Hex 字符串)");
}
try {
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);
}
Comment on lines +140 to +159
}

/**
* 将 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) {
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);
}
Comment on lines +169 to +182
return data;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.testng.annotations.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* <pre>
Expand All @@ -14,6 +15,11 @@
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public class WxMaCryptUtilsTest {
// 模拟来自 getUserEncryptKey 接口返回的 encrypt_key(Base64,解码后 16 字节)
// 和 iv(Hex,32 位十六进制字符,解码后 16 字节,AES-128-CBC 要求)
private static final String ENCRYPT_KEY = "VI6BpyrK9XH4i4AIGe86tg==";
private static final String HEX_IV = "6003f73ec441c3866003f73ec441c386";

@Test
public void testDecrypt() {
String sessionKey = "7MG7jbTToVVRWRXVA885rg==";
Expand All @@ -32,4 +38,98 @@ 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() {
String plainText = "{\"userId\":\"12345\",\"amount\":100}";

String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText);
Copy link
Copy Markdown

@augmentcode augmentcode Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests only assert encrypt/decrypt round-trip symmetry, so they can still pass even if the algorithm/encoding doesn’t match WeChat’s expected wire format (both sides could be consistently wrong). Consider adding at least one fixed test vector (known plaintext → expected ciphertext, or vice versa) from the official doc/examples to validate interoperability.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

assertThat(encrypted).isNotNull().isNotEmpty();

String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted);
assertThat(decrypted).isEqualTo(plainText);
}

/**
* 测试加密网络通道的加解密对称性(不同明文).
*/
@Test
public void testEncryptDecryptSymmetryWithEncryptKey() {
String plainText = "hello miniprogram";

String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText);
String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted);
assertThat(decrypted).isEqualTo(plainText);
}
Comment on lines +46 to +67

/**
* 测试 hexIv 为奇数长度时,应抛出 IllegalArgumentException.
*/
@Test
public void testEncryptWithEncryptKeyInvalidHexIvOddLength() {
assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, "abc", "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("长度必须为偶数");
}

/**
* 测试 hexIv 包含非十六进制字符时,应抛出 IllegalArgumentException.
*/
@Test
public void testEncryptWithEncryptKeyInvalidHexIvNonHexChar() {
// 32 位但含非法字符 'z'
assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(
ENCRYPT_KEY, "6003f73ec441c3866003f73ec441z386", "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("非法字符");
}

/**
* 测试 hexIv 解码后不足 16 字节(如仅 16 位 hex = 8 字节)时,应抛出 IllegalArgumentException.
*/
@Test
public void testEncryptWithEncryptKeyShortHexIv() {
// 16 位 hex = 8 字节,不满足 AES-CBC 要求的 16 字节
assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(
ENCRYPT_KEY, "6003f73ec441c386", "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("hexIv 解码后必须为 16 字节");
}

/**
* 测试 encryptKey 解码后不足 16 字节时,应抛出 IllegalArgumentException.
*/
@Test
public void testEncryptWithEncryptKeyShortKey() {
// Base64 编码的 8 字节 key(不符合 AES-128 要求)
String shortKey = java.util.Base64.getEncoder().encodeToString(new byte[8]);
assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(shortKey, HEX_IV, "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("encryptKey 解码后必须为 16 字节");
}

/**
* 测试 decryptWithEncryptKey 使用非法 hexIv 时,应抛出 IllegalArgumentException.
*/
@Test
public void testDecryptWithEncryptKeyInvalidHexIv() {
assertThatThrownBy(() -> WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, "abc", "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("长度必须为偶数");
}

/**
* 测试 decryptWithEncryptKey encryptKey 长度不合法时,应抛出 IllegalArgumentException.
*/
@Test
public void testDecryptWithEncryptKeyShortKey() {
String shortKey = java.util.Base64.getEncoder().encodeToString(new byte[8]);
assertThatThrownBy(() -> WxMaCryptUtils.decryptWithEncryptKey(shortKey, HEX_IV, "data"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("encryptKey 解码后必须为 16 字节");
}
}