|
|
@@ -0,0 +1,395 @@
|
|
|
+package com.uas.eis.core.config;
|
|
|
+
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.nio.charset.Charset;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.security.Permission;
|
|
|
+import java.security.PermissionCollection;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map ;
|
|
|
+import java.util.Random;
|
|
|
+import java.security.Security ;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.spec.IvParameterSpec;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
+
|
|
|
+/**
|
|
|
+ * DingTalk open platform encryption and decryption method
|
|
|
+ * Download JCE Unrestricted Permission Policy File from ORACLE Official Website
|
|
|
+ */
|
|
|
+
|
|
|
+public class DingCallbackCrypto {
|
|
|
+ private static final Charset CHARSET = Charset.forName("utf-8");
|
|
|
+ private static final Base64 base64 = new Base64();
|
|
|
+ private byte[] aesKey;
|
|
|
+ private String token ;
|
|
|
+ private String corpId;
|
|
|
+ /**
|
|
|
+ * ask getPaddingBytes key fixed length
|
|
|
+ **/
|
|
|
+ private static final Integer AES_ENCODE_KEY_LENGTH = 43;
|
|
|
+ /**
|
|
|
+ * Encrypted random string byte length
|
|
|
+ **/
|
|
|
+ private static final Integer RANDOM_LENGTH = 16;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructor
|
|
|
+ *
|
|
|
+ * @param token On the DingTalk open platform, the token set by the developer
|
|
|
+ * @param encodingAesKey The EncodingAESKey set by the developer on the DingTalk open platform
|
|
|
+ * @param corpId Enterprise self-built application-event subscription, use appKey
|
|
|
+ * Enterprise self-built application - register callback address, use corpId
|
|
|
+ * Third-party enterprise applications, use suiteKey
|
|
|
+ *
|
|
|
+ * @throws DingTalkEncryptException failed to execute, please check the exception's error code and specific error message
|
|
|
+ */
|
|
|
+ public DingCallbackCrypto(String token, String encodingAesKey, String corpId) throws DingTalkEncryptException {
|
|
|
+ if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL);
|
|
|
+ }
|
|
|
+ this.token = token;
|
|
|
+ this.corpId = corpId;
|
|
|
+ aesKey = Base64.decodeBase64(encodingAesKey + "=");
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, String> getEncryptedMap(String plaintext) throws DingTalkEncryptException {
|
|
|
+ return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Encrypt the message body synchronized with the DingTalk open platform and return the encrypted Map
|
|
|
+ *
|
|
|
+ * @param plaintext The message body plaintext passed
|
|
|
+ * @param timeStamp timestamp
|
|
|
+ * @param nonce random string
|
|
|
+ * @return
|
|
|
+ * @throws DingTalkEncryptException
|
|
|
+ */
|
|
|
+ public Map<String, String> getEncryptedMap(String plaintext, Long timeStamp, String nonce)
|
|
|
+ throws DingTalkEncryptException {
|
|
|
+ if (null == plaintext) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
|
|
|
+ }
|
|
|
+ if (null == timeStamp) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
|
|
|
+ }
|
|
|
+ if (null == nonce) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL);
|
|
|
+ }
|
|
|
+ // encrypt
|
|
|
+ String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
|
|
|
+ String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
|
|
|
+ Map<String, String> resultMap = new HashMap<String, String>();
|
|
|
+ resultMap.put("msg_signature", signature);
|
|
|
+ resultMap.put("encrypt", encrypt);
|
|
|
+ resultMap.put("timeStamp", String.valueOf(timeStamp));
|
|
|
+ resultMap.put("nonce", nonce);
|
|
|
+ return resultMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ciphertext decryption
|
|
|
+ *
|
|
|
+ * @param msgSignature signature string
|
|
|
+ * @param timeStamp timestamp
|
|
|
+ * @param nonce random string
|
|
|
+ * @param encryptMsg ciphertext
|
|
|
+ * @return decrypted original text
|
|
|
+ * @throws DingTalkEncryptException
|
|
|
+ */
|
|
|
+ public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
|
|
|
+ throws DingTalkEncryptException {
|
|
|
+ //check signature
|
|
|
+ String signature = getSignature(token, timeStamp, nonce, encryptMsg);
|
|
|
+ if (!signature.equals(msgSignature)) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
|
|
|
+ }
|
|
|
+ // decrypt
|
|
|
+ String result = decrypt(encryptMsg);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Encrypt plaintext.
|
|
|
+ * @param text The plaintext to be encrypted
|
|
|
+ * @return encrypted base64 encoded string
|
|
|
+ */
|
|
|
+ private String encrypt(String random, String plaintext) throws DingTalkEncryptException {
|
|
|
+ try {
|
|
|
+ byte[] randomBytes = random.getBytes(CHARSET);
|
|
|
+ byte[] plainTextBytes = plaintext.getBytes(CHARSET);
|
|
|
+ byte[] lengthByte = Utils.int2Bytes(plainTextBytes.length);
|
|
|
+ byte[] corpidBytes = corpId.getBytes(CHARSET);
|
|
|
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
|
|
+ byteStream.write(randomBytes);
|
|
|
+ byteStream.write(lengthByte);
|
|
|
+ byteStream.write(plainTextBytes);
|
|
|
+ byteStream.write(corpidBytes);
|
|
|
+ byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
|
|
|
+ byteStream.write(padBytes);
|
|
|
+ byte[] unencrypted = byteStream.toByteArray();
|
|
|
+ byteStream.close();
|
|
|
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
|
|
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
|
|
|
+ IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
|
|
|
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
|
|
|
+ byte[] encrypted = cipher.doFinal(unencrypted);
|
|
|
+ String result = base64.encodeToString(encrypted);
|
|
|
+ return result;
|
|
|
+ } catch ( Exception e ) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Decrypt the ciphertext.
|
|
|
+ * @param text The ciphertext to decrypt
|
|
|
+ * @return decrypted plaintext
|
|
|
+ */
|
|
|
+ private String decrypt(String text) throws DingTalkEncryptException {
|
|
|
+ byte [] originalArr ;
|
|
|
+ try {
|
|
|
+ // Set the decryption mode to CBC mode of AES
|
|
|
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
|
|
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
|
|
|
+ IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
|
|
|
+ // Decode the ciphertext using BASE64
|
|
|
+ byte[] encrypted = Base64.decodeBase64(text);
|
|
|
+ // decrypt
|
|
|
+ originalArr = cipher.doFinal(encrypted);
|
|
|
+ } catch ( Exception e ) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ String plainText;
|
|
|
+ String fromCorpid;
|
|
|
+ try {
|
|
|
+ // remove the complement character
|
|
|
+ byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
|
|
|
+ // Separate 16-bit random string, network byte order and corpId
|
|
|
+ byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
|
|
|
+ int plainTextLegth = Utils.bytes2int(networkOrder);
|
|
|
+ plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
|
|
|
+ fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
|
|
|
+ } catch ( Exception e ) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ // When the corpids are not the same
|
|
|
+ if (!fromCorpid.equals(corpId)) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
|
|
|
+ }
|
|
|
+ return plainText;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * digital signature
|
|
|
+ *
|
|
|
+ * @param token isv token
|
|
|
+ * @param timestamp timestamp
|
|
|
+ * @param nonce random string
|
|
|
+ * @param encrypt encrypted text
|
|
|
+ * @return
|
|
|
+ * @throws DingTalkEncryptException
|
|
|
+ */
|
|
|
+ public String getSignature(String token, String timestamp, String nonce, String encrypt)
|
|
|
+ throws DingTalkEncryptException {
|
|
|
+ try {
|
|
|
+ String[] array = new String[] {token, timestamp, nonce, encrypt};
|
|
|
+ Arrays.sort(array);
|
|
|
+// System.out.println(JSON.toJSONString(array));
|
|
|
+ StringBuffer sb = new StringBuffer();
|
|
|
+ for (int i = 0; i < 4; i++) {
|
|
|
+ sb.append(array[i]);
|
|
|
+ }
|
|
|
+ String str = sb.toString();
|
|
|
+// System.out.println(str);
|
|
|
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
|
+ md.update(str.getBytes());
|
|
|
+ byte[] digest = md.digest();
|
|
|
+
|
|
|
+ StringBuffer hexstr = new StringBuffer();
|
|
|
+ String shaHex = "";
|
|
|
+ for (int i = 0; i < digest.length; i++) {
|
|
|
+ shaHex = Integer.toHexString(digest[i] & 0xFF);
|
|
|
+ if (shaHex.length() < 2) {
|
|
|
+ hexstr.append(0);
|
|
|
+ }
|
|
|
+ hexstr.append(shaHex);
|
|
|
+ }
|
|
|
+ return hexstr.toString();
|
|
|
+ } catch ( Exception e ) {
|
|
|
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Utils {
|
|
|
+ public Utils () {
|
|
|
+ }
|
|
|
+
|
|
|
+ public static String getRandomStr(int count) {
|
|
|
+ String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
+ Random random = new Random();
|
|
|
+ StringBuffer sb = new StringBuffer();
|
|
|
+
|
|
|
+ for (int i = 0; i < count; ++i) {
|
|
|
+ int number = random.nextInt(base.length());
|
|
|
+ sb.append(base.charAt(number));
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static byte[] int2Bytes(int count) {
|
|
|
+ byte[] byteArr = new byte[] {(byte)(count >> 24 & 255), (byte)(count >> 16 & 255), (byte)(count >> 8 & 255),
|
|
|
+ (byte)(count & 255)};
|
|
|
+ return byteArr;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static int bytes2int(byte[] byteArr) {
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ for (int i = 0; i < 4; ++i) {
|
|
|
+ count <<= 8;
|
|
|
+ count |= byteArr[i] & 255;
|
|
|
+ }
|
|
|
+
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class PKCS7Padding {
|
|
|
+ private static final Charset CHARSET = Charset.forName("utf-8");
|
|
|
+ private static final int BLOCK_SIZE = 32;
|
|
|
+
|
|
|
+ public PKCS7Padding() {
|
|
|
+ }
|
|
|
+
|
|
|
+ public static byte[] getPaddingBytes(int count) {
|
|
|
+ int amountToPad = 32 - count % 32;
|
|
|
+ if (amountToPad == 0) {
|
|
|
+ amountToPad = 32;
|
|
|
+ }
|
|
|
+
|
|
|
+ char padChr = chr(amountToPad);
|
|
|
+ String tmp = new String();
|
|
|
+
|
|
|
+ for (int index = 0; index < amountToPad; ++index) {
|
|
|
+ tmp = tmp + padChr;
|
|
|
+ }
|
|
|
+
|
|
|
+ return tmp.getBytes(CHARSET);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static byte[] removePaddingBytes(byte[] decrypted) {
|
|
|
+ int pad = decrypted[decrypted.length - 1];
|
|
|
+ if (pad < 1 || pad > 32) {
|
|
|
+ pad = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static char chr ( int a ) {
|
|
|
+ byte target = (byte)(a & 255);
|
|
|
+ return (char)target;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class DingTalkEncryptException extends Exception {
|
|
|
+ public static final int SUCCESS = 0;
|
|
|
+ public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
|
|
|
+ public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
|
|
|
+ public static final int ENCRYPTION_NONCE_ILLEGAL = 900003;
|
|
|
+ public static final int AES_KEY_ILLEGAL = 900004;
|
|
|
+ public static final int SIGNATURE_NOT_MATCH = 900005;
|
|
|
+ public static final int COMPUTE_SIGNATURE_ERROR = 900006;
|
|
|
+ public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
|
|
|
+ public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
|
|
|
+ public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
|
|
|
+ public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
|
|
|
+ private static Map<Integer, String> msgMap = new HashMap();
|
|
|
+ private Integer code ;
|
|
|
+
|
|
|
+ static {
|
|
|
+ msgMap .put ( 0 , " success" ) ;
|
|
|
+ msgMap.put ( 900001 , " Illegal encrypted plaintext" ) ;
|
|
|
+ msgMap.put ( 900002 , " Illegal encryption timestamp parameter" ) ;
|
|
|
+ msgMap.put ( 900003 , " The encrypted random string parameter is illegal" );
|
|
|
+ msgMap.put ( 900005 , "Signature mismatch " ) ;
|
|
|
+ msgMap.put ( 900006 , " Signature calculation failed" ) ;
|
|
|
+ msgMap .put ( 900004 , " Illegal aes key" );
|
|
|
+ msgMap.put ( 900007 , " Error computing encrypted text" ) ;
|
|
|
+ msgMap.put ( 900008 , " Error computing decrypted text" ) ;
|
|
|
+ msgMap.put ( 900009 , " Calculation of decrypted text length does not match" );
|
|
|
+ msgMap.put ( 900010 , "Compute decrypted literal corpid does not match" );
|
|
|
+ }
|
|
|
+
|
|
|
+ public Integer getCode () {
|
|
|
+ return this.code;
|
|
|
+ }
|
|
|
+
|
|
|
+ public DingTalkEncryptException(Integer exceptionCode) {
|
|
|
+ super((String)msgMap.get(exceptionCode));
|
|
|
+ this.code = exceptionCode;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ static {
|
|
|
+ try {
|
|
|
+ Security.setProperty("crypto.policy", "limited");
|
|
|
+ RemoveCryptographyRestrictions();
|
|
|
+ } catch (Exception var1) {
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ private static void RemoveCryptographyRestrictions() throws Exception {
|
|
|
+ Class<?> jceSecurity = getClazz("javax.crypto.JceSecurity");
|
|
|
+ Class<?> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions");
|
|
|
+ Class<?> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission");
|
|
|
+ if (jceSecurity != null) {
|
|
|
+ setFinalStaticValue(jceSecurity, "isRestricted", false);
|
|
|
+ PermissionCollection defaultPolicy = (PermissionCollection)getFieldValue(jceSecurity, "defaultPolicy", (Object)null, PermissionCollection.class);
|
|
|
+ if (cryptoPermissions != null) {
|
|
|
+ Map<?, ?> map = (Map)getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class);
|
|
|
+ map.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cryptoAllPermission != null) {
|
|
|
+ Permission permission = (Permission)getFieldValue(cryptoAllPermission, "INSTANCE", (Object)null, Permission.class);
|
|
|
+ defaultPolicy.add(permission);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ private static Class<?> getClazz(String className) {
|
|
|
+ Class clazz = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ clazz = Class.forName(className);
|
|
|
+ } catch (Exception var3) {
|
|
|
+ }
|
|
|
+
|
|
|
+ return clazz ;
|
|
|
+ }
|
|
|
+ private static void setFinalStaticValue(Class<?> srcClazz, String fieldName, Object newValue) throws Exception {
|
|
|
+ Field field = srcClazz.getDeclaredField(fieldName);
|
|
|
+ field.setAccessible(true);
|
|
|
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
|
|
|
+ modifiersField.setAccessible(true);
|
|
|
+ modifiersField.setInt(field, field.getModifiers() & -17);
|
|
|
+ field.set((Object)null, newValue);
|
|
|
+ }
|
|
|
+ private static <T> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception {
|
|
|
+ Field field = srcClazz.getDeclaredField(fieldName);
|
|
|
+ field.setAccessible(true);
|
|
|
+ return dstClazz.cast(field.get(owner));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|