Search code examples
javaencryptionclassloaderbukkit

Java Bukkit load .jar file from InputStream


Hello Stackoverflow community,

i have a question about java and bukkit specific. I have a encrypted plugin and i dont want to save the decrypted file on disk. So i used InputStream. But my problem now is how to inject this file into minecraft ( bukkit ). Is there any Custom Classloader available ? I searched alot but i dont find any working solution.

I decrypt an encrypted plugin with AES-128:

    FileInputStream fin; 
    CipherInputStream cin;
    int nread = 0;
    byte [] inbuf = new byte [MAX_FILE_BUF];

    fin = new FileInputStream (input);
    cin = new CipherInputStream (fin, mDecipher);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    while ((nread = cin.read (inbuf)) > 0 )
    {
        byte[] trimbuf = new byte [nread];
        for (int i = 0; i < nread; i++)
        {
            trimbuf[i] = inbuf[i];
        }
        baos.write(trimbuf);
    }

So now i tried to load the file with InputStream

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    InputStream is2 = new ByteArrayInputStream(baos.toByteArray());
    JarInputStream in = new JarInputStream(is2);

This is working well at this point. Now i want to load this InputStream "is2" into bukkit server.


Solution

  • Because the way class loading works in java, you can not directly load a class from a inputstream like you have. What you can do, is:

    • Load every file from the jar to the memory
    • Use a custom classloader that loads the files from the memory
    • Use that classloader to load your plugin

    The first step is easy, we are going to make a Map<String,byte[]> for the files that come from the jar file.

    Map<String,byte[]> map = new HashMap<>();
    ZipEntry entry;
    byte[] read = new byte[1024];
    while((entry = in.getNextEntry()) != null) {
        ByteArrayOutputStream r = new ByteArrayOutputStream(Math.max(128, entry.getSize()));
        int i;
        while((i = in.read(read) >= 0)
            r.write(read, 0, i);
        is.close();
        map.put(entry.getName(), r.toByteArray());
    }
    

    Having our map of filename -> byte array of data, we can then implement our custom class loader:

    public class MappedJarClassLoader extends ClassLoader {
         Map<String,byte[]> map = new HashMap<>();
    
         public MappedJarClassLoader (ClassLoader parent, Map<String,byte[]> map) {
             super(parent);
             this.map = map;
         }
    
         public Class findClass(String name) throws ClassNotFoundException {
             byte[] b = map.get(name.replace('/', '$').replace('.', File.separatorChar));
             if(b == null)
                 throw new ClassNotFoundException(name);
             return defineClass(name, b, 0, b.length);
         }
    }
    

    Because of the estrictions in bukkit, we cannot place the class extending JavaPlugin inside our encrypted part of the jar. Instead of doing that, place a class inside the encrypted jar that has a constructor that accepts the main instance, and load the class in the onEnable using:

    mappedJarClassLoader.loadClass("path.to.new.class").getConstructor(getClass()).newInstance(this)