Encrypted Class Files
By Greg Travis
Most people are concerned about the user's privacy on the Web, but what about the developer's privacy? With traditional languages like C or C++, it's easy to protect your source codejust don't distribute it.
Unfortunately, code written in Java is particularly easy to "borrow." With a decompiler, anyone can look inside your code and use it for their own purposes. Fortunately, the very flexibility that makes Java easy to steal also makes it relatively easy to protect using encryption.
All it takes is an understanding of Java's
ClassLoader object. Of course, if you're going to work with encryption, you should learn about the Java Cryptography Extension (JCE) as well.
There are ways to obscure class files so that decompilers have trouble processing them. It isn't hard to fix a decompiler so that it can process these tainted class files, however. You can't simply rely on security through obscurity.
Of course, you could encrypt your application using a popular encryption tool such as Pretty Good Privacy (PGP) or GNU Privacy Guard (GPG). End users would have to decrypt it before they can run it. Once they do, they then have a decrypted copy of your class files, and you're no better off than before.
Fortunately, Java provides a mechanism for loading (and potentially modifying) bytecode at runtime. Each class file that the JVM loads requires an object called a
ClassLoader, which is responsible for loading the new class into a running JVM. The JVM gives the
ClassLoader a string containing the name of a class to load, such as
ClassLoader is then responsible for locating the class, loading the raw data, and turning it into a
By creating a custom
ClassLoader, you can modify the class file data just before execution. This opens up many possibilitiesin our case, we'll use it to decrypt the class files right before they're loaded into the system. Think of it as a just-in-time decrypter. Because the decrypted bytecodes are never stored on the computer's file system, it's very difficult for snoopers to read the decrypted codes.
Luckily, creating a custom
ClassLoader doesn't require any magic, because the process of converting raw bytes into a
Class object is something the system handles. You only have to acquire the raw bytes, and then you can use any transformation that deals with raw bytesincluding decryption (see "
A running JVM already has a
ClassLoader. This default
ClassLoader looks for raw class bytes in the local filesystem according to the settings of the
CLASSPATH environment variable.
Using a custom
ClassLoader involves taking charge of this process to some degree. For it to work, your code must first create an instance of your custom
ClassLoader class, and then explicitly ask it to load another class. This forces the JVM to assign that class, and all other classes it needs, to your custom
Listing 1 demonstrates how you use a
ClassLoader to load a class file.
As I mentioned earlier, your
ClassLoader only has to acquire the raw bytes of a class file. After that you can pass the bytes to the runtime system, which takes care of the rest.
ClassLoader has several important methods. To create a custom
ClassLoader, you have to override only one of them
loadClasswith code to acquire raw class file bytes. This method receives two arguments: the class name, and a flag that indicates whether the JVM wants you to resolve the class (that is, also load dependent classes). If this flag is true, you simply call
resolveClass before returning to the JVM.
Listing 2 shows a simple
loadClass implementation. Most of this code is the same for all
ClassLoader objects. Only a small amount of the code (marked by comments) is unique. You may use several other methods in the
ClassLoader object to assist you in this process:
findLoadedClass object can check to make sure the requested class isn't already present. In your
loadClass method, you should call this first.
defineClass. Once you've acquired the raw class file bytes, you call
defineClass to magically turn the bytes into a
Class object. You'll need to call this in any implementation of
findSystemClass object provides access to the default
ClassLoader. If your custom method for loading classes fails to find a particular class (or chooses not to find it), you can call this method to try the default approach. This is useful, in particular, for loading the standard Java classes from the usual JAR files.
resolveClass. When the JVM wants to load not just a given class, but also all other classes referred to by the given class, it tells you so by setting the
resolve parameter of
true. Then, you must call
resolveClass just before you return your loaded
Class object to the caller.
Java Cryptography Extension
The JCE is Sun's package for cryptographic services including encryption and key generation. It's an extension of the Java Cryptography Architecture (JCA), and provides support for encryption technologies (subject to export control regulations).
The JCE doesn't provide a particular encryption algorithm, but rather provides a framework into which cryptographic implementations can be added as service providers. In addition to the JCE framework, the JCE software includes the SunJCE service provider, which contains numerous useful encryption algorithms including Data Encryption Standard (DES) and Blowfish.
For our purposes, we aren't concerned with algorithm strength or export regulations. To keep things simple, we'll use the DES algorithm to encrypt and decrypt our bytecodes. As always when employing cryptography, you'll want to read up on your local regulations before you ship an actual product.
Here are the basic steps you'll follow in this application to encrypt and decrypt data with JCE:
Step 1: Generating a Secret Key. Before you can encrypt or decrypt anything, you need to generate a secret key. This is a small piece of data you'll distribute with your encrypted application that lets it decrypt itself before execution.
Listing 3 shows how to generate a key.
Step 2: Encrypting Data. Once you have your key, you can use it to encrypt some bytes. In addition to your decrypting
ClassLoader, you'll make a stand-alone tool to encrypt your application before distribution (see
Step 3: Decrypting Data. When it's time to run your encrypted application, the
ClassLoader steps in and decrypts the class files (see
Listing 5 for the steps).
Deploying the System
Now that you know how to encrypt and decrypt raw bytes, follow these steps to deploy an encrypted application.
Step 1: Creating the Application. In our example, we'll work with a sample application consisting of a main class called
App, and two auxiliary classes called
Bar. This application doesn't really do anything, but if you can encrypt it, you could encrypt any application.
Step 2: Generating a Secret Key. At the command line, use the
GenerateKey tool (see the code for
GenerateKey.java) to write the key to a file:
% java GenerateKey key.data
Step 3: Encrypt the Application. At the command line, use the
EncryptClasses tool (see the code for
EncryptClasses.java) to encrypt the application classes:
% java EncryptClasses key.data App.class Foo.class Bar.class
This replaces each .class file with an encrypted version of itself.
Step 4: Run the Encrypted Application. The user runs the encrypted application using a shell program called DecryptRun (see
Listing 6). The unencrypted version of the application is normally run like this:
% java App arg0 arg1 arg2
To run the encrypted version, do this instead:
% java DecryptRun key.data App arg0 arg1 arg2
DecryptRun serves two purposes. An instance of
DecryptRun is the custom
ClassLoader that does the on-the-fly decryption.
DecryptRun also contains a
main routine that creates this instance and then uses it to load and run the application.
Bar.java contain the classes for the dummy application used in our example, called App. Also take a look at the code for
Util.java, a file I/O utility class used throughout the example code.
A Word of Caution
You've now seen how a Java application can be easily adapted to decrypt itself without modifying the original application's source code. But of course, there's no such thing as a completely secure system. This scheme provides reasonable protection, but it's vulnerable to some attacks.
Even though the application itself is encrypted, the stub program is not. An intruder could decompile the stub program and modify it to save the decrypted class files to disk. One way to mitigate this risk is to apply high-quality obfuscation to the stub program. Or, for even more security, the stub program could use native code, which would give it the security of a traditional executable format.
Yet, remember that most JVMs themselves are not secure. A wily hacker could potentially compile a modified JVM that subverted our security by grabbing the decrypted data right out of our
ClassLoader and saving it to a file. Java provides no real remedy for this.
Note, however, that both of these potential attacks require that the key be available to the hacker. Without the key, your application's security rests entirely on the security of the algorithm you've used for the encryption. Protecting your code in this way isn't perfect, but it can be an effective solution for safeguarding your intellectual property, and for keeping valuable user data private.
(Get the source code for this article here.)
Greg is a freelance Java programmer and technology writer living in New York City. After spending three years in the world of high-end PC games, he joined EarthWeb, where he developed new technologies with the then-new Java programming language. Since 1997, he has been a consultant in a variety of Web technologies. Contact him at firstname.lastname@example.org.