基于Android签名机制的文件安全下发方法
问题背景
Android中存在大量需要动态下发文件的场景:
-
下发布局资源文件实现UI动态化
-
下发Dex文件、lua文件,实现局部功能的动态化
-
下发Android插件
-
下发多媒体资源等
-
等等
对于其中一些文件,如Dex、lua可以通过混淆、编译做一定程度的保护,但是仍然潜在被篡改,进而攻击系统内部的可能,因此,在使用动态下发的文件之前,最好有一种机制,能够做到
-
防篡改(内容可信)
-
防劫持(来源可信)
方法描述
这个方法最投机的一点就是利用了一个前提:宿主已经安装成功,说明公钥是相对可信的(由于是自签名,所以不是绝对可信的)。那么,第三方想要篡改,由于没有私钥,必然导致宿主加载失败。
这么看似乎更直接一点:
实例演示
根据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)); }
效果:
局限性
对于第三方SDK的动态化及类似场景并不适用,因为依赖于宿主的密钥,不同宿主的签名不一样,会变得十分繁琐,除非搭建一套后台系统,由SDK接入方适配。
对于安全性追求较高的或者单一App的文件下发,这确实是一种比较好的方案。