DingCallbackCrypto.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. package com.malk.server.dingtalk.crypto;
  2. import com.google.common.io.BaseEncoding;
  3. import org.apache.commons.codec.binary.Base64;
  4. import javax.crypto.Cipher;
  5. import javax.crypto.spec.IvParameterSpec;
  6. import javax.crypto.spec.SecretKeySpec;
  7. import java.io.ByteArrayOutputStream;
  8. import java.lang.reflect.Field;
  9. import java.nio.charset.Charset;
  10. import java.security.MessageDigest;
  11. import java.security.Permission;
  12. import java.security.PermissionCollection;
  13. import java.security.Security;
  14. import java.util.Arrays;
  15. import java.util.HashMap;
  16. import java.util.Map;
  17. import java.util.Random;
  18. /**
  19. * 钉钉开放平台加解密方法
  20. * 在ORACLE官方网站下载JCE无限制权限策略文件
  21. * JDK6的下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
  22. * JDK7的下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  23. * JDK8的下载地址 https://www.oracle.com/java/technologies/javase-jce8-downloads.html
  24. */
  25. public class DingCallbackCrypto {
  26. private static final Charset CHARSET = Charset.forName("utf-8");
  27. private static final Base64 base64 = new Base64();
  28. private byte[] aesKey;
  29. private String token;
  30. private String corpId;
  31. /**
  32. * ask getPaddingBytes key固定长度
  33. **/
  34. private static final Integer AES_ENCODE_KEY_LENGTH = 43;
  35. /**
  36. * 加密随机字符串字节长度
  37. **/
  38. private static final Integer RANDOM_LENGTH = 16;
  39. /**
  40. * 构造函数
  41. *
  42. * @param token 钉钉开放平台上,开发者设置的token
  43. * @param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey
  44. * @param corpId 企业自建应用-事件订阅, 使用appKey
  45. * 企业自建应用-注册回调地址, 使用corpId
  46. * 第三方企业应用, 使用suiteKey
  47. * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
  48. */
  49. public DingCallbackCrypto(String token, String encodingAesKey, String corpId) throws DingTalkEncryptException {
  50. if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
  51. throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL);
  52. }
  53. this.token = token;
  54. this.corpId = corpId;
  55. // ppExt: 23.10.26 钉钉新方式以Steam接入, HTTP形式commonsc-codec在升级之后,其内部做了一个validateCharacter校验. 使用 guava 替代
  56. aesKey = BaseEncoding.base64().decode(encodingAesKey + "=");
  57. }
  58. public Map<String, String> getEncryptedMap(String plaintext) throws DingTalkEncryptException {
  59. return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
  60. }
  61. /**
  62. * 将和钉钉开放平台同步的消息体加密,返回加密Map
  63. *
  64. * @param plaintext 传递的消息体明文
  65. * @param timeStamp 时间戳
  66. * @param nonce 随机字符串
  67. * @return
  68. * @throws DingTalkEncryptException
  69. */
  70. public Map<String, String> getEncryptedMap(String plaintext, Long timeStamp, String nonce)
  71. throws DingTalkEncryptException {
  72. if (null == plaintext) {
  73. throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
  74. }
  75. if (null == timeStamp) {
  76. throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
  77. }
  78. if (null == nonce) {
  79. throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL);
  80. }
  81. // 加密
  82. String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
  83. String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
  84. Map<String, String> resultMap = new HashMap<String, String>();
  85. resultMap.put("msg_signature", signature);
  86. resultMap.put("encrypt", encrypt);
  87. resultMap.put("timeStamp", String.valueOf(timeStamp));
  88. resultMap.put("nonce", nonce);
  89. return resultMap;
  90. }
  91. /**
  92. * 密文解密
  93. *
  94. * @param msgSignature 签名串
  95. * @param timeStamp 时间戳
  96. * @param nonce 随机串
  97. * @param encryptMsg 密文
  98. * @return 解密后的原文
  99. * @throws DingTalkEncryptException
  100. */
  101. public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
  102. throws DingTalkEncryptException {
  103. //校验签名
  104. String signature = getSignature(token, timeStamp, nonce, encryptMsg);
  105. if (!signature.equals(msgSignature)) {
  106. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
  107. }
  108. // 解密
  109. String result = decrypt(encryptMsg);
  110. return result;
  111. }
  112. /*
  113. * 对明文加密.
  114. * @param text 需要加密的明文
  115. * @return 加密后base64编码的字符串
  116. */
  117. private String encrypt(String random, String plaintext) throws DingTalkEncryptException {
  118. try {
  119. byte[] randomBytes = random.getBytes(CHARSET);
  120. byte[] plainTextBytes = plaintext.getBytes(CHARSET);
  121. byte[] lengthByte = Utils.int2Bytes(plainTextBytes.length);
  122. byte[] corpidBytes = corpId.getBytes(CHARSET);
  123. ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
  124. byteStream.write(randomBytes);
  125. byteStream.write(lengthByte);
  126. byteStream.write(plainTextBytes);
  127. byteStream.write(corpidBytes);
  128. byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
  129. byteStream.write(padBytes);
  130. byte[] unencrypted = byteStream.toByteArray();
  131. byteStream.close();
  132. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
  133. SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
  134. IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
  135. cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
  136. byte[] encrypted = cipher.doFinal(unencrypted);
  137. String result = base64.encodeToString(encrypted);
  138. return result;
  139. } catch (Exception e) {
  140. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
  141. }
  142. }
  143. /*
  144. * 对密文进行解密.
  145. * @param text 需要解密的密文
  146. * @return 解密得到的明文
  147. */
  148. private String decrypt(String text) throws DingTalkEncryptException {
  149. byte[] originalArr;
  150. try {
  151. // 设置解密模式为AES的CBC模式
  152. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
  153. SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
  154. IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
  155. cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
  156. // 使用BASE64对密文进行解码
  157. byte[] encrypted = Base64.decodeBase64(text);
  158. // 解密
  159. originalArr = cipher.doFinal(encrypted);
  160. } catch (Exception e) {
  161. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
  162. }
  163. String plainText;
  164. String fromCorpid;
  165. try {
  166. // 去除补位字符
  167. byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
  168. // 分离16位随机字符串,网络字节序和corpId
  169. byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
  170. int plainTextLegth = Utils.bytes2int(networkOrder);
  171. plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
  172. fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
  173. } catch (Exception e) {
  174. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
  175. }
  176. // corpid不相同的情况
  177. if (!fromCorpid.equals(corpId)) {
  178. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
  179. }
  180. return plainText;
  181. }
  182. /**
  183. * 数字签名
  184. *
  185. * @param token isv token
  186. * @param timestamp 时间戳
  187. * @param nonce 随机串
  188. * @param encrypt 加密文本
  189. * @return
  190. * @throws DingTalkEncryptException
  191. */
  192. public String getSignature(String token, String timestamp, String nonce, String encrypt)
  193. throws DingTalkEncryptException {
  194. try {
  195. String[] array = new String[]{token, timestamp, nonce, encrypt};
  196. Arrays.sort(array);
  197. // System.out.println(JSON.toJSONString(array));
  198. StringBuffer sb = new StringBuffer();
  199. for (int i = 0; i < 4; i++) {
  200. sb.append(array[i]);
  201. }
  202. String str = sb.toString();
  203. // System.out.println(str);
  204. MessageDigest md = MessageDigest.getInstance("SHA-1");
  205. md.update(str.getBytes());
  206. byte[] digest = md.digest();
  207. StringBuffer hexstr = new StringBuffer();
  208. String shaHex = "";
  209. for (int i = 0; i < digest.length; i++) {
  210. shaHex = Integer.toHexString(digest[i] & 0xFF);
  211. if (shaHex.length() < 2) {
  212. hexstr.append(0);
  213. }
  214. hexstr.append(shaHex);
  215. }
  216. return hexstr.toString();
  217. } catch (Exception e) {
  218. throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
  219. }
  220. }
  221. public static class Utils {
  222. public Utils() {
  223. }
  224. public static String getRandomStr(int count) {
  225. String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  226. Random random = new Random();
  227. StringBuffer sb = new StringBuffer();
  228. for (int i = 0; i < count; ++i) {
  229. int number = random.nextInt(base.length());
  230. sb.append(base.charAt(number));
  231. }
  232. return sb.toString();
  233. }
  234. public static byte[] int2Bytes(int count) {
  235. byte[] byteArr = new byte[]{(byte) (count >> 24 & 255), (byte) (count >> 16 & 255), (byte) (count >> 8 & 255),
  236. (byte) (count & 255)};
  237. return byteArr;
  238. }
  239. public static int bytes2int(byte[] byteArr) {
  240. int count = 0;
  241. for (int i = 0; i < 4; ++i) {
  242. count <<= 8;
  243. count |= byteArr[i] & 255;
  244. }
  245. return count;
  246. }
  247. }
  248. public static class PKCS7Padding {
  249. private static final Charset CHARSET = Charset.forName("utf-8");
  250. private static final int BLOCK_SIZE = 32;
  251. public PKCS7Padding() {
  252. }
  253. public static byte[] getPaddingBytes(int count) {
  254. int amountToPad = 32 - count % 32;
  255. if (amountToPad == 0) {
  256. amountToPad = 32;
  257. }
  258. char padChr = chr(amountToPad);
  259. String tmp = new String();
  260. for (int index = 0; index < amountToPad; ++index) {
  261. tmp = tmp + padChr;
  262. }
  263. return tmp.getBytes(CHARSET);
  264. }
  265. public static byte[] removePaddingBytes(byte[] decrypted) {
  266. int pad = decrypted[decrypted.length - 1];
  267. if (pad < 1 || pad > 32) {
  268. pad = 0;
  269. }
  270. return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
  271. }
  272. private static char chr(int a) {
  273. byte target = (byte) (a & 255);
  274. return (char) target;
  275. }
  276. }
  277. public static class DingTalkEncryptException extends Exception {
  278. public static final int SUCCESS = 0;
  279. public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
  280. public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
  281. public static final int ENCRYPTION_NONCE_ILLEGAL = 900003;
  282. public static final int AES_KEY_ILLEGAL = 900004;
  283. public static final int SIGNATURE_NOT_MATCH = 900005;
  284. public static final int COMPUTE_SIGNATURE_ERROR = 900006;
  285. public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
  286. public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
  287. public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
  288. public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
  289. private static final long serialVersionUID = 4139585721260116198L;
  290. private static Map<Integer, String> msgMap = new HashMap();
  291. private Integer code;
  292. static {
  293. msgMap.put(0, "成功");
  294. msgMap.put(900001, "加密明文文本非法");
  295. msgMap.put(900002, "加密时间戳参数非法");
  296. msgMap.put(900003, "加密随机字符串参数非法");
  297. msgMap.put(900005, "签名不匹配");
  298. msgMap.put(900006, "签名计算失败");
  299. msgMap.put(900004, "不合法的aes key");
  300. msgMap.put(900007, "计算加密文字错误");
  301. msgMap.put(900008, "计算解密文字错误");
  302. msgMap.put(900009, "计算解密文字长度不匹配");
  303. msgMap.put(900010, "计算解密文字corpid不匹配");
  304. }
  305. public Integer getCode() {
  306. return this.code;
  307. }
  308. public DingTalkEncryptException(Integer exceptionCode) {
  309. super((String) msgMap.get(exceptionCode));
  310. this.code = exceptionCode;
  311. }
  312. }
  313. static {
  314. try {
  315. Security.setProperty("crypto.policy", "limited");
  316. RemoveCryptographyRestrictions();
  317. } catch (Exception var1) {
  318. }
  319. }
  320. private static void RemoveCryptographyRestrictions() throws Exception {
  321. Class<?> jceSecurity = getClazz("javax.crypto.JceSecurity");
  322. Class<?> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions");
  323. Class<?> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission");
  324. if (jceSecurity != null) {
  325. setFinalStaticValue(jceSecurity, "isRestricted", false);
  326. PermissionCollection defaultPolicy = (PermissionCollection) getFieldValue(jceSecurity, "defaultPolicy", (Object) null, PermissionCollection.class);
  327. if (cryptoPermissions != null) {
  328. Map<?, ?> map = (Map) getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class);
  329. map.clear();
  330. }
  331. if (cryptoAllPermission != null) {
  332. Permission permission = (Permission) getFieldValue(cryptoAllPermission, "INSTANCE", (Object) null, Permission.class);
  333. defaultPolicy.add(permission);
  334. }
  335. }
  336. }
  337. private static Class<?> getClazz(String className) {
  338. Class clazz = null;
  339. try {
  340. clazz = Class.forName(className);
  341. } catch (Exception var3) {
  342. }
  343. return clazz;
  344. }
  345. private static void setFinalStaticValue(Class<?> srcClazz, String fieldName, Object newValue) throws Exception {
  346. Field field = srcClazz.getDeclaredField(fieldName);
  347. field.setAccessible(true);
  348. Field modifiersField = Field.class.getDeclaredField("modifiers");
  349. modifiersField.setAccessible(true);
  350. modifiersField.setInt(field, field.getModifiers() & -17);
  351. field.set((Object) null, newValue);
  352. }
  353. private static <T> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception {
  354. Field field = srcClazz.getDeclaredField(fieldName);
  355. field.setAccessible(true);
  356. return dstClazz.cast(field.get(owner));
  357. }
  358. }