Dr. Dobb's Journal February 1999
Every nontrivial Java application requires multiple class files, but dealing with them can be a pain. One problem with multiple class files, for instance, involves installation. Installation of any software, not just a Java application, often requires registry keys and sometimes additional environment variables. Java applications are especially prone to classpath issues, and sometimes even class packaging incompatibilities, such as those between zip and cab files.
A second problem with multiple class files is that Java makes it all too easy for users to decompile and reuse an application's classes. Each class you create takes some measure of time and money. However, typical Java packaging methodologies allow others to steal your work.
One way to get around both problems is to create zip files. However, zip files often require users to modify the classpath. Secondly, zip files are not encrypted, so users can unzip files and decompile or use your classes.
The only real solution to both problems, therefore, is to build a custom encryption and packaging system and implement it as a self-extracting, Java-executable file. Even though the default Java class loader looks to the classpath to find a given class file, a class can override the default loader and control how and where additional classes are found and loaded. This lets you create any packaging method you wish. In this article, we'll present CodePacker (available electronically; see "Resource Center," page 5), a custom loader that is both easy to install (it's self-extracting) and secure.
This project consists of two distinct parts, the codepacker executable and a template file called container.class. The codepacker executable takes a list of all the user defined classes, and combines them with the container.class into a single, self-extracting executable class file.
For example, "Java codepacker container.class username.class depend1.class depend2.class main.class" would produce a single class file called "username.class." This class file (username.class), when executed, would load and run the user's main class, which is the last one listed (main.class). Naturally, username.class would also load the dependent classes, shown here as depend1.class and depend2.class. A Java class file is a specific format defined for the portability of object code from one implementation of a virtual machine (VM) to another.
A class file consists of a number of fields and counted arrays, most of them containing variable length items. The main portions are the constant pool, interface list, field list, method list, and attribute list. Additionally, there's the "this" index, a fixed-size field between the constant pool and interface list.
The constant pool, like all the lists, starts with a count. Then it has a number of items, each starting with a 1-byte tag. Depending on the type, the data in that constant pool item may be of fixed size, or it may have a byte count itself. The particular items we're interested in here are of the CONSTANT_Utf8 type, which consists of counted UTF8 strings. UTF8 encoding is a way of representing Unicode such that ASCII characters only take up one byte each, while nonASCII values may take one, two, or three bytes.
Throughout the rest of the file, when an index is used, it usually refers to a particular item in the constant pool. Especially in the case of strings, this avoids repetitious usage of the same thing throughout the file, and makes much of the rest of the data a fixed size.
The first index we use is the "this" index, which simply describes the name of the class file with which you're dealing. Since you can let users rename the Container class, you have to change the string that the index points to or change the index to point to another string.
There are several predefined attributes that describe the Java class. For example, one attribute is Code, which contains the byte code and a few other fields. The VM will only process attributes that it recognizes. Consequently, you can add a new attribute and not affect the behavior of the application.
Being able to manipulate the class-file format is critical to the notion of a self-extracting class file. The next step is to execute a contained class's main method.
The Java reflection classes let you load, query, and invoke class methods. Using the reflection classes, container classes are able to detect which classes have a main method.
However, detecting which class has a main method isn't enough. A Java application may have several classes that have main methods. Often these extra main methods contain unit-testing code. Consequently, there needs to be an additional way for the user to specify the main executable class for the self-executing class. The application can do this by either having a command line option or requiring the user to order the list of classes in such a way that the main class is distinct from supporting classes.
Invoking the main class is rather trivial. The application must first query the class via the java.lang.Class.getDeclaredMethod() method to find the main method. This method takes two parameters, a string representing the method, and an array of parameters. To find the main method, the application would call getDeclaredMethod() with a string of "main" and an array containing a String array.
Once you have found the main method, you simply invoke it. The getDeclaredMethod() returns a java.lang.reflect.Method object. From the main of the self-executing class, we call the Method.invoke() method with the String array using the same parameters passed to us from the command line. Control then passes to the encrypted executable class's main() method.
Packaging a list of class files into a data file is trivial. However, what if the class list's data was partially encrypted? What if various sections of the class list used different encryption methods? Now the problem of how to handle this data becomes much more complex.
Before answering these questions, we need to set a few ground rules. First, assume the user will specify a list of class files; however, that list will include class files that encrypt and decrypt data. Second, the application will not use incremental encryption. Instead, the application switches from one encryption method to another. Third, only class-file data is encrypted. Fourth, the main class file has some special attributes to distinguish it from other class files.
These rules define the structure
int type;
int length;
byte[] encrypted_class_file;
that will represent a single class file. The type field tells how to handle the class record. This value indicates if the class is the main class, an encryption/decryption class, or just a typical support class. The length field represents the length of the encrypted_class_file field. The bytes of the encrypted_class_file field hold an encrypted class file. An array of these structures represents a list of encrypted classes. This tells how we are going to take a list and convert it into a list of encrypted classes; the second problem is where to put them. There are several ways to load a class into a VM. We will choose a way that current decompilers will not support.
You can create a new attribute containing the encrypted class file list and write the data to some other class file. The VM will simply ignore the class list data and execute the class file as usual. However, the application can read its own class file, decode the class file list, and load the resulting classes directly into the VM.
CodePacker is a Java application that encrypts a list of class files into another class. Users specify an ordered list of class files to encrypt, and a target name for the resulting class file.
The entire application is dependent on only three class files. The CodePacker.class file is responsible for loading, encrypting, and writing a list of class files to an attribute in another class.
The second class, Container.class, is the base class for all encryption/decryption classes and extends the ClassLoader abstract class. This class first exposes two static methods, encrypt() and decrypt(). These methods are null place holders. By extending from the ClassLoader class, the Container class is able to load classes into the VM. The Container.class file is also responsible for the decoding and loading of stored class files, and invoking the main class file.
The third class is the encryption/decryption class file, derived from Container. The Container class's encrypt and decrypt methods do nothing. Consequently, it is necessary to override the Container class encrypt/decrypt method for the application to use an encryption method.
The application is broken down into four phases: packaging, encryption/decryption, loading, and execution.
Packaging. The class packaging method is the method this application uses. The application simply builds a ByteArrayOutputStream object and writes the class bundle to the list (see Listing One). As the application works through the class list, it is able to switch between encryption methods by examining the class file list and searching for methods with a signature matching the Container class's encrypt() method.
Once the array of class data is built, the application writes the data to the target class file in 48 KB blocks. An attribute may be as large as 64 KB, however, this gives the application a little more leeway should the encrypted class structure require additional fields.
After the class array is written to the Container class, an additional attribute is added to the end of the attribute table. This added attribute specifies the beginning index of the attribute table for the class file. This will simplify loading by allowing the application to simply read the last few bytes of the class file and move to the offset it specifies. This is much faster than reading the entire class file and interpreting each element of every table.
However, many other modifications also occur. First, attributes require a string constant in the constant pool. This string must uniquely identify the attribute so that the VM can distinguish custom attributes from those the VM should try to interpret. Another important change is that the user is able to specify a target class name. This means that the name of the Container class must be a changeable option. This will require the CodePacker to add a new entry to the constant pool specifying the new name, and update the "this" field to point to the new name index.
The safest way to perform all of these steps without destroying the original file is to read in the entire class and rewrite it with all of its changes and additions.
Encryption/Decryption. Once you are unpacking the class files, you can also decrypt them. The interface to the encryption and decryption methods is simple enough that you can substitute more complex algorithms of your choosing. Each method takes a byte array, and returns a (different) byte array. The only requirement is that the decryption method be able to invert whatever the encryption method does.
Looking at Listing Two (Encrypt.java), we see a single class with two methods. It would have been better to break it into two separate classes, but it's simpler to describe this way. The principle used is that one can easily find a pair of 32-bit integers that are inverses of each other, assuming normal twos-complement overflow. In other words, when you multiply them together, the result will equal 0x00000001. It also turns out, with simple algebra, that if you multiply any other value by one of them, then by the other, you wind up with the original number. In other words, if we call these inverses magic1 and magic2, then (X*magic1) * magic2 == X for all possible values of X.
Next, we introduce a random 24-bit seed (extracted just before starting encrypt, and stored in an int). For each byte of the buffer, we merge the byte with the seed (to get a 32-bit value), multiply by magic, then extract one byte from the result, saving the other three in seed. Repeat for all the bytes of the buffer, and then add three more bytes of seed to the end of the buffer. At this point, the buffer has grown by 3 bytes, and contains all the information from the original buffer, plus the information of the randomly chosen seed. Of course, this information is nicely scrambled by the magic of our multiply scheme.
Much later, when the loader invokes our decrypt method, we reverse the process. Using the inverse magic value, we start by extracting the final seed value from the last three bytes of the buffer. Then we work backwards through the buffer, applying the same algorithm (but with the inverse magic value). When we're done, we have the original seed value (which we discard), and the precise buffer we started with. The buffer is returned to the caller, who hands it to the VM as a class file image.
We now know everything necessary to store a list of class files.
Loading. To execute the contained application, users merely run the target Container class. The class begins by opening its own class file as an ordinary data file. It begins by going to the last few bytes. This portion of the class file indicates the location of the first class data attribute. The application can then seek that location and read in all the class data.
Once all the data is read in, the application rebuilds the data array and interprets the data. The data is a mix of encrypted data. However, it follows the format where the first integer specifies the type of class. The second integer specifies the length of the data. The third integer specifies the size of the encrypted data. The application calls the decryption routine, and loads the resulting class image into the VM.
Executing. The last step is to execute the main class file. Once all the classes are loaded into the VM, the application invokes the contained application's main method, passing the command line parameters from the Container class. The contained application will execute and behave as though it were loaded by the normal VM's class loader.
CodePacker demonstrates that it is possible to build a self-extracting, encrypted Java application. By using a combination of encryption and class file manipulation, it is possible to create a single class file that contains all the component classes of an application.
The next step would be to add features to manage the intelligent loading of system-dependent libraries, resources (bitmaps, icons, and so on), and class files that are specifically geared to a given platform. As a side benefit, it is also possible to use a Container class file as a package library, giving the package some protection from those who decompile your work.
DDJ
public void build ( ) {
byte[] encryptedClasses = readClassFiles( );
writeClassFile( m_destination, m_newName, encryptedClasses );
}
protected byte[] readClassFiles( )
{
CodePackerClassLoader cl = new CodePackerClassLoader();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( bout );
for ( int i = 0; i < m_pFiles.length; i++ )
{
try
{
Method c = null;
byte[] classBytes = cl.readClassFile ( m_pFiles[i], true );
c = cl.getEncrypt( stripExtension ( m_pFiles[i] ) );
if ( m_crypto != null )
{
System.out.println ( "Encrypting something " +
classBytes.length );
classBytes = (byte[])m_crypto.invoke( null,
new Object[] { classBytes } );
System.out.println ( "Encrypted something " +
classBytes.length );
}
if ( c != null )
{
System.out.println ( "Switching cryptography method" );
m_crypto = c; // switch to new crypto class
out.writeByte( 1 ); // crypto class
} else if ( i == ( m_pFiles.length - 1 ) )
{
// is this the last file in the list
// main class is always last class in list
out.writeByte( 2 );
} else
{
out.writeByte( 0 ); // do nothing special with the class
}
out.writeInt( classBytes.length);
out.write( classBytes );
} catch ( Exception e )
{
System.out.println ( e.getMessage() + " occured in build " );
}
}
return bout.toByteArray();
}
protected String stripExtension( String className )
{
if ( className.endsWith( ".class" ) == true )
{
return className.substring(0, className.length() -
".class".length() );
}
return className;
}
public void writeClassFile ( String name, String newName, byte[] bytes )
{
try
{
RandomAccessFile inFile = new RandomAccessFile( name,"r" );
RandomAccessFile outFile = new RandomAccessFile( newName, "rw" );
// write magic number, major and minor version info
int magic = inFile.readInt();
int minor = inFile.readUnsignedShort();
int major = inFile.readUnsignedShort();
outFile.writeInt( magic );
outFile.writeShort( minor );
outFile.writeShort( major );
// write the attribute table
int constCount = inFile.readUnsignedShort();
outFile.writeShort( constCount + 2 );
int codePackerAttributeIndex = constCount;
long[] offsetTable = new long[ codePackerAttributeIndex ] ;
short j = 0;
for ( j=0; j < (constCount-1); j++ )
{
int type = inFile.readUnsignedByte();
outFile.writeByte( type );
switch ( type )
{
case 1:// utf-8
int len = inFile.readUnsignedShort();
byte[] utf8bytes = new byte[len];
inFile.read ( utf8bytes );
//System.out.println ( (j+1) + " UTF8 " +
new String(utf8bytes) );
outFile.writeShort( len );
outFile.write( utf8bytes );
break;
case 3: // int
case 4: // float;
outFile.writeInt(inFile.readInt());
break;
case 5: // long
case 6: // double
outFile.writeLong(inFile.readLong());
j++;
break;
case 7: // class
case 8: // string
if ( type == 7 )
{
int tmp = inFile.readShort();
//System.out.println ( (j+1) +
" Class utf8 at index " + tmp );
offsetTable[ j+1 ] = outFile.getFilePointer();
outFile.writeShort ( tmp );
} else
{
outFile.writeShort(inFile.readShort());
}
break;
case 9: // field
case 10: // method
case 11: // interface
case 12: // name and type
outFile.writeInt(inFile.readInt());
break;
default:
System.out.print ( "Unknown type" );
break;
}
}
// write a new UTF8 object for the new class name
int classNameIndex = j+1;
outFile.writeByte( (byte) 1 );
String tmpName = stripExtension ( newName );
outFile.writeShort( tmpName.getBytes().length );
outFile.write( tmpName.getBytes() );
// write the type, length and new attribute info
int customAttributeIndex = j+2;
outFile.writeByte( (byte) 1 );
outFile.writeShort( "CodePackerCustomAttribute".
getBytes().length );
outFile.write( "CodePackerCustomAttribute".getBytes() );
/* Attribute table is now written */
// write access flags
outFile.writeShort( inFile.readUnsignedShort() ); // access Flags
// replace this index with classConstIndex to represent new name
int thisIndex = inFile.readUnsignedShort(); // this index
outFile.writeShort( thisIndex );
long currentPosition = outFile.getFilePointer();
outFile.seek( offsetTable[ thisIndex ] );
outFile.writeShort ( classNameIndex );
outFile.seek ( currentPosition );
// write the super class
outFile.writeShort( inFile.readUnsignedShort() );
// write the interface table
int interface_count = inFile.readUnsignedShort();
byte[] interfaceBuffer = new byte[ interface_count * 2 ];
inFile.read( interfaceBuffer );
outFile.writeShort( interface_count );
outFile.write ( interfaceBuffer );
// write the field table
writeFieldMethodTable( inFile, outFile );
// write the method table
writeFieldMethodTable( inFile, outFile );
// write attribute table
long attributesOffset = outFile.getFilePointer();
System.out.println ( "Attribute Offset at " + attributesOffset );
int attributes_count = inFile.readUnsignedShort();
outFile.writeShort ( attributes_count );
// write exisiting attributes
for ( int k = 0; k < attributes_count; k++ )
{
writeAttributes( inFile, outFile );
}
// write class data attributes
int attributesAdded = writeAttributeRecords( outFile,
customAttributeIndex, bytes );
// write attribute to indicate start of attribute table
outFile.writeShort ( customAttributeIndex );
outFile.writeInt ( 9 );
outFile.writeByte ( (byte) 1 );
outFile.writeLong ( attributesOffset );
attributesAdded++;
// set the attribute count with the new attributes
outFile.seek ( attributesOffset );
outFile.writeShort( attributes_count + attributesAdded );
System.out.println ( "attributes: " + ( attributes_count +
attributesAdded ) );
} catch ( Exception e )
{
e.printStackTrace();
} }
import java.util.*;public class Encrypt extends Container
{
public static int seed; //note, it's really three bytes
public static int magic;
public static byte scrabble(byte value)
{
int temp = seed | (value<<24);
temp *= magic;
seed = temp & 0xffffff;
temp >>>= 24;
return (byte)temp;
}
public static byte[] encrypt(byte[] bytes)
{
System.out.println ( "Encrypt.encrypt() " + bytes.length );
//Given an array of bytes, produce another array of bytes,
// somewhat longer, that are an encoding of that array
int size = bytes.length;
byte[] buffer = new byte[size+3];
seed = (int)(16000000.*java.lang.Math.random());
magic = 653216881;
for (int i=0; i<size; ++i)
{
buffer[i] = scrabble(bytes[i]);
}
buffer[size] = (byte)(seed>>16);
buffer[size+1] = (byte)(seed>>8);
buffer[size+2] = (byte)seed;
return buffer;
}
public static byte[] decrypt(byte[] bytes)
{
//Given an encoded array of bytes, produce another array of bytes,
// somewhat shorter, that are the original set
magic = 1278932113;
int size = bytes.length-3;
byte[] buffer = new byte[size];
seed = bytes[size]&255;
seed = (seed<<8) | (bytes[size+1]&255);
seed = (seed<<8) | (bytes[size+2]&255);
for (int i=size-1; i>=0; --i)
{
buffer[i] = scrabble(bytes[i]);
}
return buffer;
}
};