基于Android签名机制的文件安全下发方法

问题背景

Android中存在大量需要动态下发文件的场景:

  • 下发布局资源文件实现UI动态化

  • 下发Dex文件、lua文件,实现局部功能的动态化

  • 下发Android插件

  • 下发多媒体资源等

  • 等等

对于其中一些文件,如Dex、lua可以通过混淆、编译做一定程度的保护,但是仍然潜在被篡改,进而攻击系统内部的可能,因此,在使用动态下发的文件之前,最好有一种机制,能够做到

  • 防篡改(内容可信)

  • 防劫持(来源可信)

方法描述

structure

这个方法最投机的一点就是利用了一个前提:宿主已经安装成功,说明公钥是相对可信的(由于是自签名,所以不是绝对可信的)。那么,第三方想要篡改,由于没有私钥,必然导致宿主加载失败。

这么看似乎更直接一点: Snipaste 2020 03 11 20 19 36

实例演示

根据keystore提取私钥,并进行签名(服务器端):

    public static void run(HashMap<String, String> args) throws Exception{
        String keyStoreType = "jks";
        String keystoreFile = args.get(INPUT);
        String password = args.get(PASS);
        String alias = args.get(ALIAS);
        String aliasPassword = args.get(ALIASPASS);
        if (keystoreFile == null || password == null || alias == null || aliasPassword == null) {
            return;
        }
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(new FileInputStream(keystoreFile), password.toCharArray());
        KeyPair keyPair = getKeyPair(keyStore, alias, aliasPassword.toCharArray());
        if (keyPair != null) {
            String  ret = RsaUtils.sign(FileUtils.toByteArray2("./demo.dex"), Base64.encode(keyPair.getPrivate().getEncoded()));
            System.out.println(ret);
        }
    }

	// RsaUtils.java
    public static String sign(byte[] data, String privateKey) throws Exception {
        byte[] keyBytes = Base64.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initSign(privateK);
        signature.update(data);
        return Base64.encode(signature.sign());
    }

根据以下代码生成一个Dex,用于动态加载:

public class Test {
    public void  toast(Context c) {
        Toast.makeText(c, "just do it~~~", Toast.LENGTH_SHORT).show();
    }
}

获取宿主的公钥,并验证Dex的签名:

		....
        String base64Key = GetPublicKey.getSignInfo(this);
        try {
            boolean b = RsaUtils.verify(FileUtils.toByteArray2(filePath),  base64Key, sign);
            if (! b) {
                return;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        DexClassLoader dexClassLoader = new DexClassLoader(filePath, path, null, getClassLoader());
        Class c = null;
        try {
            c = dexClassLoader.loadClass("com.tencent.demo.Test");
            Object obj = c.newInstance();

            Method getImeiMethod = c.getMethod("toast", Context.class);
            getImeiMethod.setAccessible(true);
            getImeiMethod.invoke(obj, new Object[] {this});

        }
    ......
    // GetPublicKey.java
    protected static String getSignInfo(Context mContext) {
        String signcode = "";
        try {
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES);
            Signature[] signs = packageInfo.signatures;
            Signature sign = signs[0];
            signcode = parseSignature(sign.toByteArray());
        } catch (Exception e) { }
        return signcode;
    }

    protected static String parseSignature(byte[] signature) {
        String sign = "";
        try {
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(signature));
            sign = Base64.encodeToString(cert.getPublicKey().getEncoded(), Base64.DEFAULT);
        } catch (CertificateException e) {}
        return sign;
    }

    // RsaUtils.java
    public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {
        byte[] keyBytes = Base64.decode(publicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initVerify(publicK);
        signature.update(data);
        return signature.verify(Base64.decode(sign));
    }

效果:

demo android dynamic

局限性

对于第三方SDK的动态化及类似场景并不适用,因为依赖于宿主的密钥,不同宿主的签名不一样,会变得十分繁琐,除非搭建一套后台系统,由SDK接入方适配。

structure02

对于安全性追求较高的或者单一App的文件下发,这确实是一种比较好的方案。