赞
踩
目录
由于业务需要,需要将提供给客户的可执行 Jar 包源码加密处理;基本要求:不能改变原有的逻辑和代码结构;团队研发人员无感知。最终实现过程可以概括为:
1、使用Maven工具,执行打包命令,生成加密 jar 包;
2、java -jar 命令直接运行加密后的 jar 包。
本文章参考了 Xjar 项目的加密方式,结合自定义 Maven 插件和JVMTI,最终实现了上述过程。
1、SpringBoot 项目使用 Maven 工具自动构建可执行 jar 包;
2、(主要过程)实现加密 jar 包的过程本质上是将 Maven 构建的可执行 jar 包进一步封装,对 jar 包中的 class 文件进行加密处理;
Tip: 具体的加密算法根据不同需求场景自由定义
3、C/C++ 创建动态链接库 encrypt.dll(window平台,linux平台为 encrypt.so),通过以下命令参数匹配动态链接库,并实现 class 文件的解密运行过程
java -agentlib:encrypt -jar your-executeable-jar-file.jar
windows
gcc -shared encrypt.c -o encrypt.dll -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/win32"
linux
gcc -shared encrypt.c -o encrypt.so -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/linux"
将通过以上指令生成的动态链接库文件(.dll 或 .so)放到系统路径下;
Java 可通过以下方式查看系统路径:
System.getProperty("java.library.path")
encrypt.c 文件:
由 C/C++ 语言编写的代码,用于生成动态链接库,通过 java agent 方式实现 class 文件解密过程
完整代码
#include <string.h> #include <stdlib.h> #include "jni.h" #include <jvmti.h> #include <jni_md.h> // 加密方法 void encode(char *str) { unsigned int len = strlen(str); for (int i = 0; i < len; i ++) { str[i] = str[i] ^ 0x07; } } // jni 接口实现 JNIEXPORT jbyteArray JNICALL Java_com_wyhw_plugin_encrypt_encrypt_JarEncryptUtil_encrypt(JNIEnv * env, jclass cls, jbyteArray bytes) { char* dst = (char *) ((*env) -> GetByteArrayElements(env, bytes, 0)); encode(dst); (*env) -> SetByteArrayRegion(env, bytes, 0, strlen(dst), (jbyte *) dst); return bytes; } // class 文件解密 void JNICALL ClassDecryptHook(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jclass class_being_redefined, jobject loader, const char* name, jobject protection_domain, jint class_data_len, const unsigned char* class_data, jint* new_class_data_len, unsigned char** new_class_data) { //printf("ClassDecryptHook Files: %s\n", (char *) name); *new_class_data_len = class_data_len; (*jvmti_env) -> Allocate(jvmti_env, class_data_len, new_class_data); unsigned char* _data = *new_class_data; if (name && strncmp(name, "com/zk/sdk", 10) == 0 && strstr(name, "$$") == NULL) { //printf("\n\nhit: %s\n", (char *) name); //printf("len=%d\n\n", class_data_len); for (int i = 0; i < class_data_len; ++i) { _data[i] = class_data[i]; } encode(_data); } else { for (int i = 0; i < class_data_len; ++i) { _data[i] = class_data[i]; } } } JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { jvmtiEnv *jvmtiEnv; jint ret = (*vm) -> GetEnv(vm, (void **)&jvmtiEnv, JVMTI_VERSION); if (JNI_OK != ret) { printf("ERROR: Fail get jvmtiEvn!\n"); return ret; } jvmtiCapabilities jvmtiCapabilities; (void) memset(&jvmtiCapabilities, 0, sizeof(jvmtiCapabilities)); jvmtiCapabilities.can_generate_all_class_hook_events = 1; jvmtiCapabilities.can_tag_objects = 1; jvmtiCapabilities.can_generate_object_free_events = 1; jvmtiCapabilities.can_get_source_file_name = 1; jvmtiCapabilities.can_get_line_numbers = 1; jvmtiCapabilities.can_generate_vm_object_alloc_events = 1; jvmtiError error = (*jvmtiEnv) -> AddCapabilities(jvmtiEnv, &jvmtiCapabilities); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Unable to AddCapabilities JVMTI!\n"); return error; } jvmtiEventCallbacks callbacks; (void) memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &ClassDecryptHook; error = (*jvmtiEnv) -> SetEventCallbacks(jvmtiEnv, &callbacks, sizeof(callbacks)); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Fail setEventCallBacks!\n"); return error; } error = (*jvmtiEnv) -> SetEventNotificationMode(jvmtiEnv, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Fail SetEventNotificationMode!\n"); return error; } return JNI_OK; }
在项目 pom.xml 文件中添加 spring-boot-maven-plugin 插件
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
之后会在 Maven 插件处出现 spring-boot 插件
运行 mvn clean package 命令,生成可执行 jar 包(一般放在项目 target 目录下)
Tip:Maven 命令 clean 和 package 所涉及的生命周期不同,可以参考相关文档 参考文献
本插件的目标是对 spring-boot-maven-plugin 插件生成的可执行 jar 包进行二次处理,实现 class 字节码文件加密处理。
引入 pom 依赖
<dependency> <groupId>org.apache.maven.plugin-tools</groupId> <artifactId>maven-plugin-annotations</artifactId> <version>3.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-core</artifactId> <version>2.2.1</version> </dependency>
1、创建自定义 Mojo 类 EncryptMojo;
2、继承 org.apache.maven.plugin.AbstractMojo 抽象类;
3、类文件需要添加 org.apache.maven.plugins.annotations.Mojo 注解
4、实现 execute() 方法(加密过程就是在这里实现的)
EncryptMojo.java 完整代码
package com.wyhw.plugins.plugin.encrypt; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; /** * maven 加密插件(只适用于 springboot 项目) * * 此插件处于 Maven 生命周期的 package 阶段(若多个插件同处于 package 阶段时,此插件应处于最后执行的位置), * 将 spring-boot-maven-plugin 插件生成的可执行 jar 包加密处理,生成加密包 * * @author wanyanhw * @since 2021/12/31 10:44 */ @Mojo( name = "encrypt", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME ) public class EncryptMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; /** * 是否执行加密逻辑标识,默认“不加密” * 支持命令行设置,如:mvn package -Dencrypt.on=true */ @Parameter(property = "encrypt.on", defaultValue = "true") private boolean on; /** * 是否覆盖原包,默认“不覆盖” * 支持命令行设置,如:mvn package -Dencrypt.over=true */ @Parameter(property = "encrypt.over", defaultValue = "false") private boolean over; /** * 生成新包的名称后缀,例如:encrypt-maven-plugin-0.0.1-SNAPSHOT-entry.jar * 支持命令行设置,如:mvn package -Dencrypt.suffix=-entry */ @Parameter(property = "encrypt.suffix", defaultValue = "-entry") private String archiveSuffix; public void setOn(String on) { this.on = Boolean.valueOf(on); } public void setOver(String over) { this.over = Boolean.valueOf(over); } public void setArchiveSuffix(String archiveSuffix) { this.archiveSuffix = archiveSuffix; } public void execute() throws MojoExecutionException, MojoFailureException { Log log = getLog(); if (!on) { log.info("Disable encrypt package !!!"); return; } String originalArchivePath = project.getArtifact().getFile().getAbsolutePath(); log.info("Original file path: " + originalArchivePath); int lastSeparatorIndex = originalArchivePath.lastIndexOf("."); String fileName = originalArchivePath.substring(0, lastSeparatorIndex); String extendName = originalArchivePath.substring(lastSeparatorIndex + 1); String newFileName = fileName + archiveSuffix + "." + extendName; log.info("Encrypt file path: " + newFileName); try { if (over) { log.info("Overwrite original file"); newFileName = originalArchivePath; } JarEncryptUtil.encrypt(originalArchivePath, newFileName); } catch (Exception e) { log.error("自动加密失败", e); } } }
引入 pom 依赖
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>LATEST</version> </dependency>
直接上代码
JarEncryptUtil .java
package com.wyhw.plugins.plugin.encrypt; import org.apache.commons.compress.archivers.jar.JarArchiveEntry; import org.apache.commons.compress.archivers.jar.JarArchiveInputStream; import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream; import java.io.*; import java.util.jar.Manifest; import java.util.logging.Logger; /** * @author wanyanhw * @since 2021/12/20 13:52 */ public class JarEncryptUtil { static { System.loadLibrary("encrypt"); } private static Logger logger = Logger.getGlobal(); public native static byte[] encrypt(byte[] bytes); public static void encrypt(String src, String desc) throws Exception { File srcFile = new File(src); File descFile = new File(desc); try (FileInputStream fis = new FileInputStream(srcFile); FileOutputStream fos = new FileOutputStream(descFile); JarArchiveInputStream jis = new JarArchiveInputStream(fis); JarArchiveOutputStream jos = new JarArchiveOutputStream(fos);) { UnclosedInputStream nis = new UnclosedInputStream(jis); UnclosedOutputStream nos = new UnclosedOutputStream(jos); JarArchiveEntry jarArchiveEntry; while ((jarArchiveEntry = jis.getNextJarEntry()) != null) { long time = jarArchiveEntry.getTime(); String name = jarArchiveEntry.getName(); JarArchiveEntry descJarEntry = new JarArchiveEntry(name); descJarEntry.setTime(time); if (jarArchiveEntry.isDirectory()) { jos.putArchiveEntry(descJarEntry); } else if (name.equals("META-INF/MANIFEST.MF")) { Manifest manifest = new Manifest(nis); jos.putArchiveEntry(descJarEntry); manifest.write(nos); } else if (name.startsWith("BOOT-INF/classes/")) { jos.putArchiveEntry(descJarEntry); if (name.contains("com/zk/") && name.endsWith(".class")) { transfer(encryptCus(nis), nos); logger.info(name + " 文件已加密"); } else { transfer(nis, nos); } } else if (name.startsWith("BOOT-INF/lib/")) { descJarEntry.setMethod(JarArchiveEntry.STORED); descJarEntry.setSize(jarArchiveEntry.getSize()); descJarEntry.setCrc(jarArchiveEntry.getCrc()); jos.putArchiveEntry(descJarEntry); transfer(nis, nos); } else { jos.putArchiveEntry(descJarEntry); transfer(nis, nos); } jos.closeArchiveEntry(); } jos.finish(); } } private static InputStream encryptCus(InputStream nis) throws IOException { try (BufferedInputStream bis = new BufferedInputStream(nis); ByteArrayOutputStream bos = new ByteArrayOutputStream();) { byte[] buffer = new byte[4096]; int len; while ((len = bis.read(buffer)) != -1) { bos.write(buffer, 0, len); } bos.flush(); byte[] bytes = bos.toByteArray(); encrypt(bytes); return new ByteArrayInputStream(bytes); } } /** * 输入流传输到输出流 * * @param in 输入流 * @param out 输出流 * @throws IOException I/O 异常 */ private static void transfer(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[4096]; int length; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } out.flush(); } }
UnclosedOutputStream.java
package com.wyhw.plugins.plugin.encrypt; import java.io.IOException; import java.io.OutputStream; /** * 不关闭的输出流 * * @author wanyanhw * @since 2021/12/31 10:44 */ public class UnclosedOutputStream extends OutputStream { private final OutputStream out; public UnclosedOutputStream(OutputStream out) { this.out = out; } @Override public void write(int b) throws IOException { out.write(b); } @Override public void write(byte[] b) throws IOException { out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } @Override public void flush() throws IOException { out.flush(); } @Override public void close() { } }
UnclosedInputStream .java
package com.wyhw.plugins.plugin.encrypt; import java.io.IOException; import java.io.InputStream; /** * 不关闭的输入流 * * @author wanyanhw * @since 2021/12/31 10:44 */ public class UnclosedInputStream extends InputStream { private final InputStream in; public UnclosedInputStream(InputStream in) { this.in = in; } @Override public int read() throws IOException { return in.read(); } @Override public int read(byte[] b) throws IOException { return in.read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return in.read(b, off, len); } @Override public long skip(long n) throws IOException { return in.skip(n); } @Override public int available() throws IOException { return in.available(); } @Override public void mark(int readLimit) { in.mark(readLimit); } @Override public void reset() throws IOException { in.reset(); } @Override public boolean markSupported() { return in.markSupported(); } @Override public void close() { } }
在 execute() 方法中直接调用加密工具方法:
JarEncryptUtil.encrypt(originalArchivePath, newFileName);
需要加密的项目在使用自定义 Maven 插件时,需要引入插件 Jar 包;所以需要将此插件打包成供第三方项目依赖的 Jar 包
引入打包插件:
<build> <plugins> <plugin> <artifactId>maven-plugin-plugin</artifactId> <version>3.6.0</version> <executions> <execution> <id>default-addPluginArtifactMetadata</id> <phase>package</phase> <goals> <goal>addPluginArtifactMetadata</goal> </goals> </execution> <execution> <id>default-descriptor</id> <phase>process-classes</phase> <goals> <goal>descriptor</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
执行 maven 命令:
mvn clean package
至此,已经完成了加密插件的定义;
在需要加密的第三方项目 pom.xml 文件中引入依赖:
- <dependency>
- <groupId>com.wyhw.plugin</groupId>
- <artifactId>encrypt-maven-plugin</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- </dependency>
配置打包插件
在 spring-boot-maven-plugin 插件之后配置自定义加密插件(spring-boot-maven-plugin 插件和自定义的加密插件都是处于 Maven 生命周期中 package 阶段的插件,插件执行顺序跟配置的前后顺序一致;只有先执行 spring-boot-maven-plugin 插件,再执行自定义加密插件,加密过程才能正常执行)
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.wyhw.plugin</groupId> <artifactId>encrypt-maven-plugin</artifactId> <version>0.0.1-SNAPSHOT</version> <executions> <execution> <goals> <goal>encrypt</goal> </goals> </execution> </executions> </plugin> </plugins>
执行 package 命令,生成加密 jar 包
mvn clean package
加密成功后,控制台会打印出相应文件加密成功的日志信息
- "C:\Program Files\Java\jdk1.8.0_211\bin\java.exe" -Dmaven.multiModuleProjectDirectory=E:\space\idea\sdk "-Dmaven.home=D:\IntelliJ IDEA 2019.1.2\plugins\maven\lib\maven3" "-Dclassworlds.conf=D:\IntelliJ IDEA 2019.1.2\plugins\maven\lib\maven3\bin\m2.conf" "-javaagent:D:\IntelliJ IDEA 2019.1.2\lib\idea_rt.jar=57987:D:\IntelliJ IDEA 2019.1.2\bin" -Dfile.encoding=UTF-8 -classpath "D:\IntelliJ IDEA 2019.1.2\plugins\maven\lib\maven3\boot\plexus-classworlds-2.5.2.jar" org.codehaus.classworlds.Launcher -Didea.version2019.1.2 -DskipTests=true package
- [INFO] Scanning for projects...
- [INFO]
- [INFO] ------------------------------------------------------------------------
- [INFO] Building sdk 0.0.1-SNAPSHOT
- [INFO] ------------------------------------------------------------------------
- [INFO]
- [INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ sdk ---
- [INFO] Using 'UTF-8' encoding to copy filtered resources.
- [INFO] Using 'UTF-8' encoding to copy filtered properties files.
- [INFO] Copying 1 resource
- [INFO] Copying 0 resource
- [INFO]
- [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ sdk ---
- [INFO] Nothing to compile - all classes are up to date
- [INFO]
- [INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ sdk ---
- [INFO] Using 'UTF-8' encoding to copy filtered resources.
- [INFO] Using 'UTF-8' encoding to copy filtered properties files.
- [INFO] skip non existing resourceDirectory E:\space\idea\sdk\src\test\resources
- [INFO]
- [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ sdk ---
- [INFO] Nothing to compile - all classes are up to date
- [INFO]
- [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ sdk ---
- [INFO] Tests are skipped.
- [INFO]
- [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ sdk ---
- [INFO] Building jar: E:\space\idea\sdk\target\sdk-0.0.1-SNAPSHOT.jar
- [INFO]
- [INFO] --- spring-boot-maven-plugin:2.5.3:repackage (repackage) @ sdk ---
- [INFO] Replacing main artifact with repackaged archive
- [INFO]
- [INFO] --- zk-common-plugins:0.0.1-SNAPSHOT:encrypt (default) @ sdk ---
- [INFO] Original file path: E:\space\idea\sdk\target\sdk-0.0.1-SNAPSHOT.jar
- [INFO] Encrypt file path: E:\space\idea\sdk\target\sdk-0.0.1-SNAPSHOT-encrypt.jar
- 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
- 信息: BOOT-INF/classes/com/zk/sdk/core/util/CRC32Util.class 文件已加密
- 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
- 信息: BOOT-INF/classes/com/zk/sdk/core/wrapper/RGBTriple.class 文件已加密
- 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
- 信息: BOOT-INF/classes/com/zk/sdk/SdkApplication.class 文件已加密
- [INFO] ------------------------------------------------------------------------
- [INFO] BUILD SUCCESS
- [INFO] ------------------------------------------------------------------------
- [INFO] Total time: 2.235 s
- [INFO] Finished at: 2022-01-04T19:11:42+08:00
- [INFO] Final Memory: 27M/408M
- [INFO] ------------------------------------------------------------------------
-
- Process finished with exit code 0

JVMTI 官方文档:JVM(TM) Tool Interface 1.2.3
XJar 开源项目:GitHub - core-lib/xjar: Spring Boot JAR 安全加密运行工具,支持的原生JAR。
Maven: Maven 的生命周期
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。