/*
*
* Copyright (c) 2015, alipay.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alipay.euler.andfix.security;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
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.jar.JarEntry;
import java.util.jar.JarFile;
import javax.security.auth.x500.X500Principal;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils;
import android.util.Log;
/**
* security check
*
* @author sanping.li@alipay.com
*
*/
public class SecurityChecker {
private static final String TAG = "SecurityChecker";
private static final String SP_NAME = "_andfix_";
private static final String SP_MD5 = "-md5";
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);
}
/**
* @param path
* Dex file
* @return true if verify fingerprint success
*/
public boolean verifyOpt(File file) {
Log.d("euler", "SecurityChecker, verifyOpt");
String fingerprint = getFileMD5(file);
String saved = getFingerprint(file.getName());
if (fingerprint != null && TextUtils.equals(fingerprint, saved)) {
return true;
}
return false;
}
/**
* @param path
* Dex file
*/
public void saveOptSig(File file) {
Log.d("euler", "SecurityChecker, saveOptSig");
String fingerprint = getFileMD5(file);
saveFingerprint(file.getName(), fingerprint);
}
/**
* @param path
* Apk file
* @return true if verify apk success
*/
public boolean verifyApk(File path) {
Log.d("euler", "SecurityChecker, verifyApk");
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();
if (certs == null) {
return false;
}
return 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 {
Log.d("euler", "SecurityChecker, loadDigestes");
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) {
Log.d("euler", "SecurityChecker, check");
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;
}
private String getFileMD5(File file) {
Log.d("euler", "SecurityChecker, getFileMD5");
if (!file.isFile()) {
return null;
}
MessageDigest digest = null;
FileInputStream in = null;
byte buffer[] = new byte[8192];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer)) != -1) {
digest.update(buffer, 0, len);
}
} catch (Exception e) {
Log.e(TAG, "getFileMD5", e);
return null;
} finally {
try {
if (in != null)
in.close();
} catch (IOException e) {
Log.e(TAG, "getFileMD5", e);
}
}
BigInteger bigInt = new BigInteger(digest.digest());
return bigInt.toString();
}
// md5 as fingerprint
private void saveFingerprint(String fileName, String md5) {
Log.d("euler", "SecurityChecker, saveFingerprint");
SharedPreferences sharedPreferences = mContext.getSharedPreferences(
SP_NAME, Context.MODE_PRIVATE);
Editor editor = sharedPreferences.edit();
editor.putString(fileName + SP_MD5, md5);
editor.commit();
}
private String getFingerprint(String fileName) {
Log.d("euler", "SecurityChecker, getFingerprint");
SharedPreferences sharedPreferences = mContext.getSharedPreferences(
SP_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getString(fileName + SP_MD5, null);
}
// initialize,and check debuggable
private void init(Context context) {
Log.d("euler", "SecurityChecker, init");
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);
}
}
}