最近被公司交付了一个新的工作。由于java的可编译性,导致将jar包进行反编译非常容易,为了保证公司代码不被泄露,我的任务就是将jar包进行加密,然后在主程序调用的时候,实时地将加密后的jar解密,再被主程序调用。
说上去好像很绕,其实就是说,我要完成三个包的编写,被加密的jar(Test.jar),用于解密的jar(Decode.jar),用于加密的jar(Encode.jar)。
这里就是算是给自己做一个记录。这里是一个简单的demo。首先是Test.jar。这里就是先弄一个最简单的方法,能够被调用就行
public class Hello { public void say(){ System.out.println("这是加密后的内容"); } }
这样就行了。其次是用于加密的jar。老实说,这个jar我写出来,自己都感觉很挫,但是没找到好的方法,不会百度,我真的是个假的程序员。
首先是决定加密的方式。个人选择的加密方式是RSA的加密。首先是定义出自己的公私钥
public class RSAHelp {
public static PublicKey getPublicKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = (new BASE64Decoder()).decodeBuffer(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
public static PrivateKey getPrivateKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = (new BASE64Decoder()).decodeBuffer(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs = hs + "0" + stmp;
} else {
hs = hs + stmp;
}
}
return hs.toUpperCase();
}
public static String getKeyString(Key key) throws Exception {
byte[] keyBytes = key.getEncoded();
String s = (new BASE64Encoder()).encode(keyBytes);
return s;
}
public static void main(String[] args) throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
PublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
PrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
PrivateKey privateKey2 = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = getKeyString(publicKey);
System.out.println("public:\n" + publicKeyString);
String privateKeyString = getKeyString(privateKey);
//String privateKeyString2 = getKeyString(privateKey2);
System.out.println("private:\n" + privateKeyString);
//System.out.println("private2:\n" + privateKeyString2);
Cipher cipher = Cipher.getInstance("RSA");//Cipher.getInstance("RSA/ECB/PKCS1Padding");
String textStr = "您好";
byte[] plainText = textStr.getBytes();
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] enBytes = cipher.doFinal(plainText);
System.out.println("------------");
//System.out.println(DESHelper.byte2hex(enBytes));
publicKey = getPublicKey(publicKeyString);
privateKey = getPrivateKey(privateKeyString);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] deBytes = cipher.doFinal(enBytes);
publicKeyString = getKeyString(publicKey);
System.out.println("public:\n" + publicKeyString);
privateKeyString = getKeyString(privateKey);
System.out.println("private:\n" + privateKeyString);
String s = new String(deBytes);
System.out.println(s);
}
}
执行main方法,就可以得到公私钥。
得到了公私钥之后,接下来的工作,就是针对jar包,进行加密了。
对于加密工作,我们还需要准备好两个类,一个是文件操作工具类类,一个则是加密工具类,还有一个是读取工具类
文件操作如下
/**
* @author LiuZhengyang
* @since 2018/4/13
*/
public class FileOperateUtil {
public static void setLogPath(String logPath) {
FileOperateUtil.logPath = logPath;
}
// private static Logger logger = Logger.getLogger(FileOperateUtil.class);
private static String logPath = "";
public static String getLogPath() {
return logPath;
}
/**
* 压缩文件
*
* @param zipFilePath 压缩的文件完整名称(目录+文件名)
* @param srcPathName 需要被压缩的文件或文件夹
*/
public void compressFiles(String zipFilePath, String srcPathName) {
File zipFile = new File(zipFilePath);
File srcdir = new File(srcPathName);
if (!srcdir.exists()) {
throw new RuntimeException(srcPathName + "不存在!");
}
Project prj = new Project();
FileSet fileSet = new FileSet();
fileSet.setProject(prj);
if (srcdir.isDirectory()) { //是目录
fileSet.setDir(srcdir);
fileSet.setIncludes("*"); //包括哪些文件或文件夹 eg:zip.setIncludes("*.java");
//fileSet.setExcludes(...); //排除哪些文件或文件夹
} else {
fileSet.setFile(srcdir);
}
Zip zip = new Zip();
zip.setProject(prj);
zip.setDestFile(zipFile);
zip.setEncoding("gbk"); //以gbk编码进行压缩,注意windows是默认以gbk编码进行压缩的
zip.addFileset(fileSet);
zip.execute();
// logger.debug("---compress files success---");
}
/**
* 解压文件到指定目录
*
* @param // packageNames 包名
* @param //zipFile 目标文件
* @param //descDir 解压目录
* @author isDelete 是否删除目标文件
*/
@SuppressWarnings("unchecked")
public void unZipFiles(String zipFilePath, String fileSavePath, boolean isDelete) {
FileOperateUtil fileOperateUtil = new FileOperateUtil();
boolean isUnZipSuccess = true;
try {
(new File(fileSavePath)).mkdirs();
File f = new File(zipFilePath);
if ((!f.exists()) && (f.length() <= 0)) {
throw new RuntimeException("not find " + zipFilePath + "!");
}
//一定要加上编码,之前解压另外一个文件,没有加上编码导致不能解压
ZipFile zipFile = new ZipFile(f, "utf-8");
String gbkPath, strtemp;
Enumeration<ZipEntry> e = zipFile.getEntries();
while (e.hasMoreElements()) {
ZipEntry zipEnt = e.nextElement();
gbkPath = zipEnt.getName();
strtemp = fileSavePath + File.separator + gbkPath;
if (zipEnt.isDirectory()) { //目录
File dir = new File(strtemp);
if (!dir.exists()) {
dir.mkdirs();
}
continue;
} else {
// 读写文件
InputStream is = zipFile.getInputStream(zipEnt);
BufferedInputStream bis = new BufferedInputStream(is);
// 建目录
String strsubdir = gbkPath;
for (int i = 0; i < strsubdir.length(); i++) {
if (strsubdir.substring(i, i + 1).equalsIgnoreCase("/")) {
String temp = fileSavePath + File.separator
+ strsubdir.substring(0, i);
File subdir = new File(temp);
if (!subdir.exists())
subdir.mkdir();
}
}
System.out.println("gbkPath: " + gbkPath);
if (!gbkPath.endsWith("class")) {
System.out.println("startemp: " + strtemp);
FileOutputStream fos = new FileOutputStream(strtemp);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int len;
byte[] buff = new byte[5120];
while ((len = bis.read(buff)) != -1) {
bos.write(buff, 0, len);
}
bos.close();
fos.close();
} else {
System.out.println("startemp: " + strtemp);
long length = zipEnt.getSize();
byte classComm[] = new byte[(int) length];
int r = bis.read(classComm);
if (r != length) {
throw new IOException("Only read " + r + " of " + length + " for " + length);
}
System.out.println(RSAHelp.byte2hex(classComm));
EncryptClass encryptClass = new EncryptClass();
byte[] encryptedData = encryptClass.encrypt(classComm);
Util.writeFile(strtemp, encryptedData);
//Util.writeFile(strtemp, classComm);
}
}
}
zipFile.close();
} catch (Exception e) {
// logger.error("解压文件出现异常:", e);
isUnZipSuccess = false;
System.out.println("extract file error: " + zipFilePath);
fileOperateUtil.WriteStringToFile(fileOperateUtil.logPath, "extract file error: " + zipFilePath);
}
/**
* 文件不能删除的原因:
* 1.看看是否被别的进程引用,手工删除试试(删除不了就是被别的进程占用)
2.file是文件夹 并且不为空,有别的文件夹或文件,
3.极有可能有可能自己前面没有关闭此文件的流(我遇到的情况)
*/
if (isDelete && isUnZipSuccess) {
boolean flag = new File(zipFilePath).delete();
// logger.debug("删除源文件结果: " + flag);
fileOperateUtil.WriteStringToFile(fileOperateUtil.logPath, "delete " + zipFilePath + "result: " + flag);
}
// logger.debug("compress files success");
}
/**
* 删除指定文件夹下所有文件
* param path 文件夹完整绝对路径
*
* @param path
* @return
*/
public static boolean delAllFile(String path) {
System.out.println(path);
boolean flag = false;
File file = new File(path);
if (!file.exists()) {
return flag;
}
if (!file.isDirectory()) {
return flag;
}
String[] tempList = file.list();
File temp = null;
for (int i = 0; i < tempList.length; i++) {
if (path.endsWith(File.separator)) {
temp = new File(path + tempList[i]);
} else {
temp = new File(path + File.separator + tempList[i]);
}
if (temp.isFile()) {
temp.delete();
}
if (temp.isDirectory()) {
delAllFile(path + "/" + tempList[i]);// 先删除文件夹里面的文件
boolean success = (new File(path + "/" + tempList[i])).delete();
flag = success;
}
}
return flag;
}
/**
* 复制单个文件
*
* @param oldPath String 原文件路径 如:c:/fqf.txt
* @param newPath String 复制后路径 如:f:/fqf.txt
* @return boolean
*/
public void copyFile(String oldPath, String newPath) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
File file = new File(newPath);
if (!file.exists()) {
file.mkdirs();
}
bis = new BufferedInputStream(new FileInputStream(oldPath));
bos = new BufferedOutputStream(new FileOutputStream(newPath));
int hasRead = 0;
byte b[] = new byte[2048];
while ((hasRead = bis.read(b)) > 0) {
bos.write(b, 0, hasRead);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 复制整个文件夹内容
*
* @param oldPath String 原文件路径 如:c:/fqf
* @param newPath String 复制后路径 如:f:/fqf/ff
* @return boolean
*/
public void copyFolder(String oldPath, String newPath) {
System.out.println("copy path: " + oldPath);
try {
(new File(newPath)).mkdirs(); //如果文件夹不存在 则建立新文件夹
File a = new File(oldPath);
String[] file = a.list();
File temp = null;
for (int i = 0; i < file.length; i++) {
if (oldPath.endsWith(File.separator)) {
temp = new File(oldPath + file[i]);
} else {
temp = new File(oldPath + File.separator + file[i]);
}
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
FileOutputStream output = new FileOutputStream(newPath + "/" +
(temp.getName()).toString());
byte[] b = new byte[5120];
int len;
while ((len = input.read(b)) != -1) {
output.write(b, 0, len);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {//如果是子文件夹
copyFolder(oldPath + "/" + file[i], newPath + "/" + file[i]);
}
}
} catch (Exception e) {
System.out.println("复制整个文件夹内容操作出错");
e.printStackTrace();
}
}
/**
* 写内容到指定文件
*
* @param filePath
* @param content
*/
public void WriteStringToFile(String filePath, String content) {
try {
FileWriter fw = new FileWriter(filePath, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(content + "\r\n");// 往已有的文件上添加字符串
bw.close();
fw.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
加密操作如下
/**
* @author LiuZhengyang
* @since 2018/4/13
*/
public class EncryptClass {
/**
* 公钥,这个我就不放出来了,你们反正之前拿到了自己的公私钥,自己往上面套
*/
private static final String PUBLICKEY = "";
/**
* 最大长度
*/
private static final Integer MAX_ENCRYPT_BLOCK = 117;
/**
* 根据传入的classComm,返回加密后的classComm
* @param classComm
* @return
* @throws Exception
*/
public byte[] encrypt(byte[] classComm) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
PublicKey publicKey = RSAHelp.getPublicKey(PUBLICKEY);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//RSA的加密有长度限制,所以对大的byte[],进行分段
int inputLen = classComm.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int j = 0;
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(classComm, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(classComm, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
j++;
offSet = j * MAX_ENCRYPT_BLOCK;
}
return out.toByteArray();
}
}
读取工具如下
/**
* @author LiuZhengyang
* @since 2018/4/10
*/
public class Util {
// 把文件读入byte数组
static public byte[] readFile(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte data[] = new byte[(int)len];
FileInputStream fin = new FileInputStream(file);
int r = fin.read(data);
if (r != len){
throw new IOException("Only read "+r+" of "+len+" for "+file);
}
fin.close();
return data;
}
static public void writeFile(String filename, byte data[]) throws IOException {
FileOutputStream fout = new FileOutputStream(filename);
fout.write(data);
fout.close();
}
public static String readFileAllText(String filename, String charsetName) {
BufferedReader br;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), charsetName));
String line;
String text = "";
while ((line = br.readLine()) != null) {
text += line+"\n";
}
br.close();
return text;
} catch (Exception ex) {
System.out.println("私钥读取失败");
}
return "";
}
}
工具类都准备好以后,就可以开始main方法了,不过这个main太挫了,希望大家给个意见
public class Execute {
public static void main(String[] args) throws Exception {
FileOperateUtil fileOperateUtil = new FileOperateUtil();
//这里加入你们自己要加密的jar包所在的地址
//String rootPath = "D:\\work\\jar";
String rootPath = args[0];
if ("".equals(rootPath) || rootPath == null) {
System.out.println("please input extract path:");
fileOperateUtil.WriteStringToFile(FileOperateUtil.getLogPath(), "please input extract path:");
}
if (rootPath.endsWith("\\")) {
rootPath = rootPath.substring(0, rootPath.length() - 1);
}
FileOperateUtil.setLogPath(rootPath + "\\extractLog.txt");
long startTime = System.currentTimeMillis();
long endTime = 0;
System.out.println("extract path: " + rootPath);
fileOperateUtil.WriteStringToFile(FileOperateUtil.getLogPath(), "extract path: " + rootPath);
Execute execute = new Execute();
File dir = new File(rootPath);
File[] files = dir.listFiles();
if (files != null) {
int filesNum = files.length;
for (File file : files) {
String fileName = file.getName();
if (!fileName.endsWith(".svn") && !fileName.endsWith("extractLog.txt") && !fileName.endsWith("target")) {
fileOperateUtil.WriteStringToFile(FileOperateUtil.getLogPath(), "copy path: " + rootPath + "\\" + fileName);
String JarName = file.getName();
if (!file.isDirectory() && (JarName.endsWith(".jar") || JarName.endsWith(".zip"))) { // 判断文件名是否以.jar结尾
String sourceFilePath = file.getAbsolutePath();
System.out.println("extract file: " + sourceFilePath);
fileOperateUtil.WriteStringToFile(fileOperateUtil.getLogPath(), "extract file: " + sourceFilePath);
String dirName = JarName.substring(0, JarName.length() - 4);
String resultPath = sourceFilePath.replace(JarName, dirName);
fileOperateUtil.unZipFiles(sourceFilePath, resultPath, false);
}
}
}
}
endTime = System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间:" + (endTime - startTime) / 1000 + "s"); //输出程序运行时间
fileOperateUtil.WriteStringToFile(fileOperateUtil.getLogPath(), "程序运行时间:" + (endTime - startTime) / 1000 + "s");
}
}
本来想着通过JarFile和JarEntery这两个,来做到加密jar包,但是有个很大的问题就是,我们可以获得inputStream,但是却是拿不到outputStream。也就是说我们只能读,不能写(可能有,但是我真的百度不到)。
所以这里只能用这个很蠢的方法。将jar包解压出来,针对拿到的每一个class,在解压的时候,就直接加密,也就是我们可以得到一个已经加密完了的jar包解压后的文件夹。那么之后,我们只需要通过Jar的命令,再将这个文件夹重新压缩回Jar包就可以了
所以最后一次吐槽,这个方法显得很蠢,请大家给个好意见
现在距离我们完成任务已经达成了2/3了,还差一个用于解密的包(Decode.jar)就完成了。
这里的解密我想了很久,因为我的解密是建立在ClassLoad的基础上面的。关于ClassLoad我这里就不详细解释了,解释起来太麻烦,而且我个人理解也不到位,讲出来只会误人子弟。
简单一点,我们JVM就是将class文件,读取为字节码,在经过验证,准备,解析三个步骤之后,再将类初始化。那么我的解密,必须要在读取之后,验证之前。
本来我的想法是自定义一个类加载器,用我的方式去加载我的加密类。但是想来想去,找来找去,我都没有发现如何让JVM去使用我的自定义类加载器,所以关于这点,也希望dalao能告诉一下我怎么做。毕竟用main很简单,但是实际项目都是放到tomcat,weblogic这种容器当中去运行的,在这种容器里面去定义使用自定义类加载器,个人想不通该怎么用(感觉已经快要变成求助帖了)
这个时候,我们老大给了我一个提示,javaagent。
关于这个东西,大家可以去了解一下。这是java虚拟机启动的时候,添加的一个参数,具体写法为
-javaagent:<jarpath>[=<options>]
简单来说,参数后面定义的jar(举例叫它为Agent.jar)里面,如果有一个类(举例叫他Agent.java)中有premain方法,并且在Agent.jar的MANIFEST.MF里面写明了这个类是哪个,那么在main方法启动之前,就会优先加载Agent.jar中的Agent.java中的premain方法。
一开始我的想法是在premain里面使用我的自定义类加载器,让我的被加密的Jar包(Test.jar)里面的类全部加载一遍,那么在JVM里面的方法区里面就会有相关的记录,那么再次调用Test.jar里面的类的时候,就不会再去读取class文件了,而是直接读取方法区里面的类,然后在堆里面创建实例。因为这个想法我没有具体去实现,所以可行性方面,请dalao们帮忙想想行不行,麻烦告诉我可行性
至于我为什么没有去具体实现我上面的想法,是因为我发现了一个更好的方法。当然,这个方法我个人是已经实现并且成功了的。
首先是关注premain方法。该方法有两个实现方式
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
当两个方法都存在的时候,只会执行第一个方法。
然后解密的关键就是第一个方法上面的inst这个参数上面。
进入Instrumentation之后,我们可以发现它是一个接口。我们具体要使用的方法是addTransFormer
这里我们就直接贴出代码好了
public class Agent {
static private Instrumentation _inst = null;
public static void premain(String agentArgs, Instrumentation inst) {
/* Provides services that allow Java programming language agents to instrument programs running on the JVM.*/
_inst = inst;
/* ClassFileTransformer : An agent provides an implementation of this interface in order to transform class files.*/
ClassFileTransformer readClass = new ReadClass();
System.out.println("Adding a ReadClass instance to the JVM.");
ClassFileTransformer classCheck = new AllClassCheck();
/*Registers the supplied transformer.*/
//_inst.addTransformer(classCheck);
_inst.addTransformer(readClass);
}
public static void premain(String agentArgs){
}
}
可以看到这个addTransFormer方法需要的参数类型是ClassFileTransformer,而这个ClassFileTransformer又是一个接口,在代码中,我们定义了一个叫ReadClass的类,它是直接去实现ClassFileTransFormer。我们可以去看看它要我们去实现什么东西。
public class AllClassCheck implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("这里是AllClassCheck");
System.out.println("ClassLoad : " + loader);
System.out.println("ClassName : "+className);
return classfileBuffer;
}
}
我们这里有一个类AllClassCheck,它实现了ClassFileTransformer接口,这里要求我们实现的方法就是transform。我们可以看到这个方法的入参,有ClassLoad,有className,有classfileBuffer,这就正好符合了我们心目中的想法。
那么我们这里就可以去通过ClassName来判断,这个类是不是我们加密过的类,如果是加密的类,那么我们就根据入参的字节码直接进行解密,然后返回就可以了。