|
|
@@ -0,0 +1,792 @@
|
|
|
+package net.jd.classwinter.executor;
|
|
|
+
|
|
|
+import javassist.CannotCompileException;
|
|
|
+import javassist.ClassPool;
|
|
|
+import javassist.NotFoundException;
|
|
|
+import net.jd.classwinter.Reverses;
|
|
|
+import net.jd.classwinter.author.JustryDeng;
|
|
|
+import net.jd.classwinter.exception.ClassWinterException;
|
|
|
+import net.jd.classwinter.util.Cache;
|
|
|
+import net.jd.classwinter.util.Constant;
|
|
|
+import net.jd.classwinter.util.EncryptUtil;
|
|
|
+import net.jd.classwinter.util.IOUtil;
|
|
|
+import net.jd.classwinter.util.JarUtil;
|
|
|
+import net.jd.classwinter.util.JavassistUtil;
|
|
|
+import net.jd.classwinter.util.Logger;
|
|
|
+import net.jd.classwinter.util.Pair;
|
|
|
+import net.jd.classwinter.util.PathUtil;
|
|
|
+import net.jd.classwinter.util.StrUtil;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.SecureRandom;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Base64;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * java class加密
|
|
|
+ *
|
|
|
+ * @author {@link JustryDeng}
|
|
|
+ * @since 2021/4/28 22:07:51
|
|
|
+ */
|
|
|
+public class EncryptExecutor {
|
|
|
+
|
|
|
+ /** 本项目(class-winter)需要打包的代码 */
|
|
|
+ public static final Set<String> CLASS_WINTER_FILES = new HashSet<>();
|
|
|
+
|
|
|
+ static {
|
|
|
+ CLASS_WINTER_FILES.add("Cache.class");
|
|
|
+ CLASS_WINTER_FILES.add("JustryDeng.class");
|
|
|
+ CLASS_WINTER_FILES.add("ClassWinterException.class");
|
|
|
+ CLASS_WINTER_FILES.add("DecryptExecutor.class");
|
|
|
+ CLASS_WINTER_FILES.add("Constant.class");
|
|
|
+ CLASS_WINTER_FILES.add("EncryptUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("ExceptionUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("IOUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("JarUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("JavaagentCmdArgs.class");
|
|
|
+ CLASS_WINTER_FILES.add("JavassistUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("Logger.class");
|
|
|
+ CLASS_WINTER_FILES.add("Pair.class");
|
|
|
+ CLASS_WINTER_FILES.add("PathUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("StrUtil.class");
|
|
|
+ CLASS_WINTER_FILES.add("Reverses.class");
|
|
|
+ CLASS_WINTER_FILES.add("Reverses$1.class");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 待加密的的jar/war包路径(如" /tmp/abc.jar) */
|
|
|
+ private final String originJarOrWar;
|
|
|
+
|
|
|
+ /** true-originJarOrWar是jar包;false-originJarOrWar是war包 */
|
|
|
+ private final boolean originIsJar;
|
|
|
+
|
|
|
+ /** 加密后生成的jar(or war)包的名称(如:abc-encrypted) */
|
|
|
+ private final String finalName;
|
|
|
+
|
|
|
+ /** 解压jar/war的根目录*/
|
|
|
+ private final String targetRootDir;
|
|
|
+
|
|
|
+ /** 解压的jar/war包里,lib的根目录*/
|
|
|
+ private final String targetLibDir;
|
|
|
+
|
|
|
+ /** 解压的jar/war包里,class文件的根目录*/
|
|
|
+ private final String targetClassesDir;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加密密码(若为空,则系统会自动生成)
|
|
|
+ */
|
|
|
+ private final String password;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 要加密的类的全类名前缀(也可以精确匹配)
|
|
|
+ */
|
|
|
+ private final Set<String> includePrefixSet;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 要加密的lib包(若不指定,则加密筛选范围默认仅从项目代码本身进行筛选加密)。
|
|
|
+ * <p>
|
|
|
+ * 提示: 不论项目里面的依赖的传递关系是怎样的,这里只需要指定需要加密的jar包全称即可。
|
|
|
+ * 说明:假设我们项目的pom依赖关系为: myProject -> a -> b -> c
|
|
|
+ * 即: 我们的项目myProject直接依赖的是a,我们在pom中主动指定依赖的也是a,没有主动指定需要依赖b和c,但是由于maven依赖传递的原因,
|
|
|
+ * 我们的项目实际上也依赖了b和c, 当我们把项目myProject打成jar包时,也会把a、b、c都打进jar包里。
|
|
|
+ * 并且,将 a -> b -> c这样的层级关系平铺开了, 为
|
|
|
+ * │
|
|
|
+ * ├─BOOT-INF
|
|
|
+ * │ │
|
|
|
+ * │ └─lib
|
|
|
+ * │ └─a.jar
|
|
|
+ * │ └─b.jar
|
|
|
+ * │ └─c.jar
|
|
|
+ * 这里,如果需要对a、b、c进行加密的话,只需要使includeLibSet值为["a.jar","b.jar","c.jar"]即可
|
|
|
+ */
|
|
|
+ private final Set<String> includeLibSet;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 不需要加密的类的全类名前缀(也可以精确匹配)
|
|
|
+ * <p>
|
|
|
+ * includePrefixSet匹配的类中再排除excludePrefixSet匹配的类,即为最终会被加密的类
|
|
|
+ */
|
|
|
+ private final Set<String> excludePrefixSet;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 已经被class-winter加密过了的lib的信息
|
|
|
+ * <p>
|
|
|
+ * 每个pair对象的属性介绍:
|
|
|
+ * pair-左: lib包名,如: abc.jar
|
|
|
+ * pair-右: 加密该lib时的密码
|
|
|
+ * <p>
|
|
|
+ * 想一想这种case:
|
|
|
+ * 第三方给你提供了一个被class-winter加密过的jar包,你把这个jar包依赖进你的项目your-project中,
|
|
|
+ * 你想正常的运行起来项目,那么你就需要用到这个属性了。
|
|
|
+ */
|
|
|
+ private final Set<Pair<String, String>> alreadyProtectedLibSet;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加密时,额外往加密上下文中添加的jar文件(或者jar文件所在的目录)。
|
|
|
+ * <p>
|
|
|
+ * 在进行加密时,某些类可能会因为缺失必要的依赖类而导致加密失败。就可以通过指定此字段的值(值应为某个jar或者某个目录的全路径),
|
|
|
+ * 这样一来,class-winter混淆目标jar时,除了加载要加载的jar/war内部的所有jar外,还会额外加载这里指定的jar(或者这里指定的目录下的所有子孙jar),
|
|
|
+ * 进而保证在加密时,不会因为确实依赖类而加密失败。
|
|
|
+ * <p>
|
|
|
+ * 注: 某些项目在打jar包时,是没有把其依赖的lib打进jar包中的,所以会出现[某些类可能会因为缺失必要的依赖类而导致加密失败]的情况。
|
|
|
+ * <p>
|
|
|
+ * 示例说明:
|
|
|
+ * 假设加密时日志报warn说
|
|
|
+ * 2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for className [com.aspire.ssm.handler.impl.DefaultAesPreHandlerImpl], Cannot find 'org.apache.ibatis.executor.parameter.ParameterHandler'
|
|
|
+ * 2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for className [com.aspire.ssm.interceptor.CustomizeMybatisPlugin], Cannot find 'org.apache.ibatis.plugin.Invocation'
|
|
|
+ * 这说明了
|
|
|
+ * DefaultAesPreHandlerImpl类中使用到了org.apache.ibatis.executor.parameter.ParameterHandler类,
|
|
|
+ * DCustomizeMybatisPlugin类中使用到了org.apache.ibatis.plugin.Invocation类,
|
|
|
+ * 进一步说明了
|
|
|
+ * 要加密的jar包没有把mybatis的lib包打进去。
|
|
|
+ * 这时,我们就可以通过指定此字段的值为mybatisjar包的全路径名(如/tmp/lib/mybatis-3.5.1.jar)来解决问题。
|
|
|
+ */
|
|
|
+ private final String supportFile;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 要加密的lib jar文件的绝对路径 & 其对应的加密临时目录
|
|
|
+ * 如: /xyz/abc.jar 与 /xyz/abc__temp__
|
|
|
+ */
|
|
|
+ private final Map<String, String> libJarAndTmpDirMap = new HashMap<>(8);
|
|
|
+
|
|
|
+ private EncryptExecutor(String originJarOrWar, boolean originIsJar, String finalName, String targetRootDir,
|
|
|
+ String targetLibDir, String targetClassesDir, String password, Set<String> includePrefixSet,
|
|
|
+ Set<String> excludePrefixSet, Set<String> includeLibSet, Set<Pair<String, String>> alreadyProtectedLibSet, String supportFile) {
|
|
|
+ this.originJarOrWar = originJarOrWar;
|
|
|
+ this.originIsJar = originIsJar;
|
|
|
+ this.finalName = finalName;
|
|
|
+ this.targetRootDir = targetRootDir;
|
|
|
+ this.targetLibDir = targetLibDir;
|
|
|
+ this.targetClassesDir = targetClassesDir;
|
|
|
+ this.password = password;
|
|
|
+ this.includePrefixSet = includePrefixSet;
|
|
|
+ this.excludePrefixSet = excludePrefixSet;
|
|
|
+ this.includeLibSet = includeLibSet;
|
|
|
+ this.alreadyProtectedLibSet = alreadyProtectedLibSet;
|
|
|
+ this.supportFile = supportFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * do
|
|
|
+ *
|
|
|
+ * @return 加密处理后生成的jar/war文件的位置
|
|
|
+ */
|
|
|
+ public String process() {
|
|
|
+ // step0. 解压jar到jarRootDir下
|
|
|
+ JarUtil.unJarWar(originJarOrWar, targetRootDir);
|
|
|
+ // step1. jar中的某些lib可能也需要加密,这里先(将那些需要加密的lib)进行解压,(解压到对应的临时目录,以便后面进行加密处理)
|
|
|
+ unLibJar();
|
|
|
+ // step2. 找到需要加密混淆的class文件
|
|
|
+ File targetRootDirFile = new File(targetRootDir);
|
|
|
+ List<File> allFiles = IOUtil.listSubFile(targetRootDirFile, 1);
|
|
|
+ List<File> allNeedEncryptedClassFileList = filterClasses(allFiles);
|
|
|
+ // step3.1. 根据原.class文件,生成加密后的.class文件,并存至Constant.ENCRYPTED_CLASSES_SAVE_DIR
|
|
|
+ List<String> allAlreadyEncryptedClassFileList = encryptClasses(allNeedEncryptedClassFileList, new File(targetRootDir,
|
|
|
+ Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR));
|
|
|
+ // step3.2. 生成记录已加密class的全类名的清单文件
|
|
|
+ generateChecklistFile(allAlreadyEncryptedClassFileList);
|
|
|
+
|
|
|
+ // step4. 记录印章(在解密时,可通过 checklist + 印章 判断一个class是否是被class-winter加密过)
|
|
|
+ generateSealFile();
|
|
|
+
|
|
|
+ // step5. 混淆原.class文件(即:清空方法体)
|
|
|
+ clearClassMethod(allNeedEncryptedClassFileList);
|
|
|
+
|
|
|
+ // step6. 添加class-winter的用于javaagent解密的代码
|
|
|
+ addClassWinterAgent();
|
|
|
+
|
|
|
+ // step7. 汇总那些在本次加密前本身就已经是被class-winter混淆的lib的相关信息到当前项目中
|
|
|
+ collectAlreadyProtectedLibInfo();
|
|
|
+
|
|
|
+ // step8. 与step1项对应,将临时目录还原为原来的lib(此时得到的lib是加密后的)
|
|
|
+ doLibJar();
|
|
|
+
|
|
|
+ // step9. 压缩targetRootDir目录为绝对文件路径名为generatedJarWar的jar(or war)包
|
|
|
+ String generatedJarWar = generateJarWarPath(this.originIsJar);
|
|
|
+ JarUtil.doJarWar(targetRootDir, generatedJarWar);
|
|
|
+
|
|
|
+ // step10. 删除临时目录
|
|
|
+ IOUtil.delete(targetRootDirFile);
|
|
|
+
|
|
|
+ return generatedJarWar;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 采集本来就已经被class-winter混淆了的lib的信息
|
|
|
+ */
|
|
|
+ private void collectAlreadyProtectedLibInfo() {
|
|
|
+ if (alreadyProtectedLibSet == null ||alreadyProtectedLibSet.size() == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * k - 创建出来的jar包对应的文件夹 如: META-INF/winter/abc-1.0.0_jar/
|
|
|
+ * v - jar包的checklist信息, 如: abc-1.0.0.jar对应的libChecklist值
|
|
|
+ */
|
|
|
+ Map<String, String> allChecklistInfoMap = new HashMap<>(8);
|
|
|
+ alreadyProtectedLibSet.forEach(pair -> {
|
|
|
+ String libJarName = pair.getLeft();
|
|
|
+ String password = pair.getRight();
|
|
|
+ File lib = new File(targetLibDir, libJarName);
|
|
|
+ if (!lib.exists()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 形如: META-INF/winter/abc-1.0.0_jar/
|
|
|
+ String dirRelativePathForLib = Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + libJarName.substring(0, libJarName.length() - 4) + "_jar" + Constant.LINUX_FILE_SEPARATOR;
|
|
|
+ String dirAbsolutePathForLib = this.targetRootDir + File.separator + Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + libJarName.substring(0, libJarName.length() - 4) + "_jar" + File.separator;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * my-project依赖的lib包本身可以是被class-winter混淆后的,但是这个lib包依赖的lib包不能是被class-winter混淆后
|
|
|
+ * <p>
|
|
|
+ * 注: 也就是说对lib的处理,只往下一层,不继续往下了。
|
|
|
+ * 注: 所以如果符合预期的话,lib中的对应位置是没有Constant.CHECKLIST_OF_ALL_LIBS文件的,这时
|
|
|
+ * IOUtil.readFileFromWorkbenchRoot(lib, Constant.CHECKLIST_OF_ALL_LIBS)会返回null
|
|
|
+ */
|
|
|
+ byte[] checklistOfAllLibsByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.CHECKLIST_OF_ALL_LIBS);
|
|
|
+ if (checklistOfAllLibsByte != null) {
|
|
|
+ throw new ClassWinterException(libJarName+"'s lib cannot be protected by class-winter.");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清单文件
|
|
|
+ byte[] checklistByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE);
|
|
|
+ if (checklistByte == null) {
|
|
|
+ // 清单文件不存在,说明这个lib中没有任何类被混淆
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ String libChecklist = new String(checklistByte, StandardCharsets.UTF_8);
|
|
|
+ IOUtil.writeContentToFile(libChecklist, new File(dirAbsolutePathForLib, Constant.CHECKLIST_FILE_SIMPLE_NAME));
|
|
|
+
|
|
|
+ // 所有lib的checklist汇总至checklistOfAllLibs
|
|
|
+ allChecklistInfoMap.put(dirRelativePathForLib, libChecklist);
|
|
|
+
|
|
|
+ // 将清单文件指向的加密后的class文件,从lib中挪动至当前项目的dirForLib下
|
|
|
+ String[] libClassesArr = libChecklist.split(",");
|
|
|
+ byte[] libClazzByte;
|
|
|
+ for (String nonSuffixClassLongName : libClassesArr) {
|
|
|
+ libClazzByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + nonSuffixClassLongName);
|
|
|
+ IOUtil.toFile(libClazzByte, new File(dirAbsolutePathForLib, nonSuffixClassLongName), true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 印章
|
|
|
+ byte[] libSealByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.SEAL_FILE);
|
|
|
+ if (libSealByte == null) {
|
|
|
+ // lib中没有印章文件
|
|
|
+ throw new ClassWinterException("Cannot find seal in lib [" + libJarName + "]. This lib is not "
|
|
|
+ + "protected by class-winter.");
|
|
|
+ } else {
|
|
|
+ IOUtil.writeContentToFile(new String(libSealByte, StandardCharsets.UTF_8), new File(dirAbsolutePathForLib,
|
|
|
+ Constant.SEAL_FILE_SIMPLE_NAME));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 密码
|
|
|
+ boolean libUserIfInputPwd = Boolean.parseBoolean(new String(IOUtil.readFileFromWorkbenchRoot(lib, Constant.USER_IF_INPUT_PWD)));
|
|
|
+ Logger.debug(EncryptExecutor.class, "libUserIfInputPwd info of lib [" + libJarName + "] -> " + libUserIfInputPwd);
|
|
|
+ if (libUserIfInputPwd && StrUtil.isBlank(password)) {
|
|
|
+ // 加密lib时,用户主动指定了密码,那么这里password就不应该为空
|
|
|
+ throw new ClassWinterException("Password is required to decrypt lib [" + libJarName + "]");
|
|
|
+ }
|
|
|
+ if (StrUtil.isBlank(password)) {
|
|
|
+ byte[] libEncryptedPwdByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.PWD_WINTER);
|
|
|
+ if (libEncryptedPwdByte == null) {
|
|
|
+ // lib中没有密码文件,请指定此lib的密码
|
|
|
+ throw new ClassWinterException("Cannot find pwd from lib [" + libJarName + "]");
|
|
|
+ } else {
|
|
|
+ // 因为写之前是加密写进去的,那么这里解密一下得到明文
|
|
|
+ // 存时,是加密存进去的; 这里读取时,(用lib的印章)解密一下
|
|
|
+ password = EncryptUtil.decrypt(new String(libEncryptedPwdByte, StandardCharsets.UTF_8),
|
|
|
+ new String(libSealByte, StandardCharsets.UTF_8).toCharArray());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 存时,加密存进去
|
|
|
+ String encryptedPassword =EncryptUtil.encrypt(password, Cache.sealCache.toCharArray());
|
|
|
+ IOUtil.writeContentToFile(encryptedPassword, new File(dirAbsolutePathForLib, Constant.PWD_WINTER_SIMPLE_NAME));
|
|
|
+ // 由于这里已经处理了,将所有lib的密码(不论当初加密lib时密码是用户主动输入的还是系统自动产生的),都当做系统自动产生的进行录入,所以这里写死为false
|
|
|
+ IOUtil.writeContentToFile("false", new File(dirAbsolutePathForLib, Constant.USER_IF_INPUT_PWD_SIMPLE_NAME));
|
|
|
+
|
|
|
+ });
|
|
|
+
|
|
|
+ // 校验 - 当多个lib之间,发生加密类冲突时,快速失败
|
|
|
+ Map<String, String> tmpMap = new HashMap<>(128);
|
|
|
+ StringBuilder checklistOfAllLibs = new StringBuilder(64);
|
|
|
+ allChecklistInfoMap.forEach((k, v) -> {
|
|
|
+ if (StrUtil.isBlank(v)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 当多个lib之间,发生加密类冲突时,快速失败
|
|
|
+ Arrays.stream(v.split(",")).filter(item -> !StrUtil.isBlank(item)).forEach(nonSuffixClassLongName -> {
|
|
|
+ String libDir = tmpMap.get(nonSuffixClassLongName);
|
|
|
+ if (!StrUtil.isBlank(libDir)) {
|
|
|
+ // 当多个lib之间,发生加密类冲突时,快速失败
|
|
|
+ throw new ClassWinterException(String.format("protected class[%s] conflict. one lib is [%s], another lib is [%s]",
|
|
|
+ nonSuffixClassLongName, libDir, k));
|
|
|
+ }
|
|
|
+ tmpMap.put(nonSuffixClassLongName, k);
|
|
|
+ });
|
|
|
+ checklistOfAllLibs.append(k).append("=").append(v).append(Constant.LINE_SEPARATOR);
|
|
|
+ });
|
|
|
+ if (checklistOfAllLibs.length() > 0) {
|
|
|
+ IOUtil.writeContentToFile(checklistOfAllLibs.toString(), new File(this.targetRootDir, Constant.CHECKLIST_OF_ALL_LIBS));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解压jar包中需要加密的lib包
|
|
|
+ */
|
|
|
+ private void unLibJar() {
|
|
|
+ if (includeLibSet == null || includeLibSet.size() == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 罗列出targetRootDir下的所有.jar文件
|
|
|
+ List<File> allJarFiles = IOUtil.listFileOnly(new File(targetRootDir), Constant.JAR_SUFFIX);
|
|
|
+ // 筛选出需要加密的lib包
|
|
|
+ List<File> neededEncryptedLibs = allJarFiles.stream().filter(jarFile -> includeLibSet.contains(jarFile.getName())).collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 依次解密
|
|
|
+ neededEncryptedLibs.forEach(jar -> {
|
|
|
+ // 假设: jarAbsolutePath为 /xyz/abc.jar
|
|
|
+ String jarAbsolutePath = jar.getAbsolutePath();
|
|
|
+ // 那么: temDir就为 /xyz/abc__temp__
|
|
|
+ String temDir = jarAbsolutePath.substring(0, jarAbsolutePath.length() - Constant.JAR_SUFFIX.length()) + Constant.TMP_DIR_SUFFIX;
|
|
|
+ JarUtil.unJarWar(jarAbsolutePath, temDir);
|
|
|
+ libJarAndTmpDirMap.put(jarAbsolutePath, temDir);
|
|
|
+ // 删除原来的lib包
|
|
|
+ IOUtil.delete(new File(jarAbsolutePath));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将unLibJar()中解压出来的临时目录打包成jar包
|
|
|
+ */
|
|
|
+ private void doLibJar() {
|
|
|
+ if (libJarAndTmpDirMap.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ libJarAndTmpDirMap.forEach((jarFilePath, tmpDirPath) -> {
|
|
|
+ JarUtil.doJarWar(tmpDirPath, jarFilePath);
|
|
|
+ // 删除临时目录
|
|
|
+ IOUtil.delete(new File(tmpDirPath));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成记录已加密class的全类名的清单文件
|
|
|
+ *
|
|
|
+ * @param classLongNameList
|
|
|
+ * 已加密class的全类名集合
|
|
|
+ */
|
|
|
+ private void generateChecklistFile(List<String> classLongNameList) {
|
|
|
+ if (classLongNameList == null || classLongNameList.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String content = String.join(Constant.COMMA, classLongNameList);
|
|
|
+ // 将加密后的内容存储到指定位置下
|
|
|
+ IOUtil.writeContentToFile(content, new File(targetRootDir, Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成记录本次加密印章的文件
|
|
|
+ */
|
|
|
+ private void generateSealFile() {
|
|
|
+ // 将加密后的内容存储到指定位置下
|
|
|
+ IOUtil.writeContentToFile(Constant.SEAL, new File(targetRootDir, Constant.SEAL_FILE));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加class-winter的用于javaagent解密的代码
|
|
|
+ */
|
|
|
+ private void addClassWinterAgent() {
|
|
|
+ String classWinterProjectRootDir = PathUtil.getProjectRootDir(this.getClass());
|
|
|
+ File classWinterProjectRootDirFile = new File(classWinterProjectRootDir);
|
|
|
+ int classWinterProjectRootDirLength = classWinterProjectRootDir.length();
|
|
|
+ // 当class-winter未打成jar包时
|
|
|
+ if (classWinterProjectRootDir.endsWith(Constant.CLASSES_DIR)) {
|
|
|
+ List<File> allFileList = IOUtil.listSubFile(new File(classWinterProjectRootDir), 0);
|
|
|
+ allFileList.forEach(file -> {
|
|
|
+ String classLongNamePath = file.getAbsolutePath().substring(classWinterProjectRootDirLength);
|
|
|
+ File destFile = new File(this.originIsJar ? this.targetRootDir : this.targetClassesDir, classLongNamePath);
|
|
|
+ if (file.isFile() && CLASS_WINTER_FILES.contains(file.getName())) {
|
|
|
+ byte[] bytes = IOUtil.toBytes(file);
|
|
|
+ IOUtil.toFile(bytes, destFile, true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 当class-winter打包成jar包时
|
|
|
+ if (classWinterProjectRootDir.endsWith(Constant.JAR_SUFFIX)) {
|
|
|
+ if (this.originIsJar) {
|
|
|
+ JarUtil.unJarWar(classWinterProjectRootDir, targetRootDir, false, CLASS_WINTER_FILES);
|
|
|
+ } else {
|
|
|
+ byte[] bytes = IOUtil.toBytes(classWinterProjectRootDirFile);
|
|
|
+ IOUtil.toFile(bytes, new File(this.targetLibDir, classWinterProjectRootDirFile.getName()), true);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new ClassWinterException("Execute method addClassWinterAgent() fail. Cannot parse classWinterProjectRootDir [" + classWinterProjectRootDir + "].");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 把javaagent信息加入到MANIFEST.MF
|
|
|
+ File manifest = new File(this.targetRootDir, "META-INF/MANIFEST.MF");
|
|
|
+ String preMain = "Premain-Class: " + Reverses.class.getName();
|
|
|
+ String[] origin = {};
|
|
|
+ if (manifest.exists()) {
|
|
|
+ origin = IOUtil.readContentFromFile(manifest).split(System.lineSeparator());
|
|
|
+ }
|
|
|
+ // 在原来的启动函数行后面,插入pre-main指令
|
|
|
+ String str = StrUtil.insertStrAfterLine(origin, preMain, "Main-Class:");
|
|
|
+ str = str + System.lineSeparator();
|
|
|
+ IOUtil.writeContentToFile(str, manifest);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取生成的jar(or war)包的全路径名(如:/tmp/abc.jar)
|
|
|
+ *
|
|
|
+ * @param originIsJar
|
|
|
+ * true-生成jar路径; false-生成war路径
|
|
|
+ *
|
|
|
+ * @return 生成的jar(or war)包的全路径名(如:/tmp/abc.jar)
|
|
|
+ */
|
|
|
+ private String generateJarWarPath(boolean originIsJar) {
|
|
|
+ int endIndex = originJarOrWar.lastIndexOf(Constant.LINUX_FILE_SEPARATOR);
|
|
|
+ String jarRootParentDir;
|
|
|
+ if (endIndex > 0) {
|
|
|
+ jarRootParentDir = originJarOrWar.substring(0, endIndex + Constant.LINUX_FILE_SEPARATOR.length());
|
|
|
+ } else {
|
|
|
+ jarRootParentDir = Constant.LINUX_FILE_SEPARATOR;
|
|
|
+ }
|
|
|
+ return jarRootParentDir + finalName + (originIsJar ? Constant.JAR_SUFFIX : Constant.WAR_SUFFIX);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清空class文件的方法体,并保留参数信息
|
|
|
+ *
|
|
|
+ * @param classFiles
|
|
|
+ * 需要清空方法体的class文件
|
|
|
+ */
|
|
|
+ private void clearClassMethod(List<File> classFiles) {
|
|
|
+ // step0. 初始化javassist
|
|
|
+ ClassPool pool = ClassPool.getDefault();
|
|
|
+ // step1. 把所有可能涉及到的jar、.class添加进加载路径
|
|
|
+ try {
|
|
|
+ JavassistUtil.loadJar(pool, this.targetRootDir);
|
|
|
+ JavassistUtil.loadClass(pool, this.targetRootDir);
|
|
|
+ if (!StrUtil.isBlank(this.supportFile)) {
|
|
|
+ JavassistUtil.loadJar(pool, this.supportFile);
|
|
|
+ }
|
|
|
+ } catch (NotFoundException e) {
|
|
|
+ throw new ClassWinterException(e);
|
|
|
+ }
|
|
|
+ // step2. 修改class方法体,并保存文件
|
|
|
+ classFiles.forEach(classFile -> {
|
|
|
+ String className = JavassistUtil.resolveClassName(classFile.getAbsolutePath(), true);
|
|
|
+ try {
|
|
|
+ byte[] bts = JavassistUtil.clearMethodBody(pool, className, true);
|
|
|
+ IOUtil.toFile(bts, classFile, true);
|
|
|
+ } catch (NotFoundException e) {
|
|
|
+ // ignore
|
|
|
+ Logger.warn(EncryptExecutor.class, "Ignore clear-method-body for className [" + className + "], Cannot find '" + e.getMessage() + "'");
|
|
|
+ } catch (CannotCompileException e) {
|
|
|
+ throw new ClassWinterException(e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ pool.clearImportedPackages();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加密class文件,并将加密后的class文件放在savaDir里
|
|
|
+ *
|
|
|
+ * @param classFiles
|
|
|
+ * 需要加密的class文件
|
|
|
+ * @param savaDir
|
|
|
+ * 加密后的class文件的存储目录
|
|
|
+ * @return 已经加密了的类的全类名(如: com.aaa.bbb.Abc)
|
|
|
+ */
|
|
|
+ private List<String> encryptClasses(List<File> classFiles, File savaDir) {
|
|
|
+ // 保证目录存在
|
|
|
+ JarUtil.guarantyDirExist(savaDir);
|
|
|
+ classFiles.stream().map(File::getName).forEach(name -> {
|
|
|
+ if (!name.endsWith(Constant.CLASS_SUFFIX)) {
|
|
|
+ throw new ClassWinterException("classFiles must all be class file. file [" + name + "] is illegal.");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 加密后存储的位置
|
|
|
+ List<String> encryptClasses = new ArrayList<>();
|
|
|
+ // 加密,并将得到的加密后的class文件另存到savaDir目录中
|
|
|
+ classFiles.forEach(classFile -> {
|
|
|
+ String className = JavassistUtil.resolveClassName(classFile.getAbsolutePath(), true);
|
|
|
+ byte[] bytes = IOUtil.toBytes(classFile);
|
|
|
+ bytes = EncryptUtil.encrypt(bytes, obtainPassword());
|
|
|
+ // 将加密后的内容存储到指定位置下
|
|
|
+ IOUtil.toFile(bytes, new File(savaDir, className), true);
|
|
|
+ encryptClasses.add(className);
|
|
|
+ });
|
|
|
+ return encryptClasses;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取密码
|
|
|
+ *
|
|
|
+ * @return 密码
|
|
|
+ */
|
|
|
+ private char[] obtainPassword() {
|
|
|
+ if (Cache.passwordCache != null) {
|
|
|
+ // 从缓存读取
|
|
|
+ return Cache.passwordCache;
|
|
|
+ }
|
|
|
+ boolean passwordIsBlank = StrUtil.isBlank(this.password);
|
|
|
+ if (passwordIsBlank) {
|
|
|
+ char[] generatedPwd = EncryptUtil.generateCharArr(new SecureRandom().nextInt(500) + 100);
|
|
|
+ // 将自动生成的密码记录到文件中
|
|
|
+ // 存时,加密存进去
|
|
|
+ String encryptedGeneratedPwd = EncryptUtil.encrypt(new String(generatedPwd), Constant.SEAL.toCharArray());
|
|
|
+ IOUtil.writeContentToFile(encryptedGeneratedPwd, new File(targetRootDir, Constant.PWD_WINTER));
|
|
|
+ // 放入缓存
|
|
|
+ Cache.passwordCache = generatedPwd;
|
|
|
+ } else {
|
|
|
+ Cache.passwordCache = this.password.toCharArray();
|
|
|
+ }
|
|
|
+ // 记录加密时,用户是否输入了密码
|
|
|
+ IOUtil.writeContentToFile(passwordIsBlank ? "false" : "true", new File(targetRootDir, Constant.USER_IF_INPUT_PWD));
|
|
|
+ return Cache.passwordCache;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 找出所有需要加密的class文件
|
|
|
+ *
|
|
|
+ * @param allFileList
|
|
|
+ * 所有文件
|
|
|
+ * @return 需要加密的class文件
|
|
|
+ */
|
|
|
+ private List<File> filterClasses(List<File> allFileList) {
|
|
|
+ return allFileList.stream()
|
|
|
+ .filter(file -> file.getName().endsWith(Constant.CLASS_SUFFIX))
|
|
|
+ .filter(file -> {
|
|
|
+ // 全类名
|
|
|
+ String classLongName = JavassistUtil.resolveClassName(file.getAbsolutePath(), true);
|
|
|
+ if (excludePrefixSet != null) {
|
|
|
+ for (String excludePrefix : excludePrefixSet) {
|
|
|
+ if (classLongName.startsWith(excludePrefix)) {
|
|
|
+ // 排除
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ if (includePrefixSet != null) {
|
|
|
+ for (String includePrefix : includePrefixSet) {
|
|
|
+ if (classLongName.startsWith(includePrefix)) {
|
|
|
+ // 包含
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return "EncryptExecutor{" +
|
|
|
+ "originJarOrWar='" + originJarOrWar + '\'' +
|
|
|
+ ", originIsJar=" + originIsJar +
|
|
|
+ ", finalName='" + finalName + '\'' +
|
|
|
+ ", targetRootDir='" + targetRootDir + '\'' +
|
|
|
+ ", targetLibDir='" + targetLibDir + '\'' +
|
|
|
+ ", targetClassesDir='" + targetClassesDir + '\'' +
|
|
|
+ // 密码脱敏
|
|
|
+ ", password='" + (StrUtil.isBlank(password) ? null : "******") + '\'' +
|
|
|
+ ", supportFile='" + supportFile + '\'' +
|
|
|
+ ", includePrefixSet=" + includePrefixSet +
|
|
|
+ ", includeLibSet=" + includeLibSet +
|
|
|
+ ", excludePrefixSet=" + excludePrefixSet +
|
|
|
+ ", alreadyProtectedLibSet=" + alreadyProtectedLibSet +
|
|
|
+ '}';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * builder for EncryptExecutor
|
|
|
+ */
|
|
|
+ public static EncryptExecutor.Builder builder() {
|
|
|
+ return new EncryptExecutor.Builder();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Builder {
|
|
|
+
|
|
|
+ private String originJarOrWar;
|
|
|
+
|
|
|
+ private String finalName;
|
|
|
+
|
|
|
+ private String password;
|
|
|
+
|
|
|
+ private String includePrefix;
|
|
|
+
|
|
|
+ private String excludePrefix;
|
|
|
+
|
|
|
+ private String includeLibs;
|
|
|
+
|
|
|
+ private String alreadyProtectedLibs;
|
|
|
+
|
|
|
+ private String supportFile;
|
|
|
+
|
|
|
+ private Boolean debug = false;
|
|
|
+
|
|
|
+ private String tips;
|
|
|
+
|
|
|
+ public Builder originJarOrWar(String originJarOrWar) {
|
|
|
+ this.originJarOrWar = originJarOrWar;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder finalName(String finalName) {
|
|
|
+ this.finalName = finalName;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder password(String password) {
|
|
|
+ this.password = password;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder includePrefix(String includePrefix) {
|
|
|
+ this.includePrefix = includePrefix;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder excludePrefix(String excludePrefix) {
|
|
|
+ this.excludePrefix = excludePrefix;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder includeLibs(String includeLibs) {
|
|
|
+ this.includeLibs = includeLibs;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder alreadyProtectedLibs(String alreadyProtectedLibs) {
|
|
|
+ this.alreadyProtectedLibs = alreadyProtectedLibs;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public Builder supportFile(String supportFile) {
|
|
|
+ this.supportFile = supportFile;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder debug(Boolean debug) {
|
|
|
+ this.debug = debug;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Builder tips(String tips) {
|
|
|
+ this.tips = tips;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public EncryptExecutor build() {
|
|
|
+ // ---- 其他配置
|
|
|
+ // 1. debug模式
|
|
|
+ Logger.ENABLE_DEBUG.set(this.debug != null && this.debug);
|
|
|
+ // 2. 错误启动jar时,System.out输出的提示信息
|
|
|
+ if (!StrUtil.isEmpty(tips)) {
|
|
|
+ JavassistUtil.TIPS = tips;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 参数校验
|
|
|
+ // 1. originJarOrWar不能为空
|
|
|
+ if (StrUtil.isEmpty(originJarOrWar)) {
|
|
|
+ throw new ClassWinterException("originJarOrWar cannot be empty.");
|
|
|
+ }
|
|
|
+ // 2. originJarOrWar必须是jar文件或者是war文件
|
|
|
+ if (!originJarOrWar.endsWith(Constant.JAR_SUFFIX) && !originJarOrWar.endsWith(Constant.WAR_SUFFIX)) {
|
|
|
+ throw new ClassWinterException("originJarOrWar must be Jar or War file.");
|
|
|
+ }
|
|
|
+ // 3. includePrefix不能为空
|
|
|
+ if (StrUtil.isEmpty(includePrefix)) {
|
|
|
+ throw new ClassWinterException("includePrefix cannot be empty.");
|
|
|
+ }
|
|
|
+ // 4. supportFile如果不为空, 那么必须存在且只能为件文夹或者.jar文件
|
|
|
+ if (!StrUtil.isBlank(supportFile)) {
|
|
|
+ File tmpFile = new File(supportFile);
|
|
|
+ if (!tmpFile.exists()) {
|
|
|
+ throw new ClassWinterException("supportFile ["+supportFile+"] non-exist.");
|
|
|
+ }
|
|
|
+ // 文件的话,必须是.jar文件
|
|
|
+ if (tmpFile.isFile() && !supportFile.endsWith(Constant.JAR_SUFFIX)) {
|
|
|
+ throw new ClassWinterException("supportFile must be dir or a jar file.");
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 构建EncryptExecutor对象
|
|
|
+ File originFile = new File(originJarOrWar);
|
|
|
+ if (!originFile.exists()) {
|
|
|
+ throw new ClassWinterException("cannot find file [" + originJarOrWar + "].");
|
|
|
+ }
|
|
|
+ // 文件路径分隔符统一为 /
|
|
|
+ originJarOrWar = originFile.getAbsolutePath().replace(File.separator, Constant.LINUX_FILE_SEPARATOR);
|
|
|
+
|
|
|
+ // 判断originJarOrWar代表的文件是jar还是war
|
|
|
+ boolean originIsJar = JarUtil.isJarOrWar(originJarOrWar);
|
|
|
+
|
|
|
+ if (StrUtil.isEmpty(this.finalName)) {
|
|
|
+ String originFileName = originFile.getName();
|
|
|
+ int idx = originFileName.lastIndexOf(".");
|
|
|
+ this.finalName(originFileName.substring(0, idx) + "-encrypted");
|
|
|
+ }
|
|
|
+ // 无论是.war还是.jar,长度都是4
|
|
|
+ String targetRootDir = originJarOrWar.substring(0, originJarOrWar.length() - 4) + Constant.TMP_DIR_SUFFIX;
|
|
|
+ String targetLibDir = String.join(File.separator, targetRootDir, originIsJar ? Constant.BOOT_INF :
|
|
|
+ Constant.WEB_INF, Constant.LIB);
|
|
|
+ String targetClassesDir = String.join(File.separator, targetRootDir, originIsJar ? Constant.BOOT_INF :
|
|
|
+ Constant.WEB_INF, Constant.CLASSES);
|
|
|
+
|
|
|
+ Set<String> includePrefixSet = StrUtil.strToSet(includePrefix);
|
|
|
+ Set<String> excludePrefixSet = StrUtil.strToSet(excludePrefix);
|
|
|
+ Set<String> includeLibSet = StrUtil.strToSet(includeLibs);
|
|
|
+ Set<Pair<String, String>> alreadyProtectedLibSet = parseAlreadyProtectedLibs();
|
|
|
+
|
|
|
+ // 如果这次需要加密的lib本身就已经是被加密了的,那么这次不再对其进行加密
|
|
|
+ alreadyProtectedLibSet.stream().map(Pair::getLeft).forEach(lib -> {
|
|
|
+ if (includeLibSet.contains(lib)) {
|
|
|
+ Logger.warn(EncryptExecutor.class, "Ignore includeLibs item [" + lib + "], because this item already be protected by class-winter.");
|
|
|
+ includeLibSet.remove(lib);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ for (String jarFile : includeLibSet) {
|
|
|
+ // jarFile 形如 abc-1.0.0.jar
|
|
|
+ if (jarFile.endsWith(Constant.JAR_SUFFIX)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ throw new ClassWinterException("includeLibs format must be shaped like xxx1.jar[,xxx2.jar,xxx3.jar]");
|
|
|
+ }
|
|
|
+ return new EncryptExecutor(this.originJarOrWar, originIsJar, this.finalName, targetRootDir, targetLibDir, targetClassesDir,
|
|
|
+ password, includePrefixSet, excludePrefixSet, includeLibSet, alreadyProtectedLibSet, this.supportFile);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析alreadyProtectedLibs信息为Set<Pair<String, String>>
|
|
|
+ */
|
|
|
+ private Set<Pair<String, String>> parseAlreadyProtectedLibs() {
|
|
|
+ Set<Pair<String, String>> alreadyProtectedLibSet = new HashSet<>();
|
|
|
+ Set<String> alreadyProtectedLibInfoSet = StrUtil.strToSet(alreadyProtectedLibs);
|
|
|
+ for (String info : alreadyProtectedLibInfoSet) {
|
|
|
+ int idx = info.indexOf(":");
|
|
|
+ if (idx >= 0) {
|
|
|
+ alreadyProtectedLibSet.add(Pair.of(info.substring(0, idx), info.substring(idx + 1)));
|
|
|
+ } else {
|
|
|
+ alreadyProtectedLibSet.add(Pair.of(info, null));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return alreadyProtectedLibSet;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|