行业相关,对安全性较高的程序一般都需要添加完整性检测的功能,以防止程序被篡改,从而导致安全问题的发生。
相关的支付应用项目今年也做了好几个,这些程序也都已通过了行业相关安全标准的认证。
下面来分享Android APP完整性校验的实现思路和代码实现。
通过sp判断当前是否是第一次安装apk,第一次安装默认apk是从市场下载安装,默认认为是没有被篡改过的。可以不用检查,只计算当前的hash值并保存到文件中。
可以在application中执行,计算apk的hash值并写文件的操作是耗时操作,记得开子线程进行。
private boolean integrityCheckResult = false;
private boolean isFirstRun;//可以通过文件保存,例如SP
@Override
public void onCreate() {
super.onCreate();
ThreadPoolManager.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
//检测apk完整性
if (isFirstRun){//skip and calculate apk’s hash
SecurityManager.getInstance().checkIntegrity(true);
integrityCheckResult = true;
}else {
integrityCheckResult = SecurityManager.getInstance().checkIntegrity(false);
}
}
});
public boolean isIntegrityCheckResult() {
return integrityCheckResult;
}
在入口activity中判断是否完整性校验通过,假如不通过,可以弹窗提示然后锁定APP,让用户重新在安全的平台重新下载安装。当前APP无法使用,存在安全问题。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (App.getApp().isIntegrityCheckResult()) {
Log.d(TAG, "onCreate: checkIntegrity success");
} else {
Log.d(TAG, "onCreate: checkIntegrity failed");
}
}
新建一个安全管理类,用于管理所有和安全相关的类
public class SecurityManager {
//做一个单例
private static SecurityManager instance = null;
private final Integrity integrity;
private SecurityManager(){
integrity = new Integrity();
}
public static synchronized SecurityManager getInstance() {
if (instance == null)
instance = new SecurityManager();
return instance;
}
public boolean checkIntegrity(boolean isFirstInstall) {
return integrity.checkIntegrity(isFirstInstall);
}
}
实现完整性检测类的接口
public interface IIntegrity {
boolean checkApkIntegrity();
}
完整性检测实现类:
public class Integrity implements IIntegrity {
public boolean checkIntegrity(boolean isFirstInstall) {
if (isFirstInstall) {
calcAndSaveApkSoHash();
return true;
} else {
return compareHashsWithLastTime();
}
}
private void calcAndSaveApkSoHash() {
File apk = new File(BaseApplication.getAppContext().getPackageCodePath());
byte[] apkHash = HashCalculator.calculateHashBytes(apk, HashCalculator.SHA_256);
FileUtils.writeBytesToFile(filePath + APK_HASH_FILE, apkHash);
}
private boolean compareHashsWithLastTime() {
//检测apk so
return checkApkIntegrity();
}
@Override
public boolean checkApkIntegrity() {
if (BuildConfig.DEBUG) {
Log.w(TAG, "Debug version,skip apk‘s hash verification");
return true;
}
try {
String apkPath = BaseApplication.getAppContext().getPackageCodePath();
byte[] originalApkHash = FileUtils.readFileToBytes(filePath + APK_HASH_FILE);
return calcSrcAndCompareWithLastHash(originalApkHash, new File(apkPath));
} catch (IOException e) {
Log.e(TAG, "checkApkAndLibs: ", e);
}
return false;
}
/**
* 计算明文数据并和上一次hash进行比较
*
* @param decHashBytes 明文hash数据
* @param decSrc 明文源数据
*/
private static boolean calcSrcAndCompareWithLastHash(byte[] decHashBytes, File decSrc) {
String decHash = Utils.bcd2Str(decHashBytes);
//计算解密ksn的hash
String calcHash = HashCalculator.calculateHash(decSrc, HashCalculator.SHA_256);
LogUtils.i(TAG,
"calculate hash = " + Utils.bcd2Str(
HashCalculator.calculateHashBytes(decSrc, HashCalculator.SHA_256)));
return decHash.equalsIgnoreCase(calcHash);
}
}
这个只是工具类,方便获取Application ,只要获取context即可,可以随意发挥。
public class BaseApplication extends Application {
private static BaseApplication mBaseApplication ;
mBaseApplication = this;
}
public static BaseApplication getAppContext(){
return mBaseApplication;
}
编码转换工具:
@NonNull
public static String bcd2Str(@Nullable byte[] b, int length) {
if (b == null) {
return "";
}
StringBuilder sb = new StringBuilder(length * 2);
for (int i = 0; i < length; ++i) {
sb.append(ARRAY_OF_CHAR[((b[i] & 0xF0) >>> 4)]);
sb.append(ARRAY_OF_CHAR[(b[i] & 0xF)]);
}
return sb.toString();
}
hash计算器
public class HashCalculator {
private static final String TAG = "HashCalculator";
public static final String SHA_256 = "SHA-256";
/**
* 计算hash
*/
public static String calculateHash(byte[] data, String algorithm) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance(algorithm);
digest.update(data);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
if (digest == null) {
return "";
}
byte[] hashBytes = digest.digest();
return hashBytesToHashString(hashBytes);
}
/**
* 计算hash
* @param file
* @param algorithm
* @return
*/
public static String calculateHash(File file, String algorithm) {
byte[] hashBytes = calculateHashBytes(file, algorithm);
return hashBytesToHashString(hashBytes);
}
/**
* bytes -》 hash
* @param hashBytes
* @return
*/
public static String hashBytesToHashString(byte[] hashBytes) {
StringBuilder hash = new StringBuilder();
for (byte hashByte : Objects.requireNonNull(hashBytes)) {
//将每个字节的十六进制表示添加到 hash 中。这里使用 & 0xff 是为了确保将字节转换为无符号整数,+ 0x100 是为了确保每个字节都至少有两位十六进制数。
//(hashByte & 0xff):这部分代码通过位运算将 hashByte 转换为无符号整数,只保留了最低的 8 位。这是为了确保转换后的值在 0 到 255 之间,不受负数的影响。
//+ 0x100:这部分代码将转换后的无符号整数加上 256(0x100 的十进制值),目的是确保结果至少为三位数。这是为了防止在转换为十六进制字符串时,如果只有一位或两位,会导致结果不满足两位的十六进制表示要求。
//Integer.toString(..., 16):将上一步得到的整数值以十六进制的格式转换为字符串。
//.substring(1):这是为了去掉前面添加的额外一位,因为我们通过 + 0x100 确保了结果至少为三位数,但我们只需要后面的两位。
//例如,假设 hashByte 的值是 10(0x0A),那么经过上述步骤后,会得到字符串 "0A",表示十六进制的值 10。
hash.append(Integer.toString((hashByte & 0xff) + 0x100, 16).substring(1));
}
return hash.toString();
}
/**
* 计算hash
*/
public static byte[] calculateHashBytes(byte[] data, String algorithm) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance(algorithm);
digest.update(data);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
if (digest == null) {
return null;
}
return digest.digest();
}
public static byte[] calculateHashBytes(File file, String algorithm) {
MessageDigest digest = null;
InputStream mInputStream = null;
try {
digest = MessageDigest.getInstance(algorithm);
mInputStream = new FileInputStream(file);
byte[] buffer = new byte[8192];//缓冲区,作为“块”
int read;
while ((read = mInputStream.read(buffer)) > 0) {//逐个块地读取文件内容
digest.update(buffer, 0, read);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
} finally {
try {
if (mInputStream != null) {
mInputStream.close();
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
if (digest == null) {
return null;
}
return digest.digest();
}
}
文件工具类
/**
* 文件锁定(File Locking)
* 强制刷新缓冲(Force Flushing Buffer):
*/
public static boolean writeBytesToFile(String filePath, byte[] bytes) {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(bytes);
// 获取文件锁定
FileChannel fileChannel = fos.getChannel();
try (FileLock fileLock = fileChannel.lock()) {
// 强制刷新缓冲
fileChannel.force(true);
}
return true;
} catch (IOException e) {
LogUtils.e(e);
return false;
}
}