Skip to content
Draft
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 @@ -84,4 +84,87 @@ 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 编码)
* iv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码)
* </pre>
*
* @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 加密(用于小程序加密网络通道).
*
* <pre>
* 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html
* encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码)
* iv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码)
* </pre>
*
* @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 字符串转换为字节数组.
*
* @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);
}
return data;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
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==";
Expand All @@ -32,4 +36,31 @@ 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);
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);
}
}