Use encryption technology to protect Java source code
Content:
1. Why encryption?
Ii. Custom Class Loader
Iii. encryption and decryption
4. Application Instances
5. Notes
Reference resources
The source code of the Java program is easily peeked at by others. Anyone can analyze others' code as long as there is an anti-compiler. This article discusses how to protect source code without modifying the original program.
1. Why encryption?
For traditional C or C ++ languages, it is easy to protect the source code on the Web, as long as it is not released. Unfortunately, the source code of the Java program is easily peeked at by others. Anyone can analyze others' code as long as there is an anti-compiler. Java's flexibility makes the source code easily stolen, but at the same time, it also makes it easier to protect the Code through encryption. The only thing we need to know is the Java classloader object. Of course, in the encryption process, knowledge about Java Cryptography Extension (JCE) is also essential.
There are several techniques that can be used to blur Java-class files, greatly reducing the effect of anti-compiler processing class files. However, it is not difficult to modify the anti-compiler so that it can process these fuzzy-processed class files. Therefore, we cannot simply rely on Fuzzy Technology to ensure source code security.
We can use popular encryption tools to encrypt applications, such as PGP (pretty good privacy) or GPG (GNU Privacy Guard ). In this case, the end user must decrypt the application before running it. However, after decryption, the end user has a non-encrypted class file, which is no different from the pre-encryption.
The mechanism of importing bytecode to Java runtime implies that the bytecode can be modified. Every time the JVM loads a class file, it needs an object called classloader, which loads the new class into the running JVM. JVM provides classloader with a class to be loaded (such as Java. lang. object) Name string, then the classloader is responsible for finding the class file, loading the original data, and converting it into a class object.
You can customize classloader to modify it before executing the class file. This technology is widely used-here, it is used for decryption when class files are loaded, so it can be seen as an instant encryptor. Since the decrypted bytecode file will never be stored in the file system, it is difficult for the hacker to obtain the decrypted code.
Because the system is fully responsible for converting the original bytecode into a class object, it is not difficult to create a custom classloader object. You only need to obtain the original data first, then, you can perform any conversions including decryption.
Java 2 simplifies the creation of custom classloader to a certain extent. In Java 2, the default Implementation of loadclass is still responsible for processing all the necessary steps, but to take into account various customized class loading processes, it also calls a new findclass method.
This provides a shortcut for writing a custom classloader, which reduces the hassle of overwriting findclass rather than loadclass. This method avoids repeated public steps that must be performed by all loaders, because this is the responsibility of loadclass.
However, this method is not used for customizing classloader in this article. The reason is simple. If the default classloader first looks for the encrypted class file, it can find it; but because the class file is encrypted, it does not recognize this class file, the loading process will fail. Therefore, we must implement loadclass by ourselves, slightly increasing the workload.
Ii. Custom Class Loader
Each running JVM already has a classloader. This default classloader searches for appropriate bytecode files in the local file system based on the value of the classpath environment variable.
Application customization classloader requires a deep understanding of this process. First, we must create an instance that customizes the classloader class and explicitly require it to be loaded into another class. This forces JVM to associate the class and all the classes it needs to the custom classloader. Listing 1 shows how to mount class files with custom classloader.
[Listing 1: using a custom classloader to load class files]
// First create a classloader object
Classloader myclassloader = new myclassloader ();
// Use a custom classloader object to load class files
// And convert it into a class Object
Class myclass = myclassloader. loadclass ("mypackage. myclass ");
// Finally, create an instance of this class
Object newinstance = myclass. newinstance ();
// Note that all other classes required by myclass will pass
// Custom automatic classloader Loading
As mentioned above, custom classloader only needs to get the data of the class file first, and then pass the bytecode to the runtime system. The latter completes the remaining tasks.
Classloader has several important methods. When creating a custom classloader, we only need to overwrite one of them, that is, loadclass, and provide code for getting the data of the original class file. This method has two parameters: the class name and a flag indicating whether the JVM needs to parse the class name (that is, whether the dependent class is loaded at the same time ). If this flag is true, we only need to call resolveclass before returning the JVM.
[Listing 2: A simple implementation of classloader. loadclass]
Public class loadclass (string name, Boolean resolve)
Throws classnotfoundexception {
Try {
// The Class Object we want to create
Class clasz = NULL;
// Required Step 1: If the class is already in the system buffer,
// We do not have to load it again
Clasz = findloadedclass (name );
If (clasz! = NULL)
Return clasz;
// The following is the custom part.
Byte classdata [] =/* obtain bytecode data in some way */;
If (classdata! = NULL ){
// The bytecode data is successfully read, and now it is converted into a class Object
Clasz = defineclass (name, classdata, 0, classdata. Length );
}
// Required Step 2: If the above is not successful,
// We try to mount it with the default classloader
If (clasz = NULL)
Clasz = findsystemclass (name );
// Required Step 3: mount the related class if necessary
If (resolve & clasz! = NULL)
Resolveclass (clasz );
// Return the class to the caller
Return clasz;
} Catch (ioexception IE ){
Throw new classnotfoundexception (ie. tostring ());
} Catch (generalsecurityexception GSE ){
Throw new classnotfoundexception (GSE. tostring ());
}
}
Listing 2 shows a simple loadclass implementation. Most of the Code is the same for all classloader objects, but a small part (marked with comments) is unique. During processing, the classloader object requires several other auxiliary methods:
Findloadedclass: used to check whether the requested class does not exist. The loadclass method should be called first.
Defineclass: after obtaining the bytecode data of the original class file, call defineclass to convert it into a class object. This method must be called for any loadclass implementation.
Findsystemclass: supports the default classloader. If you cannot find the specified class (or intentionally do not need to create a custom method), you can call this method to try the default loading method. This is useful, especially when loading standard Java classes from common jar files.
Resolveclass: When the JVM wants to load not only the specified class, but also all other classes referenced by the class, it sets the resolve parameter of loadclass to true. At this time, we must call resolveclass before returning the newly loaded Class Object to the caller.
Iii. encryption and decryption
Java encryption extension is Java Cryptography Extension (JCE. It is Sun's encryption service software, which includes encryption and key generation functions. JCE is an extension of JCA (Java Cryptography Architecture.
JCE does not specify a specific encryption algorithm, but provides a framework. The specific implementation of the encryption algorithm can be added as a service provider. In addition to the JCE framework, the JCE package also contains sunjce service providers, including many useful encryption algorithms, such as des (Data Encryption Standard) and blowfish.
In this article, we will use the DES algorithm to encrypt and decrypt bytecode. The following are the basic steps that must be followed to encrypt and decrypt data using JCE:
Step 1: generate a security key. You must have a key before encrypting or decrypting any data. The key is a small piece of data published along with the encrypted application. Listing 3 shows how to generate a key. [Listing 3: generate a key]
// The DES algorithm requires a trusted random number Source
Securerandom sr = new securerandom ();
// Generate a keygenerator object for the selected DES algorithm
Keygenerator kg = keygenerator. getinstance ("des ");
Kg. INIT (SR );
// Generate the key
Secretkey key = kg. generatekey ();
// Obtain the key data
Byte rawkeydata [] = key. getencoded ();
/* You can use the key to encrypt or decrypt it, or save it
For future use of files */
Dosomething (rawkeydata );
Step 2: encrypt data. After obtaining the key, you can use it to encrypt data. In addition to the decrypted classloader, there is usually an independent program to encrypt the application to be released (see Listing 4 ). [Listing 4: encrypt the original data with a key]
// The DES algorithm requires a trusted random number Source
Securerandom sr = new securerandom ();
Byte rawkeydata [] =/* obtain the key data in some way */;
// Create an eyspec object from the original key data
Deskeyspec DKS = new deskeyspec (rawkeydata );
// Create a key factory and use it to convert the keyspec
// A secretkey object
Secretkeyfactory keyfactory = secretkeyfactory. getinstance ("des ");
Secretkey key = keyfactory. generatesecret (DKS );
// The cipher object actually completes the encryption operation
Cipher cipher = cipher. getinstance ("des ");
// Use the key to initialize the cipher object
Cipher. INIT (Cipher. encrypt_mode, key, Sr );
// Now, obtain and encrypt the data
Byte data [] =/* Get data in some way */
// Perform the encryption operation
Byte encrypteddata [] = cipher. dofinal (data );
// Further process encrypted data
Dosomething (encrypteddata );
Step 3: decrypt the data. When an encrypted application is run, classloader analyzes and decrypts class files. The procedure is shown in listing 5. [Listing 5: decrypt data with a key]
// The DES algorithm requires a trusted random number Source
Securerandom sr = new securerandom ();
Byte rawkeydata [] =/* obtain the original key data in some way */;
// Create an eyspec object from the original key data
Deskeyspec DKS = new deskeyspec (rawkeydata );
// Create a key factory and use it to convert the keyspec object
// A secretkey object
Secretkeyfactory keyfactory = secretkeyfactory. getinstance ("des ");
Secretkey key = keyfactory. generatesecret (DKS );
// The cipher object actually completes the decryption operation
Cipher cipher = cipher. getinstance ("des ");
// Use the key to initialize the cipher object
Cipher. INIT (Cipher. decrypt_mode, key, Sr );
// Now, obtain and decrypt the data
Byte encrypteddata [] =/* obtain encrypted data */
// Perform the decryption operation.
Byte decrypteddata [] = cipher. dofinal (encrypteddata );
// Further process the decrypted data
Dosomething (decrypteddata );
4. Application Instances
This section describes how to encrypt and decrypt data. To deploy an encrypted application, follow these steps:
Step 1: Create an application. Our example contains an app main class and two helper classes (FOO and bar respectively ). This application does not have any practical functions, but as long as we can encrypt this application, it will be useless to encrypt other applications.
Step 2: generate a security key. In the command line, use the generatekey tool (see generatekey. Java) to write the key into a file: % Java generatekey. Data
Step 3: encrypt the application. Use the encryptclasses tool (see encryptclasses. Java) to encrypt the application class: % Java encryptclasses key. data app. Class Foo. Class bar. Class
This command replaces each. Class file with their respective encrypted versions.
Step 4: run the encrypted application. You can use decryptstart to run encrypted applications. The decryptstart program is shown in Listing 6. [Listing 6: decryptstart. Java, start the program of the encrypted application]
Import java. Io .*;
Import java. Security .*;
Import java. Lang. Reflect .*;
Import javax. crypto .*;
Import javax. crypto. spec .*;
Public class decryptstart extends classloader
{
// These objects are set in the constructor,
// The loadclass () method will use them to decrypt the class later
Private secretkey key;
Private cipher;
// Constructor: Set the object required for decryption
Public decryptstart (secretkey key) throws generalsecurityexception,
Ioexception {
This. Key = key;
String algorithm = "des ";
Securerandom sr = new securerandom ();
System. Err. println ("[decryptstart: Creating cipher]");
Cipher = cipher. getinstance (algorithm );
Cipher. INIT (Cipher. decrypt_mode, key, Sr );
}
// Main process: Read the key here to create the decryptstart
// Instance, which is our custom classloader.
// After classloader is set, we use it to load the application instance,
// Finally, we call the main method of the application instance through the Java reflection API.
Static public void main (string ARGs []) throws exception {
String keyfilename = ARGs [0];
String appname = ARGs [1];
// These are parameters passed to the application itself
String realargs [] = new string [args. Length-2];
System. arraycopy (ARGs, 2, realargs, 0, argS. Length-2 );
// Read the key
System. Err. println ("[decryptstart: Reading key]");
Byte rawkey [] = util. readfile (keyfilename );
Deskeyspec DKS = new deskeyspec (rawkey );
Secretkeyfactory keyfactory = secretkeyfactory. getinstance ("des ");
Secretkey key = keyfactory. generatesecret (DKS );
// Create a decrypted classloader
Decryptstart DR = new decryptstart (key );
// Create an instance of the main application class
// Load it using classloader
System. Err. println ("[decryptstart: loading" + appname + "]");
Class clasz = dr. loadclass (appname );
// Finally, call the application instance through the reflection API
// Main () method
// Obtain a reference to main ().
String proto [] = new string [1];
Class mainargs [] = {(New String [1]). getclass ()};
Method main = clasz. getmethod ("Main", mainargs );
// Create an array containing the main () method parameters
Object argsarray [] = {realargs };
System. Err. println ("[decryptstart: Running" + appname + ". Main ()]");
// Call main ()
Main. Invoke (null, argsarray );
}
Public class loadclass (string name, Boolean resolve)
Throws classnotfoundexception {
Try {
// The Class Object we want to create
Class clasz = NULL;
// Required Step 1: If the class is already in the System Buffer
// We do not have to load it again
Clasz = findloadedclass (name );
If (clasz! = NULL)
Return clasz;
// The following is the custom part.
Try {
// Read encrypted class files
Byte classdata [] = util. readfile (name + ". Class ");
If (classdata! = NULL ){
// Decrypt...
Byte decryptedclassdata [] = cipher. dofinal (classdata );
//... Convert it into a class
Clasz = defineclass (name, decryptedclassdata,
0, decryptedclassdata. Length );
System. Err. println ("[decryptstart: decrypting class" + name + "]");
}
} Catch (filenotfoundexception fnfe ){
}
// Required Step 2: If the above is not successful
// We try to mount it with the default classloader
If (clasz = NULL)
Clasz = findsystemclass (name );
// Required Step 3: mount the related class if necessary
If (resolve & clasz! = NULL)
Resolveclass (clasz );
// Return the class to the caller
Return clasz;
} Catch (ioexception IE ){
Throw new classnotfoundexception (ie. tostring ()
);
} Catch (generalsecurityexception GSE ){
Throw new classnotfoundexception (GSE. tostring ()
);
}
}
}
For unencrypted applications, the normal execution method is as follows: % Java app arg0 arg1 arg2
For encrypted applications, the corresponding running mode is: % Java decryptstart key. data app arg0 arg1 arg2
Decryptstart has two purposes. A decryptstart instance is a custom classloader that implements instant decryption. In addition, decryptstart also contains a main process, which creates an encryptor instance and uses it to load and run applications. The sample app code is included in APP. Java, foo. Java, and bar. java. Util. Java is a file I/O tool. It is used in multiple examples in this article. Complete code can be downloaded at the end of this article.
5. Notes
We can see that it is easy to encrypt a Java application without modifying the source code. However, there is no completely secure system in the world. The encryption method in this article provides source code protection to a certain extent, but it is vulnerable to some attacks.
Although the application itself is encrypted, The decryptstart program is not encrypted. Attackers can decompile the Startup Program and modify it to save the decrypted class file to the disk. One way to reduce this risk is to perform high-quality fuzzy processing on the Startup Program. Alternatively, you can use the Code directly compiled into the machine language to enable the security of the Startup Program in the traditional execution file format.
Note that most JVMs are not secure. Hackers may modify the JVM, obtain the decrypted code from outside the classloader, and save it to the disk, bypassing the encryption technology described in this article. Java does not provide effective remedial measures for this purpose.
However, it should be noted that all these possible attacks have a premise that attackers can obtain keys. Without a key, the security of the application depends entirely on the security of the encryption algorithm. Although this method of protecting code is not perfect, it is still an effective solution to protect intellectual property rights and sensitive user data.