Android apk完整性检测的实现思路和实现过程全记录

需求和背景

行业相关,对安全性较高的程序一般都需要添加完整性检测的功能,以防止程序被篡改,从而导致安全问题的发生。
相关的支付应用项目今年也做了好几个,这些程序也都已通过了行业相关安全标准的认证。

实现

下面来分享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;
        }
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林树杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值