当前位置: 首页 > 科技观察

Java源码保护方式,采用加密技术保护源码

时间:2023-03-13 08:21:52 科技观察

有些项目非常注重保密性,对保护源码的要求很高。通常,需要对源代码进行加密。下面是我之前做的一个项目,使用了保护方式,下面的内容不是我自己写的,在网上找的资料都是在自己的项目中使用的。Java程序的源代码很容易被别人窥视,只要有反编译器,任何人都可以分析别人的代码。本文讨论如何在不修改原程序的情况下,通过加密技术保护源代码。1、为什么要加密?对于像C或C++这样的传统语言,保护Web上的源代码很容易,只要你不分发它。不幸的是,Java程序的源代码很容易被其他人偷看。任何拥有反编译器的人都可以分析别人的代码。Java的灵活性使得窃取源代码变得容易,但同时也使得通过加密保护代码变得相对容易。我们唯一需要知道的是Java的ClassLoader对象。当然,在加密过程中,JavaCryptographyExtension(JCE)的知识也是必不可少的。有几种“混淆”Java类文件的技术,使反编译器也无法处理它们。但是,修改反编译器来处理这些混淆的类文件并不困难,因此您不能简单地依靠混淆技术来保证源代码的安全。我们可以使用PGP(PrettyGoodPrivacy)或GPG(GNUPrivacyGuard)等流行的加密工具对应用程序进行加密。在这种情况下,最终用户必须在运行应用程序之前解密。但是解密之后,最终用户得到的是一个未加密的类文件,这和事先没有加密没有什么区别。Java运行时隐式加载字节码的机制意味着可以对字节码进行修改。JVM每次加载一个类文件时,都需要一个名为ClassLoader的对象,它负责将新类加载到正在运行的JVM中。JVM给ClassLoader一个字符串,其中包含要加载的类的名称(如java.lang.Object),然后ClassLoader负责查找类文件,加载原始数据,并将其转换为Class对象。我们可以通过自定义ClassLoader,在类文件执行前对其进行修改。这种技术有广泛的应用——这里它被用来在加载类文件时对其进行解密,因此它可以被看作是一个即时解密器。由于解密后的字节码文件永远不会保存到文件系统中,因此窃取者很难获得解密后的代码。由于将原始字节码转换为Class对象的过程完全由系统负责,因此创建一个自定义的ClassLoader对象其实并不难。您只需要先获取原始数据,然后就可以进行包括解密在内的任何转换。Java2在某种程度上简化了自定义类加载器的构造。在Java2中,loadClass的默认实现仍然负责所有必要的步骤,但它也调用新的findClass方法以允许各种自定义类加载过程。这为我们编写自定义ClassLoader提供了一条捷径,麻烦更少:只需覆盖findClass而不是loadClass。这种方法避免了重复所有加载器必须执行的通用步骤,因为loadClass负责所有这些。但是本文自定义的ClassLoader并没有使用这个方法。原因很简单。如果默认的ClassLoader先去找加密的class文件,它就能找到;但是因为class文件已经被加密了,所以它不会识别这个class文件,加载过程就会失败。因此,我们必须自己实现loadClass,稍微增加了工作量。二、自定义类加载器每个运行的JVM都已经有一个ClassLoader。默认的ClassLoader根据CLASSPATH环境变量的值在本地文件系统中寻找合适的字节码文件。应用自定义ClassLoader需要更深入地了解这个过程。我们必须首先创建自定义ClassLoader类的实例,然后明确要求它加载另一个类。这会强制JVM将此类及其所需的所有类关联到自定义ClassLoader。清单1显示了如何使用自定义ClassLoader加载类文件。[清单1:使用自定义ClassLoader加载类文件]//首先创建一个ClassLoader对象ClassLoadermyClassLoader=newmyClassLoader();//使用自定义ClassLoader对象加载类文件//并将其转化为Class对象ClassmyClass=myClassLoader.loadClass("mypackage.MyClass");//最后创建这个类的实例ObjectnewInstance=myClass.newInstance();//注意MyClass需要的所有其他类都会被自动加载//自定义的ClassLoader比如上面提到的自定义ClassLoader只需要先获取class文件的数据,然后将字节码传递给runtime系统,由后者完成剩下的工作。ClassLoader有几个重要的方法。在创建自定义ClassLoader时,我们只需要重写其中一个loadClass,提供获取原始class文件数据的代码即可。该方法有两个参数:类名,以及JVM是否需要解析类名的标志(即是否同时加载有依赖的类)。如果此标志为真,我们只需在返回JVM之前调用resolveClass。[清单2:ClassLoader.loadClass()的一个简单实现]publicClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{try{//我们要创建的Class对象Classclasz=null;//必需步骤1:如果类已经在systembuffer中,//我们不用再加载了clasz=findLoadedClass(name);if(clasz!=null)returnclassz;//下面是自定义部分一些方法*/;if(classData!=null){//成功读取字节码数据,现在转成Class对象clasz=defineClass(name,classData,0,classData.length);}//需要第2步:如果上面没有'不起作用,//我们尝试使用默认的ClassLoader加载它if(clasz==null)clasz=findSystemClass(name);//需要第三步:如果需要,加载相关类if(resolve&&clasz!=null)resolveClass(clasz);//返回类给调用者}catch(GeneralSecurityExceptiongse){thrownewClassNotFoundException(gse.toString());}}清单2显示了一个简单的loadClass实现。大部分代码对于所有ClassLoader对象都是相同的,但一小部分(用注释标记)是唯一的。在处理过程中,ClassLoader对象使用了其他几个辅助方法:findLoadedClass:用于检查以确认请求的类当前不存在。loadClass方法应该首先调用它。defineClass:获取原始class文件的字节码数据后,调用defineClass将其转化为Class对象。任何loadClass实现都必须调用此方法。findSystemClass:提供对默认ClassLoader的支持。如果用于查找类的自定义方法找不到指定的类(或者有意不使用自定义方法),可以调用该方法尝试默认的加载方式。这在从普通JAR文件加载标准Java类时尤其有用。resolveClass:当JVM不仅要加载指定的类,而且要加载该类引用的所有其他类时,它会将loadClass的resolve参数设置为true。这时候我们必须先调用resolveClass,然后再将新加载的Class对象返回给调用者。3、加解密Java加密扩展即JavaCryptographyExtension,简称JCE。它是Sun的加密服务软件,包括加密和密钥生成功能。JCE是JCA(Java密码体系结构)的扩展。JCE没有规定具体的加密算法,只是提供了一个框架,加密算法的具体实现可以作为服务提供者加入。除了JCE框架,JCE包还包含SunJCE服务提供者,包括很多有用的加密算法,比如DES(数据加密标准)和Blowfish。为简单起见,在本文中我们将使用DES算法来加密和解密字节码。以下是使用JCE加密和解密数据必须遵循的基本步骤:步骤1:生成安全密钥。在加密或解密任何数据之前需要密钥。密钥是随加密应用程序分发的一小段数据。清单3展示了如何生成密钥。【清单3:生成密钥】//DES算法需要一个可信的随机数源SecureRandomsr=newSecureRandom();//为我们选择的DES算法生成一个KeyGenerator对象KeyGeneratorkg=KeyGenerator.getInstance("DES");kg。init(sr);//生成密钥SecretKeykey=kg.generateKey();//通过terawKeyData[]=key.getEncoded();//获取密钥数据byterawKeyData[]=key.getEncoded();/*然后就可以使用密钥加密或者解密,或者保存作为供以后使用的文件*/doSomething(rawKeyData);第2步:加密数据。获得密钥后,您就可以使用它来加密数据。除了解密后的ClassLoader之外,通常还有一个单独的程序对要发布的应用程序进行加密(参见清单4)。【清单4:用密钥加密原始数据】//DES算法需要一个可信的随机数源SecureRandomsr=newSecureRandom();byterawKeyData[]=/*通过某种方式获取密钥数据*/;//来自原始密钥datacreatesDESKeySpecobjectDESKeySpecdks=newDESKeySpec(rawKeyData);//创建密钥工厂,然后用它把DESKeySpec转换成//一个SecretKey对象SecretKeyFactoryke??yFactory=SecretKeyFactory.getInstance("DES");SecretKeykey=keyFactory.generateSecret(dks);//Cipher对象真正完成了加密操作data并加密bytedata[]=/*以某种方式获取数据*///正式进行加密操作byteencryptedData[]=cipher.doFinal(data);//进一步处理加密数据doSomething(encryptedData);第三步:解密数据。运行加密的应用程序时,ClassLoader会分析和解密类文件。操作步骤如清单5所示。【清单5:用密钥解密数据】//DES算法需要一个可信的随机数源SecureRandomsr=newSecureRandom();byterawKeyData[]=/*通过某种方式获取原始密钥数据*/;//从原始密钥数据创建aDESKeySpecobjectDESKeySpecdks=newDESKeySpec(rawKeyData);//创建密钥工厂,然后用它把DESKeySpec对象转换成//一个SecretKey对象SecretKeyFactoryke??yFactory=SecretKeyFactory.getInstance("DES");SecretKeykey=keyFactory.generateSecret(dks);//Cipher对象真正完成了解密操作byteenencryptedData[]=/*得到加密数据*///正式执行解密操作byteenencryptedData[]=cipher.doFinal(encryptedData);//对解密数据做进一步处理4.应用上面的例子描述了如何对数据进行加密和解密。部署加密应用程序的步骤如下:第一步:创建应用程序。我们的示例包含一个主App类和两个辅助类(称为Foo和Bar)。这个应用没有什么实用的功能,但是我们只要能加密这个应用,加密其他应用是没有问题的。第2步:生成安全密钥。在命令行中,使用GenerateKey实用程序(请参阅GenerateKey.java)将密钥写入文件:%javaGenerateKeykey.data第3步:加密应用程序。从命令行,使用EncryptClasses实用程序加密应用程序的类(请参阅EncryptClasses.java):第4步:运行加密的应用程序。用户通过DecryptStart程序运行加密的应用程序。DecryptStart程序如清单6所示。[清单6:DecryptStart.java,启动加密应用程序的程序]importjava.io.*;importjava.security.*;importjava.lang.reflect.*;importjavax.crypto.*;importjavax.crypto.spec.*;publicclassDecryptStartextendsClassLoader{//这些对象在构造函数中设置,//loadClass()方法后面会用到它们来解密类privateSecretKeykey;privateCiphercipher;//构造函数:设置解密需要的对象publicDecryptStart(SecretKeykey)throwsGeneralSecurityException,IOException{this.key=key;Stringalgorithm="DES";SecureRandomsr=newSecureRandom();System.err.println("[DecryptStart:creatingcipher]");cipher=Cipher.getInstance(算法);cipher.init(Cipher.DECRYPT_MODE,key,sr);}//主要流程:我们需要在这里读入key,并创建一个//DecryptStart的实例,也就是我们自定义的ClassLoader。//设置好ClassLoader后,我们用它来加载应用实例,//最后通过JavaReflectionAPI调用应用实例的main方法staticpublicvoidmain(Stringargs[])throwsException{StringkeyFilename=args[0];StringappName=参数[1];//这些是传递给应用程序本身的参数StringrealArgs[]=newString[args.length-2];System.arraycopy(args,2,realArgs,0,args.length-2);//读取关键System.err.println("[DecryptStart:readingkey]");byterawKey[]=Util.readFile(keyFilename);DESKeySpecdks=newDESKeySpec(rawKey);SecretKeyFactoryke??yFactory=SecretKeyFactory.getInstance("DES");SecretKeykey=keyFactory.generateSecret(dks);//创建解密后的ClassLoaderDecryptStartdr=newDecryptStart(key);//创建应用程序主类的实例//通过ClassLoader加载System.err.println("[DecryptStart:loading"+appName+"]");Classclasz=dr.loadClass(appName);//最后通过ReflectionAPI调用应用实例的main()方法//获取main()的引用Stringproto[]=newString[1];ClassmainArgs[]={(newString[1]).getClass()};Methodmain=clasz.getMethod("main",mainArgs);//创建数组ObjectargsArray[]={realArgs};System.err.println("[DecryptStart:running"+appName+".main()]");//调用main()main.invoke(null,argsArray);}publicClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{try{//We要创建的Class对象Classclasz=null;//需要第1步:如果类已经在系统缓存中//我们就不用再加载了clasz=findLoadedClass(name);if(clasz!=null)returnclassz;//下面是自定义的部分]=密码。doFinal(classData);//...并将其转换为类clasz=defineClass(name,decryptedClassData,0,decryptedClassData.length);System.err.println("[DecryptStart:decryptingclass"+name+"]");}}catch(FileNotFoundExceptionfnfe){}//需要第2步:如果上述不成功//我们尝试使用默认的ClassLoader加载它if(clasz==null)clasz=findSystemClass(name);//需要第3步:必要时加载相关类if(resolve&&clasz!=null)resolveClass(clasz);//返回类给调用者{thrownewClassNotFoundException(gse.toString());}}}DecryptStart有两个目的。DecryptStart的实例是实现即时解密操作的自定义ClassLoader。同时,DecryptStart还包含一个创建解密器实例并使用它来加载和运行应用程序的主程序。示例应用程序App的代码包含在App.java、Foo.java和Bar.java中。Util.java是一个文件I/O工具,在本文的很多例子中都会用到它。五、注意事项我们已经看到,在不修改源代码的情况下加密Java应用程序是非常容易的。然而,世界上没有完全安全的系统。本文中的加密提供了一定程度的源代码保护,但它容易受到某些攻击。虽然应用程序本身已加密,但启动器DecryptStart并未加密。攻击者可以反编译启动程序并对其进行修改,将解密后的类文件保存到磁盘中。降低这种风险的一种方法是对启动程序进行高质量的混淆。或者,启动程序也可以使用直接编译成机器语言的代码,使得启动程序具有传统可执行文件格式的安全性。另请记住,大多数JVM并非天生安全。狡猾的黑客可能会修改JVM,从ClassLoader外部获取解密后的代码并保存到磁盘,从而绕过本文的加密技术。Java对此没有提供真正有效的补救措施。但是需要指出的是,所有这些可能的攻击都有一个前提,即攻击者能够拿到密钥。如果没有密钥,应用程序的安全性完全取决于加密算法的安全性。虽然这种保护代码的方法并不完美,但它仍然是保护知识产权和敏感用户数据的有效方法。