android 關(guān)于利用簽名的SHA1進行安全校驗的方法之一(推薦)
最近做安卓項目中使用到了百度地圖的API,在申請百度地圖key的時候,需要我們填入“簽名的SHA1”和“客戶端包名”,然后百度為我們生成一個key。
于是就引發(fā)了思考,百度為何需要我們客戶端簽名的SHA1值呢?
第一想法就是:百度拿我們輸入的參數(shù)SHA1和包名進行一些列算法計算,生成一個key返回給我們。
為了證明這個想法,寫了demo進行測試,android獲取包名的方法很簡單,但是我們還需要從客戶端中獲取keystore的指紋SHA1。
進行各種資料的查找和分析,才得出方法。
一、首先,科普一下apk包下的META-INF目錄
我們已經(jīng)知道的是:Android對每一個Apk文件都會進行簽名,在Apk文件安裝時,系統(tǒng)會對其簽名信息進行比對,判斷程序的完整性,從而決定該Apk文件是否可以安裝,在一定程度上達到安全的目的。
給定一個Apk文件,解壓,可以看到一個META-INFO文件夾,在該文件夾下有三個文件:分別為MANIFEST.MF、CERT.SF和CERT.RSA。這三個文件分別表征以下含義:
(1)MANIFEST.MF:這是摘要文件。程序遍歷Apk包中的所有文件(entry),對非文件夾非簽名文件的文件,逐個用SHA1生成摘要信息,再用Base64進行編碼。如果你改變了apk包中的文件,那么在apk安裝校驗時,改變后的文件摘要信息與MANIFEST.MF的檢驗信息不同,于是程序就不能成功安裝。
說明:如果攻擊者修改了程序的內(nèi)容,有重新生成了新的摘要,那么就可以通過驗證,所以這是一個非常簡單的驗證。
(2)CERT.SF:這是對摘要的簽名文件。對前一步生成的MANIFEST.MF,使用SHA1-RSA算法,用開發(fā)者的私鑰進行簽名。在安裝時只能使用公鑰才能解密它。解密之后,將它與未加密的摘要信息(即,MANIFEST.MF文件)進行對比,如果相符,則表明內(nèi)容沒有被異常修改。
說明:在這一步,即使開發(fā)者修改了程序內(nèi)容,并生成了新的摘要文件,但是攻擊者沒有開發(fā)者的私鑰,所以不能生成正確的簽名文件(CERT.SF)。系統(tǒng)在對程序進行驗證的時候,用開發(fā)者公鑰對不正確的簽名文件進行解密,得到的結(jié)果和摘要文件(MANIFEST.MF)對應(yīng)不起來,所以不能通過檢驗,不能成功安裝文件。
(3)CERT.RSA文件中保存了公鑰、所采用的加密算法等信息。
說明:系統(tǒng)對簽名文件進行解密,所需要的公鑰就是從這個文件里取出來的。
結(jié)論:從上面的總結(jié)可以看出,META-INFO里面的說那個文件環(huán)環(huán)相扣,從而保證Android程序的安全性。(只是防止開發(fā)者的程序不被攻擊者修改,如果開發(fā)者的公私鑰對對攻擊者得到或者開發(fā)者開發(fā)出攻擊程序,Android系統(tǒng)都無法檢測出來。)
我們將apk包解包,然后使用命令 keytool -printcert -file CERT.RSA 查看CERT.RSA,如圖所示:

答案很明顯,CERT.RSA文件中存放了關(guān)于簽名的信息。
當(dāng)我們使用如下代碼獲取證書信息的時候:
/**
* 獲取指定包名程序的簽名信息
*
* @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可以發(fā)現(xiàn),X509Certificate類并沒有提供直接可以獲取SHA1值的方法。
研究簽名文件的加密算法之后,我們看可以根據(jù)publicKey自己計算出SHA1的值,計算方法為:
> 通過 X509Certificate 的 getEncoded() 方法獲得公鑰的字節(jié)數(shù)組。
> 使用 MessageDigest 對字節(jié)數(shù)組進行SHA1的 degest 處理,得到新的字節(jié)數(shù)組。
> 然后使用16進制對得到新字節(jié)數(shù)組進行轉(zhuǎn)換,最終得到證書文件的指紋SHA1。
更詳細的加密算法,大家可以進一步去研究 java 簽名證書規(guī)則。
我們還有一個折中的方法就是,在客戶端啟動的時候,調(diào)用服務(wù)端接口把文件CERT.RSA通過流的方式傳遞給服務(wù)端,服務(wù)端使用上面截圖中命令的方式進行處理,得到SHA1值然后返回給客戶端,客戶端將該值記錄在內(nèi)存變量中使用。
注:我們得到的證書指紋SHA1值,建議放在內(nèi)存變量中使用即可,無需存儲到緩存文件中。
下面附上一些其他有關(guān)證書方面的一些代碼
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默認標(biāo)準(zhǔn)
Cipher c2 = Cipher.getInstance("RSA/ECB/NoPadding");//android默認標(biāo)準(zhǔn)
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文件的字節(jié)流,從中獲取公鑰及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
};
// 摘要數(shù)據(jù)存儲數(shù)組
private int[] digestInt = new int[5];
// 計算過程中的臨時數(shù)據(jù)存儲數(shù)組
private int[] tmpData = new int[80];
// 計算sha-1摘要
private int process_input_bytes(byte[] bytedata) {
// 初試化常量
System.arraycopy(abcde, 0, digestInt, 0, abcde.length);
// 格式化輸入字節(jié)數(shù)組,補10及長度數(shù)據(jù)
byte[] newbyte = byteArrayFormatData(bytedata);
// 獲取數(shù)據(jù)摘要計算的數(shù)據(jù)單元個數(shù)
int MCount = newbyte.length / 64;
// 循環(huán)對每個數(shù)據(jù)單元進行摘要計算
for (int pos = 0; pos < MCount; pos++) {
// 將每個單元的數(shù)據(jù)轉(zhuǎn)換成16個整型數(shù)據(jù),并保存到tmpData的前16個數(shù)組元素中
for (int j = 0; j < 16; j++) {
tmpData[j] = byteArrayToInt(newbyte, (pos * 64) + (j * 4));
}
// 摘要計算函數(shù)
encrypt();
}
return 20;
}
// 格式化輸入字節(jié)數(shù)組格式
private byte[] byteArrayFormatData(byte[] bytedata) {
// 補0數(shù)量
int zeros = 0;
// 補位后總位數(shù)
int size = 0;
// 原始數(shù)據(jù)長度
int n = bytedata.length;
// 模64后的剩余位數(shù)
int m = n % 64;
// 計算添加0的個數(shù)以及添加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;
}
// 補位后生成的新數(shù)組內(nèi)容
byte[] newbyte = new byte[size];
// 復(fù)制數(shù)組的前面部分
System.arraycopy(bytedata, 0, newbyte, 0, n);
// 獲得數(shù)組Append數(shù)據(jù)元素的位置
int l = n;
// 補1操作
newbyte[l++] = (byte) 0x80;
// 補0操作
for (int i = 0; i < zeros; i++) {
newbyte[l++] = (byte) 0x00;
}
// 計算數(shù)據(jù)長度,補數(shù)據(jù)長度位共8字節(jié),長整型
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);
}
// 單元摘要計算函數(shù)
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字節(jié)數(shù)組轉(zhuǎn)換為整數(shù)
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);
}
// 整數(shù)轉(zhuǎn)換為4字節(jié)數(shù)組
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;
}
// 將字節(jié)轉(zhuǎn)換為十六進制字符串
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;
}
// 將字節(jié)數(shù)組轉(zhuǎn)換為十六進制字符串
private static String byteArrayToHexString(byte[] bytearray) {
String strDigest = "";
for (int i = 0; i < bytearray.length; i++) {
strDigest += byteToHexString(bytearray[i]);
}
return strDigest;
}
// 計算sha-1摘要,返回相應(yīng)的字節(jié)數(shù)組
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摘要,返回相應(yīng)的十六進制字符串
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 關(guān)于利用簽名的SHA1進行安全校驗的方法之一(推薦)就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Android Handler內(nèi)存泄漏原因及解決方案
這篇文章主要介紹了Android Handler內(nèi)存泄漏原因及解決方案,幫助大家更好的理解和利用Android進行開發(fā),感興趣的朋友可以了解下2021-02-02
Android自定義ScrollView實現(xiàn)放大回彈效果實例代碼
本篇文章主要介紹了Android自定義ScrollView實現(xiàn)放大回彈效果實例代碼,具有一定的參考價值,有興趣的可以了解一下。2017-03-03
Android SQLite數(shù)據(jù)庫的增 刪 查找操作
這篇文章主要介紹了Android SQLite數(shù)據(jù)庫的增 刪 查找操作,需要的朋友可以參考下2017-02-02
Android開發(fā)常用標(biāo)簽小結(jié)
這篇文章主要介紹了Android開發(fā)常用標(biāo)簽,分析總結(jié)了Android開發(fā)中常見標(biāo)簽的使用技巧,需要的朋友可以參考下2015-05-05
Android SdkVersion的區(qū)別及獲取版本信息方法
下面小編就為大家?guī)硪黄狝ndroid SdkVersion的區(qū)別及獲取版本信息方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03
Android 連接Wifi和創(chuàng)建Wifi熱點的實例
本篇文章介紹了Android 連接Wifi和創(chuàng)建Wifi熱點,小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧。2016-10-10
Android開發(fā)自學(xué)筆記(三):APP布局上
這篇文章主要介紹了Android開發(fā)自學(xué)筆記(三):APP布局上,本文講解了添加ViewGroup、添加ViewGroup、定義string內(nèi)容、添加Button、運行程序查看效果等內(nèi)容,需要的朋友可以參考下2015-04-04
Android判斷手機是否聯(lián)網(wǎng)及自動跳轉(zhuǎn)功能(收藏版)
這篇文章主要介紹了Android判斷手機是否聯(lián)網(wǎng)及自動跳轉(zhuǎn)功能(收藏版),在一些手機端連接wifi我們經(jīng)常會遇到這樣的功能,今天小編通過實例截圖給大家介紹下,需要的朋友可以參考下2019-11-11

