您的位置:首页 > 编程语言 > Java开发

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

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

网上有免费的如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;

@Component
public class Decryption {
private static final Logger logger = Logger.getLogger(Decryption.class);

@PostConstruct
public 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.processAnnotationsWebR
esource 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)
@ResponseBody
public BaseResponse <Map<String, List<LinkedHashMap<String,
LinkedHashMap<String, String>>>>> getPersonSummary(@PathVariable Integer personId){


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

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


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

@RequestMapping(value="{personId}/summary",method=RequestMethod.GET)
@ResponseBody
public 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);

@Override
public 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 .");
}

@Override
public 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 {

@PostConstruct
public 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.");
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息