Spring加载加密后的文件,防止反编译

来源:互联网 发布:ubuntu anaconda安装 编辑:程序博客网 时间:2024/06/05 14:36

公司的产品需要放在合作伙伴的产品里面部署到客户那边,为了防止他方很方便地反编译我们产品,需要对其进行保护。

网上有免费的如ProGuard,收费的有 Zelix 等,公司也购买了一个加密狗什么的。公司总是希望不花钱或者花很少的钱完成某种程度的保护,甚至有点是防君子不防小人了。免费的ProGuard只是混淆工具,而且不能进行Flow Obfuscation,且不说人家Debug就能厘清代码调用逻辑,混淆后Spring容器压根就没办法起来,Spring太依赖反射了。公司的加密狗也因同一的原因不能加密Spring管理的代码。

现在几乎只能寄希望于程序员来写加密代码了,我的思路是这样的:

1. 将工程包解压后,选取工程里面若干个核心的代码进行加密,然后把加密的文件放在一个特有的路径上;同时删除被加密的.class文件。

2. 写一个解密程序,该程序拿到加密的密钥,和加密后的文件路径,逐一解密并且把文件写到它应当出现的地方。

3. 使用Spring API 将这些解密过的文件注册到 Spring 容器并实例化,然后删除这些解密的文件。

其中第 2, 3 步都是写在解密程序里面的,保证加密的文件->解密->被Spring管理这三大步骤都是在Spring容器启动的时候完成的,不给他方机会去获得解密的.class文件。

当然,最最重要的部分是我的解密程序必须被公司的加密狗加密。


当前只是测试通过,还没有优化代码,很多文件路径都使用的是绝对路径。还有就是加密解密的代码都是网上拷贝的,之所以写在这里,是给自己做笔记。

1. 加密解密的密钥:

import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.security.SecureRandom;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;public class GenerateKey {Path keyFilename = Paths.get("F:/des_key2.xml");public void generateKeyFile() throws Exception {// 生成一个可信任的随机数源SecureRandom sr = new SecureRandom();// 为我们选择的DES算法生成一个KeyGenerator对象KeyGenerator kg = KeyGenerator.getInstance ("DES" );kg.init (sr);// 生成密钥SecretKey key = kg.generateKey();// 将密钥数据保存为文件供以后使用,其中key Filename为保存的文件名Files.write(keyFilename, key.getEncoded () );}}


2. 加密需要被加密的核心.class文件,这个需要线下做了,也就是上面3个步骤中的第一步,简单点就直接main方法走起:

import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.security.SecureRandom;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.DESKeySpec;public class EncryptionDemo {public void encryptFile(Path keyFilename, Path filename) throws Exception{// 产生一个可信任的随机数源SecureRandom sr = new SecureRandom();//从密钥文件key Filename中得到密钥数据byte rawKeyData [] = Files.readAllBytes(keyFilename);// 从原始密钥数据创建DESKeySpec对象DESKeySpec dks = new DESKeySpec(rawKeyData);// 创建一个密钥工厂,然后用它把DESKeySpec转换成Secret Key对象SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES" );SecretKey key = keyFactory.generateSecret( dks );// Cipher对象实际完成加密操作Cipher cipher = Cipher.getInstance( "DES" );// 用密钥初始化Cipher对象cipher.init( Cipher.ENCRYPT_MODE, key, sr );// 通过读类文件获取需要加密的数据byte data [] = Files.readAllBytes(filename);// 执行加密操作byte encryptedClassData [] = cipher.doFinal(data );// 保存加密后的文件,覆盖原有的类文件。 Files.write( Paths.get("<Absolute_Directory>/UserControllerEncrypted.class"), encryptedClassData );}public static void main(String[] args) throws Exception {EncryptionDemo instance = new EncryptionDemo();instance.encryptFile(Paths.get("F:/des_key2.xml"), Paths.get("<Target_Directory>/UserController.class"));System.out.println("done");}}

3.  解密文件,这个文件是希望被Spring管理,并且在加载并实例化结束的时候就执行解密操作,包括注册并实例化解密后的.class文件到Spring容器,完了删除硬盘上的解密后的文件,擦掉痕迹,不能让他方能够找到解密后的文件并反编译。

import java.nio.file.Files;import java.nio.file.Paths;import java.security.SecureRandom;import javax.annotation.PostConstruct;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.DESKeySpec;import org.apache.log4j.Logger;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.support.AbstractBeanDefinition;import org.springframework.beans.factory.support.DefaultListableBeanFactory;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.stereotype.Component;@Componentpublic class Decryption {private static final Logger logger = Logger.getLogger(Decryption.class);@PostConstructpublic void decrypt() {System.out.println("开始解密文件。。。。。。。");try {SecureRandom sr = new SecureRandom();byte rawKeyData[] = Files.readAllBytes( Paths.get("F:/des_key2.xml") );DESKeySpec dks = new DESKeySpec (rawKeyData);SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES" );SecretKey key = keyFactory.generateSecret( dks );Cipher cipher = Cipher.getInstance( "DES" );cipher.init( Cipher.DECRYPT_MODE, key, sr );// 获得经过加密的数据byte encryptedData [] = Files.readAllBytes(Paths.get("<Absolute_Diretory>/UserControllerEncrypted.class"));//执行解密操作byte decryptedData [] = cipher.doFinal( encryptedData );System.out.println("解密文件并写到硬盘");Files.write( Paths.get("<Target_Directory>/UserController.class"), decryptedData );System.out.println("进入UserController.class的解密并注册到Spring容器里来。。。。。");DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();AbstractBeanDefinition user = new RootBeanDefinition(UserController.class);user.setScope(BeanDefinition.SCOPE_SINGLETON);  //将bean定义注册到容器中beanRegistry.registerBeanDefinition("user", user);System.out.println("成功注册UserController.class到Spring容器里。。。。。");logger.debug("删除解密后的文件");Files.delete(Paths.get("<Target_Directory>/UserController.class"));} catch (Exception e) {throw new RuntimeException("解密文件报错。");} }}

在写这个博客的时候,还没有使用公司的加密狗加密这个解密程序并测试是否能工作。但是如果不行,只能希望 F:/des_key2.xml 里面的密钥可以被加密。

 

UserController.class被加密后的文件名,我使用的是UserControllerEncrypted.class,但它实际上已经是一个MalFormed的二进制文件,所以tomcat在启动的时候会报错:

07-Dec-2016 16:51:38.626 SEVERE [localhost-startStop-1] org.apache.catalina.startup.ContextConfig.processAnnotationsWebResource Unable to process web resource [/WEB-INF/classes/UserControllerEncrypted.class] for annotations org.apache.tomcat.util.bcel.classfile.ClassFormatException: It is not a Java .class file        at org.apache.tomcat.util.bcel.classfile.ClassParser.readID(ClassParser.java:202)        at org.apache.tomcat.util.bcel.classfile.ClassParser.parse(ClassParser.java:80)        at org.apache.catalina.startup.ContextConfig.processAnnotationsStream(ContextConfig.java:2042)        at org.apache.catalina.startup.ContextConfig.processAnnotationsWebResource(ContextConfig.java:1940)        at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1147)        at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:779)        at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:306)        at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:95)        at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)        at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5202)        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:147)        at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:725)        at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:701)        at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:717)        at org.apache.catalina.startup.HostConfig.deployDirectory(HostConfig.java:1092)        at org.apache.catalina.startup.HostConfig$DeployDirectory.run(HostConfig.java:1834)        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)        at java.util.concurrent.FutureTask.run(FutureTask.java:266)        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)        at java.lang.Thread.run(Thread.java:745)

但是这个不影响 tomcat 的启动,Srping 会起来的,并且会加载并实例化 Bean 。期间还碰到一个问题,有没有加密解密过,@PathVariable 的使用会不同的。

没有被加密过,下面的方法签名和Spring的注解联合使用没问题:

@RequestMapping(value="{personId}/summary",method=RequestMethod.GET)@ResponseBodypublic BaseResponse <Map<String, List<LinkedHashMap<String, LinkedHashMap<String, String>>>>> getPersonSummary(@PathVariable Integer personId){

但是加密过,并且解密后,他是不能工作的,会报下面的错误:

[org.springframework.core.LocalVariableTableParameterNameDiscoverer]-[DEBUG] Cannot find '.class' file for class [class <package>.UserController] - unable to determine constructor/method parameter names

修改下注解的使用方式,增加一个 value 属性给 @PathVariable 就可以工作了,暂时没搞清楚为什么。

@RequestMapping(value="{personId}/summary",method=RequestMethod.GET)@ResponseBodypublic BaseResponse <Map<String, List<LinkedHashMap<String, LinkedHashMap<String, String>>>>> getPersonSummary(@PathVariable("personId") Integer personId){

==========================2016/12/08 更新=======================

我们的程序是 Spring + Maven Module 化实现的,核心逻辑还是放在Service层,所以必须对核心的Service加密。但是 Service 会被其他Service和Controller依赖,不 像Controller 那么简单,Service之间的依赖关系决定着Spring 对 Bean 的加载顺序。假使我需要对 BService 进行加密,而AService依赖BService,按照上面的实现方案,可能AService先于Decryption类被Spring加载,必然找不到需要的BService。

我实在找不到一个办法,能让Decryption先于所有其他的Service先被Spring发现并实例化。只能找到一个插入点,那就是Tomcat启动后,Spring启动前。于是我在 web.xml 里面注册一个 ServletContextListener ,这个listener 里面写解密程序,并且注册在Spring的ContextLoaderListener的上面。

<listener><listener-class><Package_Name>.DecryptionListener</listener-class></listener> <listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener>

对应的DecryptionListener 程序:

public class DecryptionListener implements ServletContextListener {private static final Logger logger = Logger.getLogger(DecryptionListener.class);@Overridepublic void contextInitialized(ServletContextEvent sce) {logger.debug("---------------------------contextInitialized in DecryptionListener");decrypt("<KEY_STR>", "<Project_Deploy_Dir>/WEB-INF/classes/CDServiceEncrypted.dat", "<Project_Deploy_Dir>/WEB-INF/classes/<package>/CDService.class");logger.debug("------------------------contextInitialized end .");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {logger.debug("contextDestroyed in AAADecryption");}public void decrypt(String keyStr, String fileInput, String fileOutput)  {try {DESKeySpec dks = new DESKeySpec(keyStr.getBytes());SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES" );SecretKey key = keyFactory.generateSecret( dks );Cipher cipher = Cipher.getInstance( "DES" );cipher.init( Cipher.DECRYPT_MODE, key );FileInputStream fis2 = new FileInputStream(fileInput);FileOutputStream fos2 = new FileOutputStream(fileOutput);CipherOutputStream cos = new CipherOutputStream(fos2, cipher);doCopy(fis2, cos);logger.debug("FINISH DECRYPTION.......................");} catch (Exception e) {throw new RuntimeException("error when decrypt file.");}}public static void doCopy(InputStream is, OutputStream os) throws IOException {byte[] bytes = new byte[64];int numBytes;while ((numBytes = is.read(bytes)) != -1) {os.write(bytes, 0, numBytes);}os.flush();os.close();is.close();}}

公司的加密狗也有诸多限制,就是被加密的 contextInitialized 里面不能有 Exception 的处理,所以把Exception的处理挪到了decrypt() 方法里。

为了解密后的.class文件尽快被删除,我就把删除代码写到了对应的java文件里,使用 @PostConstruct 注解让其在Spring 实例化完之后马上执行。

public class CDService {@PostConstructpublic void destoryClass() {try {Files.delete(Paths.get("<Project_Deploy_Dir>/WEB-INF/classes/<package>/CDService.class"));} catch (IOException e) {throw new RuntimeException("error when delete class file.");}}




0 0
原创粉丝点击