基于JNI的秘钥保护方案

基于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.keyout 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层源码都可以进行下载阅读:

点击下载源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值