签名校验和完整性校验主要是针对于二次打包的检测防范措施,如果没有签名校验和完整性校验功能,应用可能被恶意攻击者二次打包,被盗版的风险大大增加,同时也可能进行任意代码修改。
针对上面的问题,这章我们就对我们的App进行运行时签名校验。
项目代码
项目代码的主要实现类SignatureChecker
,欢迎查看。
app-release-unsigned
进行签名,签名后的APK为app-release-keystoreA
jarsigner -verbose -keystore /Users/martin/Downloads/newsign/keystore/keystoreA -signedjar /Users/martin/Downloads/newsign/des/app-release-keystoreA.apk /Users/martin/Downloads/newsign/src/app-release-unsigned.apk android
jarsigner的参数说明
PS:参考MAC对APK包进行重新签名
解压app-release-keystoreA
删除META-INFO文件夹下的三个文件
使用我们的keyStoreB
对app-release-keystoreA
进行重新签名,生成app-release-keystoreB
jarsigner -verbose -keystore /Users/martin/Downloads/newsign/keystore/keystoreB -signedjar /Users/martin/Downloads/newsign/des/app-release-keystoreB.apk /Users/martin/Downloads/newsign/src/app-release-keystoreA.apk androidx
adb install -r /Users/martin/Downloads/newsign/des/app-release-keystoreA.apk
生成MD5值:
8ad65c2224d9c303250c7c49a3672323
adb install -r /Users/martin/Downloads/newsign/des/app-release-keystoreA.apk
生成MD5值:
8fe878055ba594ee41ba8a4b2e0ca3e5
注意:这里的MD5值不和KeyStore显示的格式一样,是因为我格式再次进行了MD5加密。
可以看出来,即使是同样的APK,但是使用不同的签名还是会出现不同的APK值。所以,我们可以在应用中做校验,当MD5值不一样时,就不是官方正版的APK了。
刚才上面已经说过了,不同的签名文件对APK打包后,肯定签名信息也是不同的,那么我们就可以针对这点,来对签名进行校验,来确保我们的App是否被别人"盗版"。这里我对SHA1和MD5都做了校验比较。
SHA1的校验代码。
public class SignatureValidator {
...
public String getCertificateSHA1Fingerprint() {
//获取包管理器
PackageManager pm = context.getPackageManager();
//获取当前要获取 SHA1 值的包名,也可以用其他的包名,但需要注意,
//在用其他包名的前提是,此方法传递的参数 Context 应该是对应包的上下文。
String packageName = context.getPackageName();
//返回包括在包中的签名信息
int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
//获得包的所有内容信息类
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
//签名信息
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
//将签名转换为字节数组流
InputStream input = new ByteArrayInputStream(cert);
//证书工厂类,这个类实现了出厂合格证算法的功能
CertificateFactory cf = null;
try {
cf = CertificateFactory.getInstance("X509");
} catch (Exception e) {
e.printStackTrace();
}
//X509 证书,X.509 是一种非常通用的证书格式
X509Certificate c = null;
try {
c = (X509Certificate) cf.generateCertificate(input);
} catch (Exception e) {
e.printStackTrace();
}
String hexString = null;
try {
//加密算法的类,这里的参数可以使 MD4,MD5 等加密算法
MessageDigest md = MessageDigest.getInstance("SHA1");
//获得公钥
byte[] publicKey = md.digest(c.getEncoded());
//字节到十六进制的格式转换
hexString = byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
} catch (CertificateEncodingException e) {
e.printStackTrace();
}
return hexString;
}
/**
* 这里是将获取到得编码进行16 进制转换
*
* @param arr
* @return
*/
private String byte2HexFormatted(byte[] arr) {
StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i <arr.length; i++) {
String h = Integer.toHexString(arr[i]);
int l =h.length();
if (l == 1)
h = "0" + h;
if (l > 2)
h = h.substring(l - 2, l);
str.append(h.toUpperCase());
if (i < (arr.length - 1))
str.append(':');
}
return str.toString();
}
/**
* 检测SHA1签名是否正确
* @return true 签名正常 false 签名不正常
*/
public boolean checkSHA1() {
if (this.realCer != null) {
cer = EncryptUtil.MD5(getCertificateSHA1Fingerprint().trim());//再次MD5加密
realCer = realCer.trim();
if (this.cer.equals(this.realCer)) {
return true;
}
}else {
Log.e(TAG, "未给定真实的签名 SHA-1 值");
}
return false;
}
...
}
上面的代码需要注意的一个地方就是cer = EncryptUtil.MD5(getCertificateSHA1Fingerprint().trim())
,这里我对Keystore的签名SHA1信息再次做了MD5的加密,这样就确保了我们的密钥的安全性。
/**
* 验证SHA1
*/
public void checkSHA1(String sha1) {
SignatureValidator signatureValidator = new SignatureValidator(this, sha1);
if (signatureValidator.checkSHA1()) {
showToast("ok");
} else {
showDialog();
}
String signature_sha1 = signatureValidator.getCertificateSHA1Fingerprint();
signaturText.setText("SHA1:\t" + signature_sha1);
Log.d(AppConfig.TAG, signature_sha1);
}