需求和背景
行业相关,对安全性较高的程序一般都需要添加完整性检测的功能,以防止程序被篡改,从而导致安全问题的发生。
相关的支付应用项目今年也做了好几个,这些程序也都已通过了行业相关安全标准的认证。
实现
下面来分享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计算器
@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(); }
文件工具类
/** * 文件锁定(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; } }