通过云端下发插件补丁的方式,我们可以很方便的用来修复线上bug和发布新功能。但是如何保证接收到的补丁是安全的呢?
很多人想到使用类似校验md5的方式,但是md5值也是通过接口下发,本来就是不安全的,使用fiddler mock接口,md5值想返回什么都行。
其实安卓的apk也是有签名的,假如补丁包也能使用apk相同的证书签名,然后对比下发补丁的签名和当前运行的apk签名是否一致就行了。
1、对补丁进行zip压缩
atlas的插件补丁文件有2个,一个是update.json,另一个是tpatch文件。对这两个文件进行压缩得到的zip文件名是patch.zip
2、使用apk的release证书对patch.zip签名
执行以下签名命令:
jarsigner -verbose -keystore KEYSTORE_FILEPATH -signedjar patch_v.zip patch.zip dengyin2000
-keystore 后面输入apk证书的路径
-signedjar 后面输入签名完成后的zip文件名,然后是需要签名的文件名,然后是证书的别名
签名成功后,你就能看到patch_v.zip文件了。
3、使用代码进行验证,SecurityChecker.java
看看代码:
package com.iteye.dengyin2000.android.securitychecker;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.security.auth.x500.X500Principal;
/**
* Created by denny on 2017/5/26.
*/
public class SecurityChecker {
private static final String TAG = "SecurityChecker";
private static final String CLASSES_DEX = "classes.dex";
private static final X500Principal DEBUG_DN = new X500Principal(
"CN=Android Debug,O=Android,C=US");
private final Context mContext;
/**
* host publickey
*/
private PublicKey mPublicKey;
/**
* host debuggable
*/
private boolean mDebuggable;
public SecurityChecker(Context context) {
mContext = context;
init(mContext);
}
public boolean verifyAtlasPatch(File path) {
JarFile jarFile = null;
try {
jarFile = new JarFile(path);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
if ("update.json".equals(jarEntry.getName()) || jarEntry.getName().endsWith(".tpatch")) {
loadDigestes(jarFile, jarEntry);
Certificate[] certs = jarEntry.getCertificates();
if (certs == null) {
return false;
}
if (!check(path, certs)) {
return false;
}
Log.i(TAG, "File: " + jarEntry.getName() + " is verified");
}
}
return true;
} catch (IOException e) {
Log.e(TAG, path.getAbsolutePath(), e);
return false;
} finally {
try {
if (jarFile != null) {
jarFile.close();
}
} catch (IOException e) {
Log.e(TAG, path.getAbsolutePath(), e);
}
}
}
/**
* @param path Apk file
* @return true if verify apk success
*/
public boolean verifyApk(File path) {
if (mDebuggable) {
Log.d(TAG, "mDebuggable = true");
return true;
}
JarFile jarFile = null;
try {
jarFile = new JarFile(path);
JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);
if (null == jarEntry) {// no code
return false;
}
loadDigestes(jarFile, jarEntry);
Certificate[] certs = jarEntry.getCertificates();
return certs != null && check(path, certs);
} catch (IOException e) {
Log.e(TAG, path.getAbsolutePath(), e);
return false;
} finally {
try {
if (jarFile != null) {
jarFile.close();
}
} catch (IOException e) {
Log.e(TAG, path.getAbsolutePath(), e);
}
}
}
private void loadDigestes(JarFile jarFile, JarEntry je) throws IOException {
InputStream is = null;
try {
is = jarFile.getInputStream(je);
byte[] bytes = new byte[8192];
while (is.read(bytes) > 0) {
}
} finally {
if (is != null) {
is.close();
}
}
}
// verify the signature of the Apk
private boolean check(File path, Certificate[] certs) {
if (certs.length > 0) {
for (int i = certs.length - 1; i >= 0; i--) {
try {
certs[i].verify(mPublicKey);
return true;
} catch (Exception e) {
Log.e(TAG, path.getAbsolutePath(), e);
}
}
}
return false;
}
// initialize,and check debuggable
private void init(Context context) {
try {
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
PackageInfo packageInfo = pm.getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
CertificateFactory certFactory = CertificateFactory
.getInstance("X.509");
ByteArrayInputStream stream = new ByteArrayInputStream(
packageInfo.signatures[0].toByteArray());
X509Certificate cert = (X509Certificate) certFactory
.generateCertificate(stream);
mDebuggable = cert.getSubjectX500Principal().equals(DEBUG_DN);
mPublicKey = cert.getPublicKey();
} catch (NameNotFoundException e) {
Log.e(TAG, "init", e);
} catch (CertificateException e) {
Log.e(TAG, "init", e);
}
}
}
init方法,获得运行apk的签名publicKey。
verifyAtlasPatch方法,验证zip包里面的update.json和.tpatch文件的签名是否跟apk的签名一致。所以验证补丁包是否合法调用这个方法就行了。:)
网友评论