android 关于利用签名的SHA1进行安全校验的方法之一(推荐)
最近做安卓项目中使用到了百度地图的api,在申请百度地图key的时候,需要我们填入“签名的sha1”和“客户端包名”,然后百度为我们生成一个key。
于是就引发了思考,百度为何需要我们客户端签名的sha1值呢?
第一想法就是:百度拿我们输入的参数sha1和包名进行一些列算法计算,生成一个key返回给我们。
为了证明这个想法,写了demo进行测试,android获取包名的方法很简单,但是我们还需要从客户端中获取keystore的指纹sha1。
进行各种资料的查找和分析,才得出方法。
一、首先,科普一下apk包下的meta-inf目录
我们已经知道的是:android对每一个apk文件都会进行签名,在apk文件安装时,系统会对其签名信息进行比对,判断程序的完整性,从而决定该apk文件是否可以安装,在一定程度上达到安全的目的。
给定一个apk文件,解压,可以看到一个meta-info文件夹,在该文件夹下有三个文件:分别为manifest.mf、cert.sf和cert.rsa。这三个文件分别表征以下含义:
(1)manifest.mf:这是摘要文件。程序遍历apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个用sha1生成摘要信息,再用base64进行编码。如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与manifest.mf的检验信息不同,于是程序就不能成功安装。
说明:如果攻击者修改了程序的内容,有重新生成了新的摘要,那么就可以通过验证,所以这是一个非常简单的验证。
(2)cert.sf:这是对摘要的签名文件。对前一步生成的manifest.mf,使用sha1-rsa算法,用开发者的私钥进行签名。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息(即,manifest.mf文件)进行对比,如果相符,则表明内容没有被异常修改。
说明:在这一步,即使开发者修改了程序内容,并生成了新的摘要文件,但是攻击者没有开发者的私钥,所以不能生成正确的签名文件(cert.sf)。系统在对程序进行验证的时候,用开发者公钥对不正确的签名文件进行解密,得到的结果和摘要文件(manifest.mf)对应不起来,所以不能通过检验,不能成功安装文件。
(3)cert.rsa文件中保存了公钥、所采用的加密算法等信息。
说明:系统对签名文件进行解密,所需要的公钥就是从这个文件里取出来的。
结论:从上面的总结可以看出,meta-info里面的说那个文件环环相扣,从而保证android程序的安全性。(只是防止开发者的程序不被攻击者修改,如果开发者的公私钥对对攻击者得到或者开发者开发出攻击程序,android系统都无法检测出来。)
我们将apk包解包,然后使用命令 keytool -printcert -file cert.rsa 查看cert.rsa,如图所示:
答案很明显,cert.rsa文件中存放了关于签名的信息。
当我们使用如下代码获取证书信息的时候:
/** * 获取指定包名程序的签名信息 * * @param context * @param packname * @author shanhy */ public static void getsinginfo(context context, string packname) { try { packageinfo packageinfo = context.getpackagemanager().getpackageinfo(packname, packagemanager.get_signatures); signature[] signs = packageinfo.signatures; signature sign = signs[0]; parsesignature(sign.tobytearray()); } catch (exception e) { e.printstacktrace(); } } public static void parsesignature(byte[] signature) { try { certificatefactory certfactory = certificatefactory.getinstance("x.509"); x509certificate cert = (x509certificate) certfactory.generatecertificate(new bytearrayinputstream(signature)); string pubkey = cert.getpublickey().tostring(); string signnumber = cert.getserialnumber().tostring(); log.d(tag, "pubkey = " + pubkey);// 输出的是16进制的公钥 log.d(tag, "signnumber = " + signnumber); } catch (exception e) { e.printstacktrace(); } }
查看api可以发现,x509certificate类并没有提供直接可以获取sha1值的方法。
研究签名文件的加密算法之后,我们看可以根据publickey自己计算出sha1的值,计算方法为:
> 通过 x509certificate 的 getencoded() 方法获得公钥的字节数组。
> 使用 messagedigest 对字节数组进行sha1的 degest 处理,得到新的字节数组。
> 然后使用16进制对得到新字节数组进行转换,最终得到证书文件的指纹sha1。
更详细的加密算法,大家可以进一步去研究 java 签名证书规则。
我们还有一个折中的方法就是,在客户端启动的时候,调用服务端接口把文件cert.rsa通过流的方式传递给服务端,服务端使用上面截图中命令的方式进行处理,得到sha1值然后返回给客户端,客户端将该值记录在内存变量中使用。
注:我们得到的证书指纹sha1值,建议放在内存变量中使用即可,无需存储到缓存文件中。
下面附上一些其他有关证书方面的一些代码
java 获取私钥信息的代码:
public static void main(string[] args) { try { // 用证书的私钥解密 - 该私钥存在生成该证书的密钥库中 fileinputstream fis2 = new fileinputstream("g:\\test.keystore"); keystore ks = keystore.getinstance("jks"); // 加载证书库 char[] kspwd = "test".tochararray(); // 证书库密码 char[] keypwd = "test".tochararray(); // 证书密码 string alias = "test";// 别名 ks.load(fis2, kspwd); // 加载证书 privatekey pk2 = (privatekey) ks.getkey(alias, keypwd); // 获取证书私钥 fis2.close(); // cipher c2 = cipher.getinstance("rsa/ecb/pkcs1padding");//jdk默认标准 cipher c2 = cipher.getinstance("rsa/ecb/nopadding");//android默认标准 c2.init(cipher.decrypt_mode, pk2); string prikey = pk2.tostring(); system.out.println("prikey = " + prikey); } catch (exception e) { e.printstacktrace(); } }
java 获取公钥信息的代码:
public static void main(string[] args) throws exception { x509certificate certificate = readsignatureblock(new fileinputstream("g:/meta-inf/cert.rsa")); rsapublickey pk = (rsapublickey) certificate.getpublickey(); sun.security.rsa.rsapublickeyimpl ppk = (sun.security.rsa.rsapublickeyimpl) pk; system.out.println(ppk.getmodulus());// 十进制公钥 system.out.println(ppk.getmodulus().tostring(16));// 十六进制公钥 system.out.println(ppk.getpublicexponent());// 65537 system.out.println(ppk.getalgorithm()); system.out.println(ppk.getformat()); system.out.println(ppk.getalgorithmid()); system.out.println(ppk.getpublicexponent().tostring(16));// 10001 system.out.println(new biginteger(ppk.getencoded()).tostring(16)); // 其中new // biginteger(ppk.getencoded()).tostring(16)的字符串包含字符串ppk.getmodulus().tostring(16)和字符串ppk.getpublicexponent().tostring(16)。 // 也可以读取整个rsa文件的字节流,从中获取公钥及modulus,序列号等信息。 inputstream in2 = new fileinputstream("g:/meta-inf/cert.rsa"); byte[] bs = new byte[2048]; int len = in2.read(bs); byte[] bs2 = new byte[len]; system.arraycopy(bs, 0, bs2, 0, len); system.out.println(new biginteger(bs2).tostring(16));// 公钥的16进制字符串也在其中。 }
sha1加密代码:
public class sha1 { private final int[] abcde = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 }; // 摘要数据存储数组 private int[] digestint = new int[5]; // 计算过程中的临时数据存储数组 private int[] tmpdata = new int[80]; // 计算sha-1摘要 private int process_input_bytes(byte[] bytedata) { // 初试化常量 system.arraycopy(abcde, 0, digestint, 0, abcde.length); // 格式化输入字节数组,补10及长度数据 byte[] newbyte = bytearrayformatdata(bytedata); // 获取数据摘要计算的数据单元个数 int mcount = newbyte.length / 64; // 循环对每个数据单元进行摘要计算 for (int pos = 0; pos < mcount; pos++) { // 将每个单元的数据转换成16个整型数据,并保存到tmpdata的前16个数组元素中 for (int j = 0; j < 16; j++) { tmpdata[j] = bytearraytoint(newbyte, (pos * 64) + (j * 4)); } // 摘要计算函数 encrypt(); } return 20; } // 格式化输入字节数组格式 private byte[] bytearrayformatdata(byte[] bytedata) { // 补0数量 int zeros = 0; // 补位后总位数 int size = 0; // 原始数据长度 int n = bytedata.length; // 模64后的剩余位数 int m = n % 64; // 计算添加0的个数以及添加10后的总长度 if (m < 56) { zeros = 55 - m; size = n - m + 64; } else if (m == 56) { zeros = 63; size = n + 8 + 64; } else { zeros = 63 - m + 56; size = (n + 64) - m + 64; } // 补位后生成的新数组内容 byte[] newbyte = new byte[size]; // 复制数组的前面部分 system.arraycopy(bytedata, 0, newbyte, 0, n); // 获得数组append数据元素的位置 int l = n; // 补1操作 newbyte[l++] = (byte) 0x80; // 补0操作 for (int i = 0; i < zeros; i++) { newbyte[l++] = (byte) 0x00; } // 计算数据长度,补数据长度位共8字节,长整型 long n = (long) n * 8; byte h8 = (byte) (n & 0xff); byte h7 = (byte) ((n >> 8) & 0xff); byte h6 = (byte) ((n >> 16) & 0xff); byte h5 = (byte) ((n >> 24) & 0xff); byte h4 = (byte) ((n >> 32) & 0xff); byte h3 = (byte) ((n >> 40) & 0xff); byte h2 = (byte) ((n >> 48) & 0xff); byte h1 = (byte) (n >> 56); newbyte[l++] = h1; newbyte[l++] = h2; newbyte[l++] = h3; newbyte[l++] = h4; newbyte[l++] = h5; newbyte[l++] = h6; newbyte[l++] = h7; newbyte[l++] = h8; return newbyte; } private int f1(int x, int y, int z) { return (x & y) | (~x & z); } private int f2(int x, int y, int z) { return x ^ y ^ z; } private int f3(int x, int y, int z) { return (x & y) | (x & z) | (y & z); } private int f4(int x, int y) { return (x << y) | x >>> (32 - y); } // 单元摘要计算函数 private void encrypt() { for (int i = 16; i <= 79; i++) { tmpdata[i] = f4(tmpdata[i - 3] ^ tmpdata[i - 8] ^ tmpdata[i - 14] ^ tmpdata[i - 16], 1); } int[] tmpabcde = new int[5]; for (int i1 = 0; i1 < tmpabcde.length; i1++) { tmpabcde[i1] = digestint[i1]; } for (int j = 0; j <= 19; j++) { int tmp = f4(tmpabcde[0], 5) + f1(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4] + tmpdata[j] + 0x5a827999; tmpabcde[4] = tmpabcde[3]; tmpabcde[3] = tmpabcde[2]; tmpabcde[2] = f4(tmpabcde[1], 30); tmpabcde[1] = tmpabcde[0]; tmpabcde[0] = tmp; } for (int k = 20; k <= 39; k++) { int tmp = f4(tmpabcde[0], 5) + f2(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4] + tmpdata[k] + 0x6ed9eba1; tmpabcde[4] = tmpabcde[3]; tmpabcde[3] = tmpabcde[2]; tmpabcde[2] = f4(tmpabcde[1], 30); tmpabcde[1] = tmpabcde[0]; tmpabcde[0] = tmp; } for (int l = 40; l <= 59; l++) { int tmp = f4(tmpabcde[0], 5) + f3(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4] + tmpdata[l] + 0x8f1bbcdc; tmpabcde[4] = tmpabcde[3]; tmpabcde[3] = tmpabcde[2]; tmpabcde[2] = f4(tmpabcde[1], 30); tmpabcde[1] = tmpabcde[0]; tmpabcde[0] = tmp; } for (int m = 60; m <= 79; m++) { int tmp = f4(tmpabcde[0], 5) + f2(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4] + tmpdata[m] + 0xca62c1d6; tmpabcde[4] = tmpabcde[3]; tmpabcde[3] = tmpabcde[2]; tmpabcde[2] = f4(tmpabcde[1], 30); tmpabcde[1] = tmpabcde[0]; tmpabcde[0] = tmp; } for (int i2 = 0; i2 < tmpabcde.length; i2++) { digestint[i2] = digestint[i2] + tmpabcde[i2]; } for (int n = 0; n < tmpdata.length; n++) { tmpdata[n] = 0; } } // 4字节数组转换为整数 private int bytearraytoint(byte[] bytedata, int i) { return ((bytedata[i] & 0xff) << 24) | ((bytedata[i + 1] & 0xff) << 16) | ((bytedata[i + 2] & 0xff) << 8) | (bytedata[i + 3] & 0xff); } // 整数转换为4字节数组 private void inttobytearray(int intvalue, byte[] bytedata, int i) { bytedata[i] = (byte) (intvalue >>> 24); bytedata[i + 1] = (byte) (intvalue >>> 16); bytedata[i + 2] = (byte) (intvalue >>> 8); bytedata[i + 3] = (byte) intvalue; } // 将字节转换为十六进制字符串 private static string bytetohexstring(byte ib) { char[] digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; char[] ob = new char[2]; ob[0] = digit[(ib >>> 4) & 0x0f]; ob[1] = digit[ib & 0x0f]; string s = new string(ob); return s; } // 将字节数组转换为十六进制字符串 private static string bytearraytohexstring(byte[] bytearray) { string strdigest = ""; for (int i = 0; i < bytearray.length; i++) { strdigest += bytetohexstring(bytearray[i]); } return strdigest; } // 计算sha-1摘要,返回相应的字节数组 public byte[] getdigestofbytes(byte[] bytedata) { process_input_bytes(bytedata); byte[] digest = new byte[20]; for (int i = 0; i < digestint.length; i++) { inttobytearray(digestint[i], digest, i * 4); } return digest; } // 计算sha-1摘要,返回相应的十六进制字符串 public string getdigestofstring(byte[] bytedata) { return bytearraytohexstring(getdigestofbytes(bytedata)); } public static void main(string[] args) { string data = "1234556"; system.out.println(data); string digest = new sha1().getdigestofstring(data.getbytes()); system.out.println(digest); // system.out.println( tomd5.convertsha1(data).touppercase()); } }
以上这篇android 关于利用签名的sha1进行安全校验的方法之一(推荐)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。
上一篇: webgl绘制线段和三角形