前言
公司项目要求进行源代码加密,防止他人进行反编译(毕竟项目要运行在客户的机器上)。项目框架采用的是:Spring + Spring MVC + Spring Data JPA。可在网上查阅资料,关于Spring项目源代码加密的内容不多,也没找到什么现成的工具。所以,只能自己动手写加密代码了。过程几经坎坷,在此进行记录一下,也希望能帮到有相同需求的朋友。
思路
写工具类手动对项目指定包下生成的class文件内容进行加密,在容器框架加载class文件时解密获取正确的字节码。头疼的来了,不少地方都对字节码文件进行了读取解析,要改不少源代码。
准备工作
1、准备JDK1.8源码(我电脑配置的JDK为1.8.0_131)。在JDK安装目录下有个src.zip,那个就是当前版本JDK的源码
2、准备Tomcat7源码。(需要安装ant进行项目构建)
3、准备Spring源码。这个下载的框架里面自带的
4、准备hibernate-entitymanager源码
5、准备aspectjweaver源码(项目中切面编程做日志)
5、一个加密解密的工具类jar包。用于对生成的class文件进行加密,在我们修改后的tomcat、spring中需要调用这个类对已加密的class进行解密。另外这个jar包最后需要使用加密锁进行壳加密以保证加解密代码的安全
6、写一个读取配置文件的工具类。配置文件中记录需要解密的包名、路径地址、是否执行解密操作(便于开发时调试)等信息
二、需要修改的类
1、JDK中需要修改的类
a) java.io.FileInputStream:修改后覆盖到rt.jar中对应包里的class文件
2、Tomcat中需要修改的类
a) org.apache.tomcat.util.bcel.classfile.ClassParser:修改后覆盖到tomcat-coyote.jar中对应包里的class文件
b) org.apache.catalina.loader.WebappClassLoader:修改后覆盖到catalina.jar中对应包里的class文件
3、Spring中需要修改的类
a) org.springframework.core.type.classreading.SimpleMetadataReader:修改后覆盖到spring-core-4.3.7.RELEASE.jar中对应包里的class文件
4、hibernate-entitymanager中需要修改的类
a) org.hibernate.ejb.packaging.AbstractJarVisitor:修改后覆盖hibernate-entitymanager-4.1.8.Final.jar中对应包里的class文件
b) org.hibernate.ejb.packaging.ExplodedJarVisitor:修改后覆盖hibernate-entitymanager-4.1.8.Final.jar中对应包里的class文件
5、aspectjweaver中需要修改的类
a) org.aspectj.apache.bcel.classfile.ClassParser:修改后覆盖aspectjweaver.jar中对应包里的class文件
b) org.aspectj.apache.bcel.util.NonCachingClassLoaderRepository:修改后覆盖aspectjweaver.jar中对应包里的class文件
三、进行修改
1、修改JDK提供的java.io.FileInputStream类
目的:提供方法获取class文件的路径,这样才能做后面的解码。
/**
* The path of the referenced file
* (null if the stream is created with a file descriptor)
*/
private final String path;
/**
*
* @title: getPath
* @description: 提供方法让外部能够获取到当前文件路径
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月11日 下午1:07:44
* @return
*/
public String getPath() {
return path;
}
2、修改Tomcat的org.apache.tomcat.util.bcel.classfile.ClassParser类
对ClassParser方法进行重写。目的:对配置文件中要求进行解密的class文件经过解密后,再将流给Tomcat。
原方法
public ClassParser(final InputStream inputStream) {
this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
}
修改后
public ClassParser(final InputStream inputStream) {
// TODO 判断该类是否需要解密
InputStream newInputStream = inputStream;
try {
//DecodeConf是记录配置信息的类
//DecodeConf.isRunDecode:记录是否需要执行解密操作
if (DecodeConf.getConf().isRunDecode() && inputStream instanceof FileInputStream) {
// 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
// FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
String path = ((FileInputStream)inputStream).getPath();
FileOperateHelper.log("ClassParser path=" + path, true);
/*
*DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
*dirs是一个集合对象,记录了所有需要解密的目录
*我配置文件中记录的是相对路径,只到包名这层
*如包名为com.abc.service则记录的目录路径为/com/abc/service/
*判断当前目录是否需要解密
*/
for(String dir : DecodeConf.getConf().getDirs()) {
// 统一文件路径分隔符
dir = dir.replace("/", "\\");
FileOperateHelper.log("ClassParser dir=" + dir, true);
if (path.indexOf(dir) != -1) {
// 该类在需要解密的目录下,进行解密
// 读取文件内容
byte[] oldcontent = FileOperateHelper.read(new File(path));
// 进行解密
byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
newInputStream = new ByteArrayInputStream(decodeByte);
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 不管上面有没有解密,用newInputStream生成DataInputStream
this.dataInputStream = new DataInputStream(new BufferedInputStream(newInputStream, BUFSIZE));
}
3、修改Tomcat的org.apache.catalina.loader.WebappClassLoader类
在类中重写下findClass方法
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
/**
* 类加载器的三个机制:委托、单一性、可见性
* 委托:指加载一个类的请求交给父类加载器,若父类加载器不可以找到或者加载到,再加载这个类
* 单一性:指子类加载器不会再次加载父类加载器已经加载过的类
* 可见性:子类加载器可以看见父类加载器加载的所有类,而父类加载器不可以看见子类加载器所加载的类
*
* 所以,return super.findClass(name);是可能加载不到类的(可能有些类需要子类加载器才加载到),
* 即意味着可能会产生ClassNotfoundException异常,所以不能放在try catch代码块里边,因为调用者需要知道是否成功加载。
*/
// 如果配置文件中配置了不执行解密,直接调用父类的findClass
if (!DecodeConf.getConf().isRunDecode()) {
return super.findClass(name);
}
try {
// TODO 判断当前类是否需要解密
// 判断当前类是否在需要解密的包路径之下
for(String pkg : DecodeConf.getConf().getPackages()) {
if (name.indexOf(pkg) != -1) {
// 将类名称转换为文件路径及文件名
String fileName = name.replace(".", "/") + ".class";
// 根据文件路径,从已加载的类的ResourceEntry集合中获取指定的ResourceEntry
ResourceEntry resourceEntry = resourceEntries.get("/" + fileName);
// 拼接class文件绝对路径,当然,ResourceEntry中也有文件的绝对路径
String basePath = DecodeConf.getConf().getClassPath();
String classPath = basePath + fileName;
// 但是,内部类获取不到ResourceEntry(不确定是不是全部的内部类都获取不到),如果获取得到就用ResourceEntry中的路径
if (resourceEntry != null) {
// 如果路径中带空格会变成"%20"导致无法成功获取文件,进行处理
classPath = URLDecoder.decode(resourceEntry.source.getPath(), "UTF-8");
if (classPath.startsWith("/")) {
classPath = classPath.substring(1);
}
}
File classFile = new File(classPath);
// 如果文件存在,则进行解密
if (classFile.exists() && classFile.isFile()) {
InputStream is = StreamDecode.decode(new FileInputStream(classFile));
// 将解密后的流包含的内容,读取到字节数组
byte[] b = new byte[is.available()];
is.read(b);
// 将字节数组内容转换成Class对象
return defineClass(b, 0, b.length);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
4、修改Spring的org.springframework.core.type.classreading.SimpleMetadataReader类
原方法
SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
InputStream is = new BufferedInputStream(resource.getInputStream());
ClassReader classReader;
try {
classReader = new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new NestedIOException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
}
finally {
is.close();
}
AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
classReader.accept(visitor, ClassReader.SKIP_DEBUG);
this.annotationMetadata = visitor;
// (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
this.classMetadata = visitor;
this.resource = resource;
}
修改后
SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
InputStream is = new BufferedInputStream(resource.getInputStream());
InputStream newInputStream = is;
ClassReader classReader;
try {
try {
//TODO 判断是否需要解密
FileOperateHelper.log("isRunDecode() = " + String.valueOf(DecodeConf.getConf().isRunDecode()), true);
if(DecodeConf.getConf().isRunDecode()) {
for(int i=0; i<DecodeConf.getConf().getDirs().length; i++) {
String dir = DecodeConf.getConf().getDirs()[i];
String filepath = "";
// 获取文件路径,判断是否为需要解密的class(即该class在需要解密的路径下)
if (resource instanceof FileSystemResource) {
filepath = ((FileSystemResource)resource).getPath();
}else if (resource instanceof ClassPathResource) {
filepath = ((ClassPathResource)resource).getPath();
}
FileOperateHelper.log("filepath = " + filepath, true);
// 统一文件路径分隔符
// dir = dir.replace("\\", "/");
FileOperateHelper.log("dir = " + dir, true);
if (filepath.indexOf(dir) != -1) {
FileOperateHelper.log("try to decrypt: " + filepath, true);
is = StreamDecode.decode(is);
break;
}
}
}
} catch (Exception e) {
throw new RuntimeException("解密出错!");
}
classReader = new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new NestedIOException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
}
finally {
is.close();
}
AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
classReader.accept(visitor, ClassReader.SKIP_DEBUG);
this.annotationMetadata = visitor;
// (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
this.classMetadata = visitor;
this.resource = resource;
}
5、修改hibernate-entitymanager中的org.hibernate.ejb.packaging.AbstractJarVisitor类
原方法
private boolean checkAnnotationMatching(InputStream is, JavaElementFilter filter) throws IOException {
if ( filter.getAnnotations().length == 0 ) {
is.close();
return true;
}
DataInputStream dstream = new DataInputStream( is);
ClassFile cf = null;
try {
cf = new ClassFile( dstream );
}
finally {
dstream.close();
is.close();
}
boolean match = false;
AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute( AnnotationsAttribute.visibleTag );
if ( visible != null ) {
for ( Class annotation : filter.getAnnotations() ) {
match = visible.getAnnotation( annotation.getName() ) != null;
if ( match ) break;
}
}
return match;
}
修改后
private boolean checkAnnotationMatching(InputStream is, JavaElementFilter filter) throws IOException {
if ( filter.getAnnotations().length == 0 ) {
is.close();
return true;
}
InputStream newInputStream = is;
// TODO 检查该类是否需要进行解密
try {
//DecodeConf是记录配置信息的类
//DecodeConf.isRunDecode:记录是否需要执行解密操作
FileOperateHelper.log("AbstractJarVisitor isRunDecode()=" + String.valueOf(DecodeConf.getConf().isRunDecode()), true);
FileOperateHelper.log("AbstractJarVisitor inputStream tpye: " + is.getClass().getSimpleName(), true);
if (DecodeConf.getConf().isRunDecode() && is instanceof MyFileInputStream) {
// 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
// FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
String path = "";
MyFileInputStream fis = (MyFileInputStream)is;
path = fis.getPath();
FileOperateHelper.log("AbstractJarVisitor path=" + path, true);
/*
*DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
*dirs是一个集合对象,记录了所有需要解密的目录
*我配置文件中记录的是相对路径,只到包名这层
*如包名为com.abc.service则记录的目录路径为\\com\\abc\\service\\
*判断当前目录是否需要解密
*/
for(String dir : DecodeConf.getConf().getDirs()) {
// 统一文件路径分隔符
dir = dir.replace("/", "\\");
FileOperateHelper.log("AbstractJarVisitor dir=" + dir, true);
if (path.indexOf(dir) != -1) {
// 该类在需要解密的目录下,进行解密
// 读取文件内容
byte[] oldcontent = FileOperateHelper.read(new File(path));
// 进行解密
byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
newInputStream = new ByteArrayInputStream(decodeByte);
break;
}
}
}
} catch (Exception e) {
FileOperateHelper.log("AbstractJarVisitor transform inputStream error: " + e.getMessage(), true);
e.printStackTrace();
}
DataInputStream dstream = new DataInputStream( newInputStream );
ClassFile cf = null;
try {
cf = new ClassFile( dstream );
}
finally {
dstream.close();
is.close();
}
boolean match = false;
AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute( AnnotationsAttribute.visibleTag );
if ( visible != null ) {
for ( Class annotation : filter.getAnnotations() ) {
match = visible.getAnnotation( annotation.getName() ) != null;
if ( match ) break;
}
}
return match;
}
6、修改hibernate-entitymanager中的org.hibernate.ejb.packaging.ExplodedJarVisitor类
原方法
private void getClassNamesInTree(File jarFile, String header)
throws IOException
{
File[] files = jarFile.listFiles();
header = header + "/";
File[] arr$ = files; int len$ = arr$.length; for (int i$ = 0; i$ < len$; ++i$) { File localFile = arr$[i$];
if (!(localFile.isDirectory())) {
String entryName = localFile.getName();
addElement(header + entryName, new BufferedInputStream(new FileInputStream(localFile)), new BufferedInputStream(new FileInputStream(localFile)));
}
else
{
getClassNamesInTree(localFile, header + localFile.getName());
}
}
}
修改后
private void getClassNamesInTree(File jarFile, String header) throws IOException {
File[] files = jarFile.listFiles();
header = header == null ? "" : header + "/";
for ( File localFile : files ) {
if ( !localFile.isDirectory() ) {
String entryName = localFile.getName();
// MyFileInputStream继承自java.io.FileInputStream,这样才能通过getPath()拿到文件路径
addElement(
header + entryName,
new MyFileInputStream( localFile ),
new MyFileInputStream( localFile )
);
}
else {
getClassNamesInTree( localFile, header + localFile.getName() );
}
}
}
7、修改aspectjweaver中的org.aspectj.apache.bcel.classfile.ClassParser类
原方法
/** Parse class from the given stream */
public ClassParser(InputStream file, String filename) {
this.filename = filename;
if (file instanceof DataInputStream) this.file = (DataInputStream)file;
else this.file = new DataInputStream(new BufferedInputStream(file,BUFSIZE));
}
public ClassParser(ByteArrayInputStream baos, String filename) {
this.filename = filename;
this.file = new DataInputStream(baos);
}
/** Parse class from given .class file */
public ClassParser(String file_name) throws IOException {
this.filename = file_name;
file = new DataInputStream(new BufferedInputStream(new FileInputStream(file_name),BUFSIZE));
}
修改后
/** Parse class from the given stream */
public ClassParser(InputStream file, String filename) {
this.filename = filename;
// 判断是否需要对流进行解密
file = decryptStream(file);
if (file instanceof DataInputStream) this.file = (DataInputStream)file;
else this.file = new DataInputStream(new BufferedInputStream(file,BUFSIZE));
}
public ClassParser(ByteArrayInputStream baos, String filename) {
this.filename = filename;
this.file = new DataInputStream(baos);
}
/** Parse class from given .class file */
public ClassParser(String file_name) throws IOException {
this.filename = file_name;
MyFileInputStream mis = new MyFileInputStream(new File(file_name));
// 判断是否需要对流进行解密
InputStream is = decryptStream(mis);
file = new DataInputStream(new BufferedInputStream(is,BUFSIZE));
}
// 判断是否需要解密,对要解密的流进行解密操作
public InputStream decryptStream(InputStream inputStream) {
InputStream newInputStream = inputStream;
try {
//DecodeConf是记录配置信息的类
//DecodeConf.isRunDecode:记录是否需要执行解密操作
if (DecodeConf.getConf().isRunDecode() && inputStream instanceof MyFileInputStream) {
// 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
// FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
String path = ((MyFileInputStream)inputStream).getPath();
FileOperateHelper.log("aspectjweaver ClassParser path=" + path, true);
/*
*DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
*dirs是一个集合对象,记录了所有需要解密的目录
*我配置文件中记录的是相对路径,只到包名这层
*如包名为com.abc.service则记录的目录路径为\\com\\abc\\service\\
*判断当前目录是否需要解密
*/
for(String dir : DecodeConf.getConf().getDirs()) {
// 统一文件路径分隔符
dir = dir.replace("/", "\\");
FileOperateHelper.log("aspectjweaver ClassParser dir=" + dir, true);
if (path.indexOf(dir) != -1) {
// 该类在需要解密的目录下,进行解密
// 读取文件内容
byte[] oldcontent = FileOperateHelper.read(new File(path));
// 进行解密
byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
newInputStream = new ByteArrayInputStream(decodeByte);
return newInputStream;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
8、修改aspectjweaver中的org.aspectj.apache.bcel.util.NonCachingClassLoaderRepository类
原方法
private JavaClass loadJavaClass(String className) throws ClassNotFoundException {
String classFile = className.replace('.', '/');
try {
InputStream is = loaderRef.getClassLoader().getResourceAsStream(classFile + ".class");
if (is == null) {
throw new ClassNotFoundException(className + " not found.");
}
ClassParser parser = new ClassParser(is, className);
return parser.parse();
} catch (IOException e) {
throw new ClassNotFoundException(e.toString());
}
}
修改后
private JavaClass loadJavaClass(String className) throws ClassNotFoundException {
String classFile = className.replace('.', '/');
try {
// InputStream is = loaderRef.getClassLoader().getResourceAsStream(classFile + ".class");
//TODO 命令行编译时,FileInputStream.getPath()方法获取不到,所以这里替换成我的MyFileInputStream
URL url = loaderRef.getClassLoader().getResource(classFile + ".class");
// 空格会显示为"%20",导致获取不到文件
String path = url.getPath();
path = URLDecoder.decode(path, "UTF-8");
MyFileInputStream is = new MyFileInputStream(new File(path));
if (is == null) {
throw new ClassNotFoundException(className + " not found.");
}
ClassParser parser = new ClassParser(is, className);
return parser.parse();
} catch (IOException e) {
throw new ClassNotFoundException(e.toString());
}
}
四、关于修改的源码编译问题
我的话,Tomcat7用myeclipse搭建了源码项目,所以编译不成问题。其他的源码,修改过后用命令行对修改的java文件进行编译。示例如下:
C:\Users\Administrator\Desktop\codeEncrypt\aspectjweaver\aspectjweaver-1.7.2-sou
rces\org\aspectj\apache\bcel\util>javac -encoding utf-8 -Djava.ext.dirs=C:\Users
\Administrator\Desktop\codeEncrypt\aspectjweaver\needJar NonCachingClassLoaderRe
pository.java
解释:-encoding 指定源文件使用的字符编码。因为文件中使用到中文注释,这里指定utf-8;
-Djava.ext.dirs 指定编译时要引用到的jar包路径,建议把所有引用到的jar包放在同一个文件夹
另外,命令行编译java文件时,不知道为什么,获取不到JDK中为java.io.FileInputStream添加的getPath()方法,编译无法通过。我确定已经替换了当前配置的JDK的FileInputStream.class文件。而且,Tomcat7的源码修改中,也用到了该方法。但是用myeclipse编译,并没有问题。最后,采取的解决方案是:新建一个类MyFileInputStream,继承自java.io.FileInputSteam。这样,在编译时就没有问题了。无法理解,希望知道缘由的朋友给予指点。
五、配置文件
通过配置文件记录加/解密的信息,放置在tomcat的lib目录下。例如:
文件内容:
#是否运行解密
isRunDecode=true
#加密文件相对目录(就是包名转成的目录),多个用“,”分割
#dirs=\\com\\smt\\iptv\\portal\\web\\json
dirs=/com/smt/iptv/portal/web/json
#加密文件的包名,多个用“,”分割
packages=com.smt.iptv.portal.web.json
#加密文件的绝对路径
classPath=C:\\Develop\\apache-tomcat-7.0.93\\webapps\\iptvmanager_maven_test\\WEB-INF\\classes
下面是的工具类、加/解密类等,建议新建java项目,放在一起,打成jar包,编译修改的源码时要用到。另外,也要放一份在Tomcat的lib目录,项目运行需要。
读取配置文件的工具类:
package com.jack.vesoft.classencrypt.classreading;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
/**
*
* Title: DecodeConf
* Description: 读取配置文件
* @author 陈家宝
* @date 2019年3月11日 下午1:50:31
*
*/
public class DecodeConf {
private Properties props = new Properties();
private boolean isRunDecode = false;
private String[] dirs; // 需要解密的路径(包的文件夹层级)
private String[] packages; // 需要解密的包
private String classPath; // 加密文件的绝对路径
private static DecodeConf decodeConf;
public Properties getProps(){
return props;
}
public boolean isRunDecode() {
return isRunDecode;
}
public void setRunDecode(boolean isRunDecode) {
this.isRunDecode = isRunDecode;
}
public String[] getDirs() {
return dirs;
}
public void setDirs(String[] dirs) {
this.dirs = dirs;
}
public String[] getPackages() {
return packages;
}
public void setPackages(String[] packages) {
this.packages = packages;
}
public String getClassPath() {
return classPath;
}
public void setClassPath(String classPath) {
this.classPath = classPath;
}
public static DecodeConf getConf(){
return decodeConf;
}
// 默认读取类路径下的decode.properties文件,所以此工具类所在的jar包,运行时要放在Tomcat的lib目录,decode.properties文件也要放在这里
static{
decodeConf = new DecodeConf();
InputStream is = DecodeConf.class.getClassLoader().getResourceAsStream("decode.properties");
if (is != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
try{
decodeConf.getProps().load(br);
decodeConf.setRunDecode(Boolean.parseBoolean(decodeConf.getProps().getProperty("isRunDecode")));
decodeConf.setDirs(decodeConf.getProps().getProperty("dirs").split(","));
decodeConf.setPackages(decodeConf.getProps().getProperty("packages").split(","));
decodeConf.setClassPath(decodeConf.getProps().getProperty("classPath"));
}catch(Exception e){
e.printStackTrace();
}finally {
try {
br.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
*
* @title: explicitPropertyFilePos
* @description: 如果decode.properties不在类路径,调用此方法明确其位置
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月12日 上午11:33:15
* @param filepath
*/
public static void explicitPropertyFilePos(String filepath) {
try {
InputStream is = new FileInputStream(new File(filepath));
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
decodeConf = new DecodeConf();
decodeConf.getProps().load(reader);
decodeConf.setRunDecode(Boolean.parseBoolean(decodeConf.getProps().getProperty("isRunDecode")));
decodeConf.setDirs(decodeConf.getProps().getProperty("dirs").split(","));
decodeConf.setPackages(decodeConf.getProps().getProperty("packages").split(","));
decodeConf.setClassPath(decodeConf.getProps().getProperty("classPath"));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "classpath = " + decodeConf.classPath;
}
}
加/解密工具类:
package com.jack.vesoft.classencrypt.main;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import com.jack.vesoft.encode.util.FileOperateHelper;
import com.jack.vesoft.encode.util.Utils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
*
* Title: EncodeUtil
* Description: 加密工具类
* @author 陈家宝
* @date 2019年3月11日 下午1:46:15
*
*/
public class EncodeUtil {
public static void main(String[] args) throws Exception {
/*String content = "admin";
System.out.println("加密前:" + content);
String key = "key";
System.out.println("加密密钥和解密密钥:" + key);
String encrypt = aesEncrypt(content, key);
System.out.println("加密后:" + encrypt);
String decrypt = aesDecrypt(encrypt, key);
System.out.println("解密后:" + decrypt); */
String content = "w4rDvsK6wr4AAAAzAGIHAAIBADFjb20vc210L2lwdHYvcG9ydGFsL3dlYi9qc29uL0RhdGFDZW50L0RhdGFDZW50ZXJDb250cm9sbGVyBwAEAQAQamF2YS9sYW5nL09iamVjdAEAEmlwdHZwcm9maWxlU2VydmljZQEANExjb20vc210L2lwdHYvcG9ydGFsL3NlcnZpY2UvY21wL0lwdHZwcm9maWxlU2VydmljZTsBABlSdW50aW1lVmlzaWJsZUFubm90YXRpb25zAQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvYmVhbnMvZmFjdG9yeS9hbm5vdGF0aW9uL0F1dG93aXJlZDsBAAY8aW5pdD4BAAMoKVYBAARDb2RlCgADAA0MAAkACgEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBADNMY29tL3NtdC9pcHR2L3BvcnRhbC93ZWIvanNvbi9EYXRhQ2VudGVyQ29udHJvbGxlcjsBAA5nZXRJcHR2cHJvZmlsZQEAJChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9MaXN0OwEACVNpZ25hdHVyZQEAOChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9MaXN0PExqYXZhL2xhbmcvU3RyaW5nOz47AQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZzsBAAV2YWx1ZQEADC9pcHR2cHJvZmlsZQEABm1ldGhvZAEAN0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdE1ldGhvZDsBAANHRVQBAAhwcm9kdWNlcwEAHmFwcGxpY2F0aW9uL2pzb247Y2hhcnNldD1VVEYtOAEANkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9i";
System.out.println(FileOperateHelper.bytesToHexString(base64Decode(content)));
}
/**
* 将byte[]转为各种进制的字符串
* @param bytes byte[]
* @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
* @return 转换后的字符
*/
public static String binary(byte[] bytes, int radix){
return new BigInteger(1, bytes).toString(radix);//
}
/**
* base 64 encode
* @param bytes 待编码的byte[]
* @return 编码后的base 64 code
*/
public static String base64Encode(byte[] bytes){
return new BASE64Encoder().encode(bytes);
}
/**
* base 64 decode
* @param base64Code 待解码的base 64 code
* @return 解码后的byte[]
* @throws Exception
*/
public static byte[] base64Decode(String base64Code) throws Exception{
return Utils.isEmptyString(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);
}
/**
* 获取byte[]的md5
* @param bytes byte[]
* @return md5
* @throws Exception
*/
public static byte[] md5(byte[] bytes) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
return md.digest();
}
/**
* 获取字符串md5
* @param msg
* @return md5
* @throws Exception
*/
public static byte[] md5(String msg) throws Exception {
return Utils.isEmptyString(msg) ? null : md5(msg.getBytes());
}
/**
* 结合base64实现md5加密
* @param msg 待加密字符串
* @return 获取md5后转为base64
* @throws Exception
*/
public static String md5Encrypt(String msg) throws Exception{
return Utils.isEmptyString(msg) ? null : base64Encode(md5(msg));
}
/**
* AES加密
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的byte[]
* @throws Exception
*/
public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(encryptKey.getBytes()));
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(kgen.generateKey().getEncoded(), "AES"));
return cipher.doFinal(content.getBytes("utf-8"));
}
/**
* AES加密为base 64 code
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的base 64 code
* @throws Exception
*/
public static String aesEncrypt(String content, String encryptKey) throws Exception {
return base64Encode(aesEncryptToBytes(content, encryptKey));
}
/**
* AES解密
* @param encryptBytes 待解密的byte[]
* @param decryptKey 解密密钥
* @return 解密后的String
* @throws Exception
*/
public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(decryptKey.getBytes()));
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(kgen.generateKey().getEncoded(), "AES"));
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}
/**
* 将base 64 code AES解密
* @param encryptStr 待解密的base 64 code
* @param decryptKey 解密密钥
* @return 解密后的string
* @throws Exception
*/
public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
return Utils.isEmptyString(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
}
/**
*
* @title: myEncrypt
* @description: 简单加密方式,每一位byte加一
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月13日 上午1:51:56
* @param buf
* @return
*/
public static byte[] simpleEncrypt(byte[] buf) {
if (buf == null || buf.length == 0) {
return null;
}
byte[] result = new byte[buf.length];
for(int i=0; i<buf.length; i++) {
result[i] = (byte) (buf[i] + 1);
}
return result;
}
/**
*
* @title: simpleDecrypt
* @description: 简单解密方式,每一位byte减一
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月13日 上午1:53:27
* @param oldBytes
* @return
*/
public static byte[] simpleDecrypt(byte[] oldBytes) {
if (oldBytes == null || oldBytes.length == 0) {
return null;
}
byte[] result = new byte[oldBytes.length];
for(int i=0; i<oldBytes.length; i++) {
result[i] = (byte) (oldBytes[i] - 1);
}
return result;
}
}
方法虽然挺多的,但实际用到的就后面两个:simpleDecrypt(byte[] oldBytes)方法和simpleEncrypt(byte[] buf)方法。之前也是想采用什么base64、aes等算法对class文件内容进行加/解密。遗憾的是,没能做出来。因为使用那些算法涉及到了字符编码的问题,在读写class文件、new String()等细节上,虽然指定了编码,但最后发现文件内容还是不正确。主要是class文件的标志(MAGIC)出现了问题,文件后面的内容加解密正常。无奈之下,全程使用byte字节进行操作,不涉及字符编码,加/解密就成功实现了。
另外一个解密工具类:
package com.jack.vesoft.classencrypt.main;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import com.jack.vesoft.encode.util.FileOperateHelper;
/**
*
* Title: StreamDecode
* Description: 解密工具类
* @author 陈家宝
* @date 2019年3月11日 下午1:54:01
*
*/
public class StreamDecode {
public static InputStream decode(InputStream is) throws Exception{
byte[] oldBytes = FileOperateHelper.read(is);
byte[] newBytes = EncodeUtil.simpleDecrypt(oldBytes);
// 解密得到的数据
String hexStr = FileOperateHelper.bytesToHexString(newBytes);
FileOperateHelper.log("StreamDecode->decode->hexStr=" + hexStr, true);
return new ByteArrayInputStream(newBytes);
}
public static void main(String[] args) throws Exception {
String pathname = "C:\\Develop\\apache-tomcat-7.0.93\\webapps\\iptvmanager_maven_test\\WEB-INF\\classes\\com\\smt\\iptv\\portal\\web\\json\\DataCenterController.class";
InputStream is = new FileInputStream(new File(pathname));
decode(is);
}
}
读写文件工具类:
package com.jack.vesoft.encode.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
*
* Title: FileOperateHelper
* Description: 文件读写帮助类
* @author 陈家宝
* @date 2019年3月12日 下午1:37:01
*
*/
public class FileOperateHelper {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final boolean HIDE_LOG = false; // 隐藏调试输出
/**
*
* @title: read
* @description: 读取文件内容
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月13日 上午2:16:04
* @param file
* @return
* @throws Exception
*/
public static byte[] read(File file) throws Exception {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
byte[] buf = new byte[1024];
int len = 0;
List<Byte> list = new ArrayList<Byte>();
while((len = bis.read(buf)) > 0) {
for(int i=0; i<len; i++) {
list.add(buf[i]);
}
}
bis.close();
// InputStream.available()只是一个估计,不可靠,要不也没这么多事了
byte[] result = new byte[list.size()];
for(int i=0; i<list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
/**
*
* @title: read
* @description: 读取流内容
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月13日 上午2:17:07
* @param in
* @return
* @throws IOException
*/
public static byte[] read(InputStream in) throws IOException {
BufferedInputStream bis = new BufferedInputStream(in);
byte[] buf = new byte[1024];
int len = 0;
List<Byte> list = new ArrayList<Byte>();
while((len = bis.read(buf)) > 0) {
for(int i=0; i<len; i++) {
list.add(buf[i]);
}
}
bis.close();
// InputStream.available()只是一个估计,不可靠,要不也没这么多事了
byte[] result = new byte[list.size()];
for(int i=0; i<list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
/**
*
* @title: write
* @description: 将内容写入文件
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月13日 上午2:19:19
* @param content
* @param file
* @throws Exception
*/
public static void write(byte[] content, File file) throws Exception {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bos.write(content);
bos.flush();
bos.close();
}
/**
*
* @title: log
* @description: 做日志,不能断点调试太惨了
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月12日 下午8:10:22
* @param content 写入文件的内容
* @param append 是否追加到文件末尾
* @throws Exception
*/
public static void log(String content, boolean append) {
if (HIDE_LOG) {
return;
}
try {
File file = new File("D:\\log.txt");
if (!file.exists()) {
file.createNewFile();
}
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file,append)));
bw.write("\r\n" + sdf.format(new Date()) + " " + content);
bw.flush();
bw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] hexToBytes(String hex) {
hex = hex.length() % 2 != 0 ? "0" + hex : hex;
byte[] b = new byte[hex.length() / 2];
for (int i = 0; i < b.length; i++) {
int index = i * 2;
int v = Integer.parseInt(hex.substring(index, index + 2), 16);
b[i] = (byte) v;
}
return b;
}
/**
* Convert byte[] to hex string
*
* @param src byte[] data
* @return hex string
*/
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
public static void main(String[] args) {
byte[] b = hexToBytes("cafebabe");
String str = bytesToHexString(b);
System.out.println(str);
}
}
MyFileInputStream类,为了解决命令行编译修改的源码文件,getPath()获取不到的问题:
package com.jack.vesoft.encode.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class MyFileInputStream extends FileInputStream {
public MyFileInputStream(File file) throws FileNotFoundException {
super(file);
}
@Override
public String getPath() {
// 命令行获取不到java.io.FileInputStream的getPath()方法,该方法是修改JDK而来。看下继承是否有效
// 事实证明,这么玩是可以的。我能怎么办呢?我也很绝望啊
return super.getPath();
}
}
手动对项目class文件进行加密的工具类:
package test;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import com.jack.vesoft.classencrypt.classreading.DecodeConf;
import com.jack.vesoft.classencrypt.main.EncodeUtil;
import com.jack.vesoft.encode.util.FileOperateHelper;
public class QuickStart {
public static void main(String[] args) throws Exception {
// 命令行输入加/解密配置文件绝对路径
// C:\\Develop\\apache-tomcat-7.0.93\\lib\\decode.properties
System.out.println("请输入加/解密配置文件绝对路径:");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
String filepath = br.readLine();
// 获取配置文件信息
DecodeConf.explicitPropertyFilePos(filepath);
// 输出配置信息
String classpath = DecodeConf.getConf().getClassPath();
String[] dirs = DecodeConf.getConf().getDirs();
String[] pkgs = DecodeConf.getConf().getPackages();
// 如果开启加密,循坏dirs路径数组,对要加密的路径下的文件递归进行加密
if (DecodeConf.getConf().isRunDecode()) {
encryptFileInTargetDirectory(dirs);
}
}
/**
*
* @title: encryptFileInTargetDirectory
* @description: 递归进行加密
* @author: 陈家宝
* @version: V1.00
* @date: 2019年3月12日 上午10:09:18
* @param dirs
* @throws Exception
*/
public static void encryptFileInTargetDirectory(String[] dirs) throws Exception {
for(int i=0; i<dirs.length; i++) {
String dir = dirs[i];
File file = new File(DecodeConf.getConf().getClassPath() + dir);
// 如果file为路径,递归
if (file.exists()) {
if (file.isDirectory()) {
encryptFiles(file.listFiles());
}else {
// 到这说明是文件,进行加密
encodeAndWriteBack(file);
}
}
}
}
private static void encodeAndWriteBack(File file) throws Exception {
// 读取文件内容
byte[] oldcontent = FileOperateHelper.read(file);
// 进行加密
byte[] encodeString = EncodeUtil.simpleEncrypt(oldcontent);
// 将加密后的内容,覆盖原来的文件内容
FileOperateHelper.write(encodeString, file);
System.out.println(file.getPath() + " --> entrypt done!");
}
private static void encryptFiles(File[] listFiles) throws Exception {
for (File file : listFiles) {
if (file.isDirectory()) {
encryptFiles(file.listFiles());
}else {
// 到这说明是文件,进行加密
encodeAndWriteBack(file);
}
}
}
}
将工具类所在的项目打成jar包后,用压缩包打开jar包,修改\META-INF\MANIFEST.MF文件,添加Main-Class: test.QuickStart指定运行jar时程序入口,修改后如下:
注意:Main-Class: test.QuickStart(冒号后面有一个空格,整个文件最后有一行空行)
这样,就可以很方便的对项目的class文件进行加密了。
在命令行运行指令:java -jar codeEncryptUtils.jar 完成加密。
运行效果如下:
这里示例对DataCenterController.class文件进行加密,没有加密前可以用jd-gui反编译:
加密后:
六、测试运行
根据上述方法修改完成后把JDK、Tomcat、Spring等需要替换的jar备份一下,替换时选中jar包鼠标右键选择使用rar或360压缩工具打开jar包,然后根据“二、需要修改的类”中说明的对应位置进行替换,rar会提示压缩文件已修改是否需要保存,确定保存后就好了(注意:如果直接在JDK或Tomcat目录中打开替换可能会保存失败,需要将jar包复制到其他目录,比如D盘根目录什么的,再根据上面说的方式替换保存,最后将jar包覆盖回原来的地方),最后将加密好的类替换掉原来的类(注意实体类不能加密,否则在运行过程中会报错),把配置文件中isRunDecode设置为true,启动项目看下结果吧!!!
七、局限性
1、不能加密entity(实体类),因为实体类的字节码读取解析用到了JNI调用非JAVA程序完成,能力有限,这部分还没法修改。
具体代码详见java.lang.ClassLoader类:
2、不能加密@WebService类,理由同上。
建议只对service和controller加密。
八、总结
做完下来,需要改动的地方不少,在摸索哪些地方需要修改以及如何修改着实花费了不少时间,甚至走了许多弯路,到目前为止感觉还是有很多不是很理解需要进一步研究的地方。如果你的项目用到了别的框架,可能需要改动别的地方,这里主要是提供一个思路,就不放项目链接了。如果有别的不错的方法防止反编译,则不建议这种方法,挺花时间的。