| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- package com.malk.server.dingtalk.crypto;
- import com.google.common.io.BaseEncoding;
- import org.apache.commons.codec.binary.Base64;
- import javax.crypto.Cipher;
- import javax.crypto.spec.IvParameterSpec;
- import javax.crypto.spec.SecretKeySpec;
- import java.io.ByteArrayOutputStream;
- import java.lang.reflect.Field;
- import java.nio.charset.Charset;
- import java.security.MessageDigest;
- import java.security.Permission;
- import java.security.PermissionCollection;
- import java.security.Security;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.Random;
- /**
- * 钉钉开放平台加解密方法
- * 在ORACLE官方网站下载JCE无限制权限策略文件
- * JDK6的下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
- * JDK7的下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
- * JDK8的下载地址 https://www.oracle.com/java/technologies/javase-jce8-downloads.html
- */
- 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固定长度
- **/
- private static final Integer AES_ENCODE_KEY_LENGTH = 43;
- /**
- * 加密随机字符串字节长度
- **/
- private static final Integer RANDOM_LENGTH = 16;
- /**
- * 构造函数
- *
- * @param token 钉钉开放平台上,开发者设置的token
- * @param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey
- * @param corpId 企业自建应用-事件订阅, 使用appKey
- * 企业自建应用-注册回调地址, 使用corpId
- * 第三方企业应用, 使用suiteKey
- * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
- */
- 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;
- // ppExt: 23.10.26 钉钉新方式以Steam接入, HTTP形式commonsc-codec在升级之后,其内部做了一个validateCharacter校验. 使用 guava 替代
- aesKey = BaseEncoding.base64().decode(encodingAesKey + "=");
- }
- public Map<String, String> getEncryptedMap(String plaintext) throws DingTalkEncryptException {
- return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
- }
- /**
- * 将和钉钉开放平台同步的消息体加密,返回加密Map
- *
- * @param plaintext 传递的消息体明文
- * @param timeStamp 时间戳
- * @param nonce 随机字符串
- * @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);
- }
- // 加密
- 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;
- }
- /**
- * 密文解密
- *
- * @param msgSignature 签名串
- * @param timeStamp 时间戳
- * @param nonce 随机串
- * @param encryptMsg 密文
- * @return 解密后的原文
- * @throws DingTalkEncryptException
- */
- public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
- throws DingTalkEncryptException {
- //校验签名
- String signature = getSignature(token, timeStamp, nonce, encryptMsg);
- if (!signature.equals(msgSignature)) {
- throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
- }
- // 解密
- String result = decrypt(encryptMsg);
- return result;
- }
- /*
- * 对明文加密.
- * @param text 需要加密的明文
- * @return 加密后base64编码的字符串
- */
- 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);
- }
- }
- /*
- * 对密文进行解密.
- * @param text 需要解密的密文
- * @return 解密得到的明文
- */
- private String decrypt(String text) throws DingTalkEncryptException {
- byte[] originalArr;
- try {
- // 设置解密模式为AES的CBC模式
- 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);
- // 使用BASE64对密文进行解码
- byte[] encrypted = Base64.decodeBase64(text);
- // 解密
- originalArr = cipher.doFinal(encrypted);
- } catch (Exception e) {
- throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
- }
- String plainText;
- String fromCorpid;
- try {
- // 去除补位字符
- byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
- // 分离16位随机字符串,网络字节序和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);
- }
- // corpid不相同的情况
- if (!fromCorpid.equals(corpId)) {
- throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
- }
- return plainText;
- }
- /**
- * 数字签名
- *
- * @param token isv token
- * @param timestamp 时间戳
- * @param nonce 随机串
- * @param encrypt 加密文本
- * @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 final long serialVersionUID = 4139585721260116198L;
- private static Map<Integer, String> msgMap = new HashMap();
- private Integer code;
- static {
- msgMap.put(0, "成功");
- msgMap.put(900001, "加密明文文本非法");
- msgMap.put(900002, "加密时间戳参数非法");
- msgMap.put(900003, "加密随机字符串参数非法");
- msgMap.put(900005, "签名不匹配");
- msgMap.put(900006, "签名计算失败");
- msgMap.put(900004, "不合法的aes key");
- msgMap.put(900007, "计算加密文字错误");
- msgMap.put(900008, "计算解密文字错误");
- msgMap.put(900009, "计算解密文字长度不匹配");
- msgMap.put(900010, "计算解密文字corpid不匹配");
- }
- 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));
- }
- }
|