对于传统的C或C++之类的语言来讲,要在Web上保护源代码是很容易的,只要不发布它就能够。遗憾的是,Java程序的源代码很容易被别人偷看。只要有一个反编译器,任何人均可以分析别人的代码。Java的灵活性使得源代码很容易被窃取,但与此同时,它也使经过加密保护代码变得相对容易,咱们惟一须要了解的就是Java的ClassLoader对象。固然,在加密过程当中,有关Java Cryptography Extension(JCE)的知识也是必不可少的。html
有几种技术能够“模糊”Java类文件,使得反编译器处理类文件的效果大打折扣。然而,修改反编译器使之可以处理这些通过模糊处理的类文件并非什么难事,因此不能简单地依赖模糊技术来保证源代码的安全。java
咱们能够用流行的加密工具加密应用,好比PGP(Pretty Good Privacy)或GPG(GNU Privacy Guard)。这时,最终用户在运行应用以前必须先进行解密。但解密以后,最终用户就有了一份不加密的类文件,这和事先不进行加密没有什么差异。算法
Java运行时装入字节码的机制隐含地意味着能够对字节码进行修改。JVM每次装入类文件时都须要一个称为ClassLoader的对象,这个对象负责把新的类装入正在运行的JVM。JVM给ClassLoader一个包含了待装入类(好比java.lang.Object)名字的字符串,而后由ClassLoader负责找到类文件,装入原始数据,并把它转换成一个Class对象。数组
咱们能够经过定制ClassLoader,在类文件执行以前修改它。这种技术的应用很是普遍――在这里,它的用途是在类文件装入之时进行解密,所以能够当作是一种即时解密器。因为解密后的字节码文件永远不会保存到文件系统,因此窃密者很可贵到解密后的代码。安全
因为把原始字节码转换成Class对象的过程彻底由系统负责,因此建立定制ClassLoader对象其实并不困难,只需先得到原始数据,接着就能够进行包含解密在内的任何转换。app
Java 2在必定程度上简化了定制ClassLoader的构建。在Java 2中,loadClass的缺省实现仍旧负责处理全部必需的步骤,但为了顾及各类定制的类装入过程,它还调用一个新的findClass方法。框架
这为咱们编写定制的ClassLoader提供了一条捷径,减小了麻烦:只需覆盖findClass,而不是覆盖loadClass。这种方法避免了重复全部装入器必需执行的公共步骤,由于这一切由loadClass负责。dom
不过,本文的定制ClassLoader并不使用这种方法。缘由很简单。若是由默认的ClassLoader先寻找通过加密的类文件,它能够找到;但因为类文件已经加密,因此它不会承认这个类文件,装入过程将失败。所以,咱们必须本身实现loadClass,稍微增长了一些工做量。函数
回页首工具
每个运行着的JVM已经拥有一个ClassLoader。这个默认的ClassLoader根据CLASSPATH环境变量的值,在本地文件系统中寻找合适的字节码文件。
应用定制ClassLoader要求对这个过程有较为深刻的认识。咱们首先必须建立一个定制ClassLoader类的实例,而后显式地要求它装入另一个类。这就强制JVM把该类以及全部它所须要的类关联到定制的ClassLoader。Listing 1显示了如何用定制ClassLoader装入类文件。
【Listing 1:利用定制的ClassLoader装入类文件】 // 首先建立一个ClassLoader对象 ClassLoader myClassLoader = new myClassLoader(); // 利用定制ClassLoader对象装入类文件 // 并把它转换成Class对象 Class myClass = myClassLoader.loadClass( "mypackage.MyClass" ); // 最后,建立该类的一个实例 Object newInstance = myClass.newInstance(); // 注意,MyClass所须要的全部其余类,都将经过 // 定制的ClassLoader自动装入
如前所述,定制ClassLoader只需先获取类文件的数据,而后把字节码传递给运行时系统,由后者完成余下的任务。
ClassLoader有几个重要的方法。建立定制的ClassLoader时,咱们只需覆盖其中的一个,即loadClass,提供获取原始类文件数据的代码。这个方法有两个参数:类的名字,以及一个表示JVM是否要求解析类名字的标记(便是否同时装入有依赖关系的类)。若是这个标记是true,咱们只需在返回JVM以前调用resolveClass。
【Listing 2:ClassLoader.loadClass()的一个简单实现】 public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { try { // 咱们要建立的Class对象 Class clasz = null; // 必需的步骤1:若是类已经在系统缓冲之中, // 咱们没必要再次装入它 clasz = findLoadedClass( name ); if (clasz != null) return clasz; // 下面是定制部分 byte classData[] = /* 经过某种方法获取字节码数据 */; if (classData != null) { // 成功读取字节码数据,如今把它转换成一个Class对象 clasz = defineClass( name, classData, 0, classData.length ); } // 必需的步骤2:若是上面没有成功, // 咱们尝试用默认的ClassLoader装入它 if (clasz == null) clasz = findSystemClass( name ); // 必需的步骤3:若有必要,则装入相关的类 if (resolve && clasz != null) resolveClass( clasz ); // 把类返回给调用者 return clasz; } catch( IOException ie ) { throw new ClassNotFoundException( ie.toString() ); } catch( GeneralSecurityException gse ) { throw new ClassNotFoundException( gse.toString() ); } }
Listing 2显示了一个简单的loadClass实现。代码中的大部分对全部ClassLoader对象来讲都同样,但有一小部分(已经过注释标记)是特有的。在处理过程当中,ClassLoader对象要用到其余几个辅助方法:
Java加密扩展即Java Cryptography Extension,简称JCE。它是Sun的加密服务软件,包含了加密和密匙生成功能。JCE是JCA(Java Cryptography Architecture)的一种扩展。
JCE没有规定具体的加密算法,但提供了一个框架,加密算法的具体实现能够做为服务提供者加入。除了JCE框架以外,JCE软件包还包含了SunJCE服务提供者,其中包括许多有用的加密算法,好比DES(Data Encryption Standard)和Blowfish。
为简单计,在本文中咱们将用DES算法加密和解密字节码。下面是用JCE加密和解密数据必须遵循的基本步骤:
步骤1:生成一个安全密匙。在加密或解密任何数据以前须要有一个密匙。密匙是随同被加密的应用一块儿发布的一小段数据,Listing 3显示了如何生成一个密匙。
【Listing 3:生成一个密匙】 // DES算法要求有一个可信任的随机数源 SecureRandom sr = new SecureRandom(); // 为咱们选择的DES算法生成一个KeyGenerator对象 KeyGenerator kg = KeyGenerator.getInstance( "DES" ); kg.init( sr ); // 生成密匙 SecretKey key = kg.generateKey(); // 获取密匙数据 byte rawKeyData[] = key.getEncoded(); /* 接下来就能够用密匙进行加密或解密,或者把它保存 为文件供之后使用 */ doSomething( rawKeyData );
步骤2:加密数据。获得密匙以后,接下来就能够用它加密数据。除了解密的ClassLoader以外,通常还要有一个加密待发布应用的独立程序(见Listing 4)。
【Listing 4:用密匙加密原始数据】 // DES算法要求有一个可信任的随机数源 SecureRandom sr = new SecureRandom(); byte rawKeyData[] = /* 用某种方法得到密匙数据 */; // 从原始密匙数据建立DESKeySpec对象 DESKeySpec dks = new DESKeySpec( rawKeyData ); // 建立一个密匙工厂,而后用它把DESKeySpec转换成 // 一个SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" ); SecretKey key = keyFactory.generateSecret( dks ); // Cipher对象实际完成加密操做 Cipher cipher = Cipher.getInstance( "DES" ); // 用密匙初始化Cipher对象 cipher.init( Cipher.ENCRYPT_MODE, key, sr ); // 如今,获取数据并加密 byte data[] = /* 用某种方法获取数据 */ // 正式执行加密操做 byte encryptedData[] = cipher.doFinal( data ); // 进一步处理加密后的数据 doSomething( encryptedData );
步骤3:解密数据。运行通过加密的应用时,ClassLoader分析并解密类文件。操做步骤如Listing 5所示。
【Listing 5:用密匙解密数据】 // DES算法要求有一个可信任的随机数源 SecureRandom sr = new SecureRandom(); byte rawKeyData[] = /* 用某种方法获取原始密匙数据 */; // 从原始密匙数据建立一个DESKeySpec对象 DESKeySpec dks = new DESKeySpec( rawKeyData ); // 建立一个密匙工厂,而后用它把DESKeySpec对象转换成 // 一个SecretKey对象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" ); SecretKey key = keyFactory.generateSecret( dks ); // Cipher对象实际完成解密操做 Cipher cipher = Cipher.getInstance( "DES" ); // 用密匙初始化Cipher对象 cipher.init( Cipher.DECRYPT_MODE, key, sr ); // 如今,获取数据并解密 byte encryptedData[] = /* 得到通过加密的数据 */ // 正式执行解密操做 byte decryptedData[] = cipher.doFinal( encryptedData ); // 进一步处理解密后的数据 doSomething( decryptedData );
前面介绍了如何加密和解密数据。要部署一个通过加密的应用,步骤以下:
% java GenerateKey key.data
% java EncryptClasses key.data App.class Foo.class Bar.class
【Listing 6:DecryptStart.java,启动被加密应用的程序】 import java.io.*; import java.security.*; import java.lang.reflect.*; import javax.crypto.*; import javax.crypto.spec.*; public class DecryptStart extends ClassLoader { // 这些对象在构造函数中设置, // 之后loadClass()方法将利用它们解密类 private SecretKey key; private Cipher cipher; // 构造函数:设置解密所须要的对象 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过程:咱们要在这里读入密匙,建立DecryptStart的 // 实例,它就是咱们的定制ClassLoader。 // 设置好ClassLoader之后,咱们用它装入应用实例, // 最后,咱们经过Java Reflection API调用应用实例的main方法 static public void main( String args[] ) throws Exception { String keyFilename = args[0]; String appName = args[1]; // 这些是传递给应用自己的参数 String realArgs[] = new String[args.length-2]; System.arraycopy( args, 2, realArgs, 0, args.length-2 ); // 读取密匙 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 ); // 建立解密的ClassLoader DecryptStart dr = new DecryptStart( key ); // 建立应用主类的一个实例 // 经过ClassLoader装入它 System.err.println( "[DecryptStart: loading "+appName+"]" ); Class clasz = dr.loadClass( appName ); // 最后,经过Reflection API调用应用实例 // 的main()方法 // 获取一个对main()的引用 String proto[] = new String[1]; Class mainArgs[] = { (new String[1]).getClass() }; Method main = clasz.getMethod( "main", mainArgs ); // 建立一个包含main()方法参数的数组 Object argsArray[] = { realArgs }; System.err.println( "[DecryptStart: running "+appName+".main()]" ); // 调用main() main.invoke( null, argsArray ); } public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { try { // 咱们要建立的Class对象 Class clasz = null; // 必需的步骤1:若是类已经在系统缓冲之中 // 咱们没必要再次装入它 clasz = findLoadedClass( name ); if (clasz != null) return clasz; // 下面是定制部分 try { // 读取通过加密的类文件 byte classData[] = Util.readFile( name+".class" ); if (classData != null) { // 解密... byte decryptedClassData[] = cipher.doFinal( classData ); // ... 再把它转换成一个类 clasz = defineClass( name, decryptedClassData, 0, decryptedClassData.length ); System.err.println( "[DecryptStart: decrypting class "+name+"]" ); } } catch( FileNotFoundException fnfe ) { } // 必需的步骤2:若是上面没有成功 // 咱们尝试用默认的ClassLoader装入它 if (clasz == null) clasz = findSystemClass( name ); // 必需的步骤3:若有必要,则装入相关的类 if (resolve && clasz != null) resolveClass( clasz ); // 把类返回给调用者 return clasz; } catch( IOException ie ) { throw new ClassNotFoundException( ie.toString() ); } catch( GeneralSecurityException gse ) { throw new ClassNotFoundException( gse.toString() ); } } }
% java App arg0 arg1 arg2
% java DecryptStart key.data App arg0 arg1 arg2
DecryptStart有两个目的。一个DecryptStart的实例就是一个实施即时解密操做的定制ClassLoader;同时,DecryptStart还包含一个main过程,它建立解密器实例并用它装入和运行应用。示例应用App的代码包含在App.java、Foo.java和Bar.java内。Util.java是一个文件I/O工具,本文示例多处用到了它。完整的代码请从本文最后下载。
咱们看到,要在不修改源代码的状况下加密一个Java应用是很容易的。不过,世上没有彻底安全的系统。本文的加密方式提供了必定程度的源代码保护,但对某些攻击来讲它是脆弱的。
虽然应用自己通过了加密,但启动程序DecryptStart没有加密。攻击者能够反编译启动程序并修改它,把解密后的类文件保存到磁盘。下降这种风险的办法之一是对启动程序进行高质量的模糊处理。或者,启动程序也能够采用直接编译成机器语言的代码,使得启动程序具备传统执行文件格式的安全性。
另外还要记住的是,大多数JVM自己并不安全。狡猾的黑客可能会修改JVM,从ClassLoader以外获取解密后的代码并保存到磁盘,从而绕过本文的加密技术。Java没有为此提供真正有效的补救措施。
不过应该指出的是,全部这些可能的攻击都有一个前提,这就是攻击者能够获得密匙。若是没有密匙,应用的安全性就彻底取决于加密算法的安全性。虽然这种保护代码的方法称不上十全十美,但它仍不失为一种保护知识产权和敏感用户数据的有效方案。
参考资料