preface

Recently I was talking to a friend who was working on an outsourcing project, and he asked me if THERE was any way I could keep my source code from being decompiled and cracked, so I told him I could obfuscate and encrypt the code. Proguard-maven-plugin can be configured directly using proGuard-Maven-plugin, which is relatively simple

Code anti – compile integral routine

1. Write encryption tool classes

@Slf4j
public class EncryptUtils {

    private static String secretKey = "test123456lyb-geek"+System.currentTimeMillis();

    private EncryptUtils(a){}


    public static void encrypt(String classFileSrcPath,String classFileDestPath) {
        System.out.println(secretKey);
        FileInputStream fis = null;
        FileOutputStream fos = null;

        try {
            fis = new FileInputStream(classFileSrcPath);
            fos = new FileOutputStream(classFileDestPath);
            int len;
            String[] arrs = secretKey.split("lyb-geek");
            long key = Long.valueOf(arrs[1]);
            System.out.println("key:"+key);
            while((len = fis.read())! = -1) {byte data = (byte)(len + key + secretKey.length()); fos.write(data); }}catch (Exception e) {
           log.error("encrypt fail:"+e.getMessage(),e);
        }finally {
            if(fis ! =null) {try {
                    fis.close();
                } catch(IOException e) { e.printStackTrace(); }}if(fos ! =null) {try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}
Copy the code

2. Encrypt the code needed to prevent decompilation

 public static void main(String[] args) {
        String classFileSrcPath = classFileSrcPath("UserServiceImpl");
        System.out.println("classFileSrcPath:--->"+classFileSrcPath);
        String classFileDestDir = ServiceGenerate.class.getClassLoader().getResource("META-INF/services/").getPath();
        System.out.println("classFileDestDir:--->"+classFileDestDir);
        String classFileDestPath = classFileDestDir + "com.github.lybgeek.user.service.impl.UserServiceImpl.lyb";
        EncryptUtils.encrypt(classFileSrcPath,classFileDestPath);
    }
Copy the code

3. Decompile and verify the encryption code

Open the decompression tool JD-GUI and drag the encrypted code into JD-GUI

If you can’t open it, at least you can’t decompilate encrypted code using JD-GUI.

We open the normal compiled class file, which looks like thisWe can probably tell a few things from the content, like the name of the package. When you open the encrypted file, the content is as followsThe content is like a book from heaven

Thought 1: How does the JVM recognize when the code is encrypted?

Answer: Since there is encryption, naturally can be used by decryption. So where does this decryption go for decryption?

Those of you who know anything about class loading know that Java class files are loaded into JVM memory through the class loader, so we can consider putting decryption in the class loader. Common class loaders include boot class loaders, extension class loaders, and system class loaders. Classes in our normal classpath path are loaded through the system class loader. Unfortunately, the loaders provided by the three JDKS didn’t meet our needs. So we have to implement our class loader ourselves. The custom loader code is as follows

@Slf4j
public class CustomClassLoader extends ClassLoader{

    /** * Authorization code */
    private String secretKey;

    private String SECRETKEY_PREFIX = "lyb-geek";


    /** * the root directory of the class file */
    private String classRootDir = "META-INF/services/";

    public CustomClassLoader(String secretKey) {
        this.secretKey = secretKey;
    }


    public String getClassRootDir(a) {
        return classRootDir;
    }

    public void setClassRootDir(String classRootDir) {
        this.classRootDir = classRootDir;
    }

    @Override
    protectedClass<? > findClass(String name)throwsClassNotFoundException { Class<? > clz = findLoadedClass(name);// Check if the class has been loaded. If it has been loaded, it returns the loaded class directly. If not, a new class is loaded.
        if(clz ! =null) {return clz;
        }else{
            ClassLoader parent = this.getParent();
            clz = getaClass(name, clz, parent);

            if(clz ! =null) {return clz;
            }else{ clz = getaClass(name); }}return clz;

    }

    privateClass<? > getaClass(String name)throwsClassNotFoundException { Class<? > clz;byte[] classData = getClassData(name);
        if(classData == null) {throw new ClassNotFoundException();
        }else{
            clz = defineClass(name, classData, 0,classData.length);
        }
        return clz;
    }

    privateClass<? > getaClass(String name, Class<? > clz, ClassLoader parent) {try {
            // Delegate to the parent class load
            clz = parent.loadClass(name);
        } catch (Exception e) {
           //log.warn("parent Load class fail: "+ LLDB etMessage(),e);
        }
        return clz;
    }

    private byte[] getClassData(String classname){
        if(StringUtils.isEmpty(secretKey) || ! secretKey.contains(SECRETKEY_PREFIX) || secretKey.split(SECRETKEY_PREFIX).length ! =2) {throw new RuntimeException("secretKey is illegal");
        }
        String path = CustomClassLoader.class.getClassLoader().getResource("META-INF/services/").getPath() +"/"+ classname+".lyb";
        InputStream is = null;
        ByteArrayOutputStream bas = null;
        try{
            is  = new FileInputStream(path);
            bas = new ByteArrayOutputStream();
            int len;
            / / decryption
            String[] arrs = secretKey.split(SECRETKEY_PREFIX);
            long key = Long.valueOf(arrs[1]);
          // System.out.println("key:"+key);
            while((len = is.read())! = -1) {byte data = (byte)(len - key - secretKey.length());
                bas.write(data);
            }
            return bas.toByteArray();
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }finally{
            try {
                if(is! =null){ is.close(); }}catch (IOException e) {
                log.error("encrypt fail:"+e.getMessage(),e);
            }
            try {
                if(bas! =null){ bas.close(); }}catch(IOException e) { e.printStackTrace(); }}}}Copy the code

You can invoke it as follows

 public static void main(String[] args) throws Exception{
        CustomClassLoader customClassLoader = new CustomClassLoader("test123456lyb-geek1603895713759");
        Class clz = customClassLoader.loadClass("com.github.lybgeek.user.service.impl.UserServiceImpl");
        if(clz ! =null){
            Method method = clz.getMethod("list", User.class);
            method.invoke(clz.newInstance(),newUser()); }}Copy the code

Thought 2: How do classes loaded with custom loaders integrate with Spring?

Answer: IOC container injection via extension points provided by Spring

Write the bean definition and register the registered bean definition

@Component
public class ServiceBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Object secretKey = YmlUtils.getValue("lyb-geek.secretKey");
        if(ObjectUtils.isEmpty(secretKey)){
            throw new RuntimeException("secretKey can not be null,you maybe need to config in application.yml with key lyb-geek.secretKey");
        }
        registerBean(beanFactory,secretKey.toString());

// setClassLoader(beanFactory,secretKey.toString());
    }

    /** * If >spring-boot-devtools is introduced in the project, The default loader is org. Springframework. Boot. Devtools. Restart. This. RestartClassLoader * at this time if you use the loader, Change the class loader for the bean to AppClassLoader *@param beanFactory
     */
    private void setClassLoader(ConfigurableListableBeanFactory beanFactory,String secretKey) {

        CustomClassLoader customClassLoader = new CustomClassLoader(secretKey);
        beanFactory.setBeanClassLoader(customClassLoader.getParent());
    }

    private void registerBean(ConfigurableListableBeanFactory beanFactory,String secretKey){
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(UserService.class);
        GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
        definition.getPropertyValues().add("serviceClz",UserService.class);
        definition.getPropertyValues().add("serviceImplClzName"."com.github.lybgeek.user.service.impl.UserServiceImpl");
        definition.getPropertyValues().add("secretKey", secretKey); definition.setBeanClass(ServiceFactoryBean.class); definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE); String beanId = StringUtils.uncapitalize(UserService.class.getSimpleName()); defaultListableBeanFactory.registerBeanDefinition(beanId, definition); }}Copy the code

2. For interface injection, FactoryBean is also used to replace the cat with the cat

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceFactoryBean <T> implements FactoryBean<T>,ApplicationContextAware.InitializingBean {

    private ApplicationContext applicationContext;

    private Class<T> serviceClz;

    private String serviceImplClzName;

    private String secretKey;

    private Object targetObj;

    @Override
    public T getObject(a) throws Exception {
        return (T) targetObj;
    }

    @Override
    publicClass<? > getObjectType() {return serviceClz;
    }

    @Override
    public void afterPropertiesSet(a) throws Exception {
        targetObj = ServiceFactory.create(secretKey,serviceImplClzName,applicationContext);

    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext; }}Copy the code

3. Verify whether the integration is successful

Validate sample code

@RestController
@RequestMapping("/user")
public class UserController {


    @Autowired
    private UserService userService;

    @PostMapping(value = "/save")
    public User save(User user){
		User newUser = userService.save(user);
		returnnewUser; }}Copy the code

If the output is normal, the integration is successful

conclusion

The examples above are just an idea, but they don’t completely prevent code from being decompiled. Because if you really want to decompile, you can actually decompile your custom class loader, and then decrypt it to reverse the encryption algorithm and restore the encryption class. There are several ways to prevent code from being decompiled

  • Increase the cost of decompilation, such as re-encrypting custom class loads and writing complex encryption algorithms

  • Write code that makes decompiling less desirable, like writing a bunch of junk code

The demo link

Github.com/lyb-geek/sp…