基于JNI的秘钥保护方案
在接触了Android平台上的Https服务器之后,需要引入秘钥,所以为了不在Java层暴露秘钥,将秘钥相关的所有东西放到JNI层去进行处理。这样,对于秘钥来说,起到一定的保护作用。这篇文章并不会给大家讲解很高深的技术,只是希望能给大家一种思路的引导吧。
方案思路
基于JNI的秘钥保护其实就是讲私钥、私钥密码、证书和秘钥库加载等相关的逻辑全部放在JNI层中实现,在Java层调用本地接口时,只能获得除了以上这些关键信息之外的其他信息,这样便能有效地保护秘钥。
如上图所示,JNI层实现了两个功能:1、apk签名信息的检测 2、秘钥的加载。Java调用本地接口之后,获取一个SSLContext实例,可以通过这个实例,便可以完成Android端Https服务器模块的初始化。
方案实施
实现方法
了解JNI的人应该都知道,Java与JNI是可以互调的。所以,我们这个方案也是基于这一点,JNI层调用Java层来实现上图中所述的两个功能。如果对于JNI层调用Java层上还有不懂得同学,可以先去查找一些资料学习一下。
方法描述
apk签名信息的校验
为什么要有这个签名信息校验呢?为了防止自己的.so动态库可以直接被别人引用。所以才增加了这么一个校验。在Java层,做apk签名校验非常得简单,通过几行代码就可以了:
// 通过包管理器获得指定包名包含签名的包信息
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager .GET_SIGNATURES);
// 通过返回的包信息获得签名数组
Signature[] signatures = packageInfo.signatures;
//获得应用签名的哈希值
return signatures[0].hashCode();
那么,按照上面介绍的实现方法,我们在JNI层来调用相关的Java方法,实现签名校验:
/**
* check signature
* @param env
* @return
*/
jboolean checkSign(JNIEnv *env) {
// Application object
jobject application = getApplication(env);
if (application == NULL) {
return JNI_FALSE;
}
// Context(ContextWrapper) class
jclass context_clz = env->GetObjectClass(application);
// getPackageManager()方法
jmethodID getPackageManager = env->GetMethodID(context_clz, "getPackageManager",
"()Landroid/content/pm/PackageManager;");
// 获取PackageManager实例
jobject package_manager = env->CallObjectMethod(application, getPackageManager);
// PackageManager class
jclass package_manager_clz = env->GetObjectClass(package_manager);
// getPackageInfo()方法
jmethodID getPackageInfo = env->GetMethodID(package_manager_clz, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// getPackageName()方法
jmethodID getPackageName = env->GetMethodID(context_clz, "getPackageName",
"()Ljava/lang/String;");
// 调用getPackageName()
jstring package_name = (jstring) (env->CallObjectMethod(application, getPackageName));
// PackageInfo实例
jobject package_info = env->CallObjectMethod(package_manager, getPackageInfo, package_name, 64);
// PackageInfo class
jclass package_info_clz = env->GetObjectClass(package_info);
// signatures字段
jfieldID signatures_field = env->GetFieldID(package_info_clz, "signatures",
"[Landroid/content/pm/Signature;");
jobject signatures = env->GetObjectField(package_info, signatures_field);
jobjectArray signatures_array = (jobjectArray) signatures;
jobject signature0 = env->GetObjectArrayElement(signatures_array, 0);
// Signature class
jclass signature_clz = env->GetObjectClass(signature0);
jmethodID signatureHashcode = env->GetMethodID(signature_clz, "hashCode", "()I");
jint hashCode = env->CallIntMethod(signature0, signatureHashcode);
if(hashCode == RELEASE_SIGN_HASHCODE)
{
return JNI_TRUE;
}
deleteLocalRef(env, signature_clz);
deleteLocalRef(env, signature0);
deleteLocalRef(env, package_info_clz);
deleteLocalRef(env, package_info);
deleteLocalRef(env, package_manager_clz);
deleteLocalRef(env, package_manager);
deleteLocalRef(env, context_clz);
return JNI_FALSE;
}
上面这段代码中,没有直接从Java层传入Context对象,是因为可以通过Java层传入的Context对象来伪造签名信息,从而可以绕过本地的签名校验。
在JNI中去获取Context实例也很简单,如下:
jobject getApplication(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(
activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL) {
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
} else {
LOGE("Cannot find method: currentApplication() in ActivityThread.");
}
env->DeleteLocalRef(activity_thread_clz);
} else {
LOGE("Cannot find class: android.app.ActivityThread");
}
return application;
}
加载秘钥库
秘钥的格式转换
在Android端可以支持pkcs8格式的秘钥,所以可以利用Openssl来进行格式转换:命令:
openssl pkcs8 -topk8 –in server.key –out server8.key -nocrypt
使用UltralEdit工具打开serve8.key文件,可以看到文件内容如下(base64编码格式)
除去头尾,将其作为字符串为私钥加载时使用:
认证证书的导入
将CA(自创建CA,可以参考 通过OpenSSL自签CA为Android服务器签发证书)签发的服务端证书作为导入目标,将其证书内容到处到文本,如下(base64编码):
将全部内容组作为一串字符串,为秘钥库加载时使用。
- 秘钥库的加载
我们先来看下如果在Java层实现秘钥库的加载:
private SSlContext makeSSLContext(){
SSLContext sslContext = SSLContext.getInstance("TLS");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//获得证书
InputStream pukIn = new ByteArrayInputStream(certificateStr.getBytes());
Certificate certificate = certificateFactory.generateCertificate(pukIn);
//获得私钥
byte[] buffer = Base64Utils.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
//将私钥和证书加载到秘钥库
keyStore.setKeyEntry("server", privateKey, KEYSTORE_PWD.toCharArray(),
new Certificate[]{certificate});
keyManagerFactory.init(keyStore, KEYSTORE_PWD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
//返回SSLContext对象
return sslContext;
}
以上就是在Java实现了秘钥库的加载,并返回SSLContext对象。剩下的就是如何在JNI层实现以上的逻辑了。由于代码量相对较大,我就不直接贴出来了。上面这段代码中涉及到的所有Java和JNI层源码都可以进行下载阅读: