magazine resources subscribe about advertising

 

 

 








 CD Home < Web Techniques < 2001 < August  

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 code—just 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.

Why Encrypt?

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 java.lang.Object. The ClassLoader is then responsible for locating the class, loading the raw data, and turning it into a Class object.

By creating a custom ClassLoader, you can modify the class file data just before execution. This opens up many possibilities—in 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 bytes—including decryption (see " Java 2's ClassLoader Shortcut").

Taking Over

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 ClassLoader. 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—loadClass—with 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. The 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 loadClass.

findSystemClass. The 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 loadClass to 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.

Using JCE

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 Listing 4).

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 Foo and 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. App.java, Foo.java, and 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 mito@panix.com.




Copyright © 2003 CMP Media LLC