基于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的文件下发,这确实是一种比较好的方案。