-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
feat(miniapp): 实现小程序加密网络通道服务端支持,修复 HMAC 签名与错误处理 Bug #3969
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Severity: medium Other Locations
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| throw new WxRuntimeException("AES解密失败!", e); | ||
| } | ||
|
Comment on lines
+101
to
+114
|
||
| } | ||
|
|
||
| /** | ||
| * 使用用户加密 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); | ||
| } | ||
|
Comment on lines
+131
to
+143
|
||
| } | ||
|
|
||
| /** | ||
| * 将 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
+153
to
+166
|
||
| return data; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
||
|
Comment on lines
+17
to
+20
|
||
| @Test | ||
| public void testDecrypt() { | ||
| String sessionKey = "7MG7jbTToVVRWRXVA885rg=="; | ||
|
|
@@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤖 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
+44
to
+65
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decryptWithEncryptKeyunconditionally hex-decodesiv(hexToBytes), but the project’s owngetUserEncryptKeyresponse examples use 16-character values like6003f73ec441c386(WxMaInternetResponse/WxMaInternetUserKeyInfo), which become only 8 bytes after hex decoding; AES-CBC requires a 16-byte IV, so this path will throw at runtime for documented API-shaped inputs and make the new encrypt/decrypt helpers unusable in that scenario.Useful? React with 👍 / 👎.