关于Java加密的相关操作

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bloodylzy/article/details/79975037

    最近被公司交付了一个新的工作。由于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来判断,这个类是不是我们加密过的类,如果是加密的类,那么我们就根据入参的字节码直接进行解密,然后返回就可以了。

猜你喜欢

转载自blog.csdn.net/bloodylzy/article/details/79975037