使用Class签名的防破解技术介绍


使用背景和签名技术介绍


使用背景


在很多情况下,我们的软件为防止盗用,都增加了license的校验功能,通过license文件进行授权,从而让使用者不能随意使用其未购买的功能。然而,道高一尺,魔高一丈,一些用户的高手往往会找到很多控制许可的程序点,通过反编译等手段(这里针对Java语言),对控制许可的Java类进行破解,从而使license文件失效。这里就介绍一种反破解的技术,对一些关键的Class进行签名,然后在程序的另一个地方对这些class签名进行验证,如果这些class中有被改写的迹象,那么签名验证肯定会失败,签名失败后程序就不会正常运行,这样就可以进行反破解操作。


什么是数字签名


数字签名是一种防伪造技术,就是将要签名的数据(文本、或文本或二进制文件、字节数组等),使用数字摘要算法(如MD5,或更安全的SHA1),先生成数字摘要,然后用非对称加密的私钥对摘要进行加密,从而得到一个数字签名。用户要验证签名是否正确,只需要拿到公钥对签名进行解密,得到摘要,再使用数字摘要算法对原始的数据进行数字摘要计算,将计算结果和解密出来的摘要进行比较,如果相同,则说明签名正确,数据未被篡改,反之,数据就是被篡改了。


防伪为什么要使用数字签名而不是数字摘要


数字摘要是一个原始数据的散列码,结果是一小段字节数组,散列码的特点是原始数据即便有很小的差别,散列码就会有很大的不同,这样拿到散列码的人很难判断原始数据时怎么样的,其实数字摘要相当于一个原始数据的全系缩影,通过一个摘要,基本上能够唯一代表一个原始数据,人们通常通过摘要算法将原始数据进行数字摘要计算,将结果和以前生成的摘要进行比较,从而判断出数据是否被篡改,如果两个摘要一致,数据就可以认为是真实的,否则就是被篡改的。


但问题就出现了,因为摘要算法是公开的,任何人都可以篡改原始数据,然后生成摘要,这样你拿到的摘要是无法保证是真实的。这样,就需要非对称加密算法来帮忙了。


非对称加密算法(如RSA)是相对于对称加密算法而言的,对称加密算法是使用一个密钥,对数据进行加密和解密。加密者加密后的结果是要给合法的解密者的,而合法解密者必须拿到相同的密钥才能够解密,这样密钥必须公开给解密者,这样就存在一个问题,密钥在传输过程中容易被窃取,如果非法的解秘者窃取到这个密钥,那他就能够进行解密,拿到原始数据。用户也可以篡改原始数据,用密钥进行加密,将加密结果发出去,这样拿到加密信息的合法解密者就无法知道解密数据的真实性。


非对称加密就解决了这个问题,非对称加密需要两个密钥,一个是私钥,由加密者拥有,不对外发布的,也不存在密钥传输过程中被窃取的现象。另一个是公钥,是对外公布的,谁都可以拿到。非对称加密解密有两种情况,一个是私钥加密公钥解密,一个是公钥加密私钥解密。由于私钥是不公开的,即不向外传输的,所以,除了私钥拥有者,任何人都拿不到私钥,无法使用私钥来加密原始数据,这样别人就无法篡改数据,生成假的加密数据,而拿到公钥的人也能确信解密出来的数据时真实有效的。


数字签名是"摘要+非对称加密",即对摘要进行私钥加密,然后公钥解密出摘要,使用公钥解密能确保摘要的真实性,而摘要能确保原始数据的真实性。


如何对类进行签名


前面介绍了一些背景知识,下面就言归正传,说一说如何防止破解,对类进行签名的过程。


1、生成非对称加密的密钥对


首先要生成非对称加密的密钥对,这里选取了最常用的非对称加密算法RSA,这里使用Java的java.security.KeyPairGenerator来生成密钥,我们可以给它一个种子(可选),程序如下:


package demo.signature;

import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;

public class KeyPairsGenerator {
private static final char[] bcdLookup = { '0', '1', '2', '3', '4', '5','6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

/**
* 生成公钥、私钥
*
* @param algorithm 算法
* @param seed 种子
* @return 返回公钥、私钥,第一个是公钥、第二个是私钥
*/
public static String[] generatorKeys(String algorithm,String seed){
String[] results = new String[2];
String priKey;
String pubKey;
java.security.KeyPairGenerator keygen;
try {
keygen = java.security.KeyPairGenerator.getInstance(algorithm);
SecureRandom secrand = new SecureRandom();
secrand.setSeed(seed.getBytes());
keygen.initialize(1024, secrand);
KeyPair keys = keygen.genKeyPair();

PublicKey pubkey = keys.getPublic();
PrivateKey prikey = keys.getPrivate();

pubKey = bytes2Hex(pubkey.getEncoded());
priKey = bytes2Hex(prikey.getEncoded());
results[0] = pubKey;
results[1] = priKey;
System.out.println("生成秘钥成功");
} catch (NoSuchAlgorithmException e) {
System.out.println("生成秘钥失败");
e.printStackTrace();
}
return results;
}

/**
* Transform the specified Hex String into a byte array.
*/
public static final byte[] hex2Bytes(String s) {
byte[] bytes;

bytes = new byte[s.length() / 2];

for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) Integer.parseInt(s.substring(2 * i, 2 * i + 2),
16);
}

return bytes;
}

/**
* Transform the specified byte into a Hex String form.
*/
public static final String bytes2Hex(byte[] bcd) {
StringBuffer s = new StringBuffer(bcd.length * 2);

for (int i = 0; i < bcd.length; i++) {
s.append(bcdLookup[(bcd[i] >>> 4) & 0x0f]);
s.append(bcdLookup[bcd[i] & 0x0f]);
}

return s.toString();
}

public static void main(String[] args) {
String[] keyPairs=generatorKeys("RSA", "myKeyPairs");
System.out.println("PublicKey="+keyPairs[0]);
System.out.println("PrivateKey="+keyPairs[1]);
}
}


运行这个程序,生成公钥PublicKey和私钥PrivateKey,结果如下:


生成秘钥成功
PublicKey=30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001
PrivateKey=30820276020100300d06092a864886f70d0101010500048202603082025c02010002818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa53020301000102818100e3019042b55102343f891faf2e193cecd093ca7e82841d28328e2e026effa6e9e26407bf60b1ce6e2c9f9253bd45b104006a199bf052168ab78e1aad156439c3d6fd3680029b7851ab1fbe524f373d3861fadd9ae7d17ba9221ffb0482ef2bae8bff4ad4b0460768e6bb7705ceacf6b6e5f64e06697fa42c283383e8a357efe1024100f4ba7f483bce6a601f8fed2874d72ec13ed4e93d128a8f38a72e088a50e0dbf57c1c3c6edc14ebbbc6e056c75d7b569813a02d216650adeed0c76d67e2f10d51024100efdada6355645bf3b86011ed380bfc2f8229a311be168ce97f7caeaa68302a353fc124cc063e0d3100796b8f60acc6e098dd80741b968ab1c7fc488824c6946302400d7265e6012b415b10c0e6c60f4d778b34b99c2b37e6972204c599c087db231ae4fbe4322f3393145944206089f969f3a73868e269edcaf0d155f3e3fe6b42510240244d4c66626339238f11434553094556ef6d5bd7f09c3b2190010ff28ca8558b0fa62cea903b4e05cf9b90f2f75fcf0de935051f0d99e04dfe05a9f8a910411f0240332ede411c2f126c492a94de8ae28007d94d298ad46ece4b09ea8e77c4268a04754e17e803d00ff217214263d6178779649b8c9509c9a5f798e19a5126b39e31



2、建立一个测试类,用于签名的类


建立一个测试用的被签名的类,如下:


package demo.signature;

public class ClassToBeSigned {
public static void main(String[] args) {
System.out.println("I'm a class to demo digital siguature");
}
}



3、对class进行签名


建立ClassSigner java文件,设置一个PRIVATE_KEY常量,将刚才生成的私钥结果写进去,然后对类路径中的Class(使用资源流读出的字节数组)进行签名,程序如下:


package demo.signature; 

import java.io.InputStream; 
import java.security.KeyFactory; 
import java.security.PublicKey; 
import java.security.spec.X509EncodedKeySpec; 
import java.security.Signature;
public class SignValidator { 
public static final StringPUBLICK_KEY="30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001";

/** 
* 将资源读为字符串 
* 
* @param resourceName 
* @return 
*/ 
private static String read2Str(String resourceName) { 
InputStream in=SignValidator.class.getResourceAsStream(resourceName); 
if(in==null
return null
else { 
byte[] bytes=null
String str=null
try { 
bytes=new byte[in.available()]; 
in.read(bytes); 
str=new String(bytes); 
return str; 
catch (Exception e) { 
return null
}finally { 
try { 
in.close(); 
catch (Exception e) { 
// ignor 





/** 
* 校验数字签名 
* 
* @param pubKeyValue 公钥 
* @param data 要验证的字节数组 
* @param sign 签名内容 
* @return 校验结果 
*/ 
private static boolean verifySign(String pubKeyValue,byte[] data,String sign){ 
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(KeyPairsGenerator.hex2Bytes(pubKeyValue)); 
KeyFactory keyFactory; 
try { 
keyFactory = KeyFactory.getInstance("RSA"); 
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec); 
byte[] signed = KeyPairsGenerator.hex2Bytes(sign);// 这是SignatureData输出的数字签名 
Signature signetcheck = Signature.getInstance("SHA1withRSA"); 
signetcheck.initVerify(pubKey); 
signetcheck.update(data); 
if(signetcheck.verify(signed)){ 
return true
}else 
return false
catch (Throwable e) { 
return false



/** 
* 验证文件的签名是否正确 
* 
* @param resourcePath 要验证的资源 
* @param signResourceName 签名的资源名称 
* @return 
*/ 
public static boolean verify(String resourcePath,String signResourceName) { 
String signature=read2Str(signResourceName); 

byte[] data=ClassSigner.getBytes(resourcePath); 

return verifySign(PUBLICK_KEY, data, signature); 



运行这个类,得到类的签名结果:



signature=16c37a90d0cf79dd7c306ca2f419927ba64b0deb6f2bf8bbc71a24c9247327d1a1620cd317276df2a66af130357df6e5175bd03ad9681a912c294a6e38fdf622df6a3b0b2d9e34c3e360b0c0c288644ec12babb9e01a873e5e196573745b264e91a1d65e284128ad89b17081ee9bdc628993e90ed3f3b731f63b73542d80d46b



4、编写签名验证类


编写一个类SignValidator,用来验证类的签名。将签名结果放入一个文件 demo/signature/1.sig文件中,内容如下:

46f0acf113a9dbd6c7df8716d31b3b6a4dbf5b28190d726ad61c59fb82c46fbb1744e9101199c411518374b1012761859a31c79b7259d2a9d1eee65de60c1563837c4bf15459a258b1cce2653adc550233cf2eecccbee99a95b583f3f20a00698d878453686453e5953c6b61a0179c234fb2b26c89b8fd8d65bf4fc5305bc436


编写一个签名验证类,类中建立一个常量PUBLIC_KEY,将前面生成的公钥值放入用于解密,java代码如下:


package demo.signature; 

import java.io.InputStream; 
import java.security.KeyFactory; 
import java.security.PublicKey; 
import java.security.spec.X509EncodedKeySpec; 
import java.security.Signature;
public class SignValidator { 
public static final StringPUBLICK_KEY="30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001";

/** 
* 将资源读为字符串 
* 
* @param resourceName 
* @return 
*/ 
private static String read2Str(String resourceName) { 
InputStream in=SignValidator.class.getResourceAsStream(resourceName); 
if(in==null
return null
else { 
byte[] bytes=null
String str=null
try { 
bytes=new byte[in.available()]; 
in.read(bytes); 
str=new String(bytes); 
return str; 
catch (Exception e) { 
return null
}finally { 
try { 
in.close(); 
catch (Exception e) { 
// ignor 





/** 
* 校验数字签名 
* 
* @param pubKeyValue 公钥 
* @param data 要验证的字节数组 
* @param sign 签名内容 
* @return 校验结果 
*/ 
private static boolean verifySign(String pubKeyValue,byte[] data,String sign){ 
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(KeyPairsGenerator.hex2Bytes(pubKeyValue)); 
KeyFactory keyFactory; 
try { 
keyFactory = KeyFactory.getInstance("RSA"); 
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec); 
byte[] signed = KeyPairsGenerator.hex2Bytes(sign);// 这是SignatureData输出的数字签名 
Signature signetcheck = Signature.getInstance("SHA1withRSA"); 
signetcheck.initVerify(pubKey); 
signetcheck.update(data); 
if(signetcheck.verify(signed)){ 
return true
}else 
return false
catch (Throwable e) { 
return false



/** 
* 验证文件的签名是否正确 
* 
* @param resourcePath 要验证的资源 
* @param signResourceName 签名的资源名称 
* @return 
*/ 
public static boolean verify(String resourcePath,String signResourceName) { 
String signature=read2Str(signResourceName); 

byte[] data=ClassSigner.getBytes(resourcePath); 

return verifySign(PUBLICK_KEY, data, signature); 



6、在程序关键启动点进行签名验证


下面是一个演示的验证class签名的程序。


package demo.signature; 

public class SignValidationDemo { 
public static void main(String[] args) { 
String resourcePath="/demo/signature/ClassToBeSigned.class"
boolean valid=SignValidator.verify(resourcePath, "1.sig"); 
if(!valid) { 
System.out.println(resourcePath+" is not valid."); 
//TODO 做相应的处理,如退出启动过程等。 



}



运行这个程序,程序没有任何输出,说明签名验证正确。这时候,我们将/demo/signature/ClassToBeSigned.java修改一下,再次运行SignValidationDemo程序,发现签名验证不过,打印下面的语句:

/demo/signature/ClassToBeSigned.class is not valid.


为什么不使用jar文件签名?


我们这里讲了一个使用类路径中class签名的例子,其实为了防伪,我们也可以针对一个jar文件进行签名和验证,但对jar签名有缺点的,


jar不能被变更,如果有新需求需要在jar中增加类或修改类(不是破解),则必须重新生成签名,签名验证类要重新修改,比较麻烦。使用类签名的好处是,只对几个关键的不能修改的类进行签名,签名的类所在的jar可以增加新文件、删除和修改非关键文件,使得扩展性更好。

使用jar签名,必须要找到jar的路径,而查找路径是一个比较麻烦的事情(特别是在不同应用服务器上,jar有可能被部署在临时目录下),而使用类签名,只要在类路径中搜索就可以了,用当前类的ClassLoader搜索类资源是非常方便的。

你可能感兴趣的:(java,算法,java,class,解密,反编译,安全)