NDK秘钥安全保存

通过native编码,在app初始化时校验app签名特征信息,以动态库so文件的方式实现秘钥的安全保存。

一、秘钥存储方式分析

如果需要在本地存储一个密钥串,典型的方式有

  1. 直接写在java源代码中(明文或者密文区别不大)
  2. 写在gradle脚本中,使用BuildConfig读取
  3. 写在gradle.properties中,再到gradle脚本中读取,后面同第二点
  4. 使用native方法,读取存放在C/C++中的字段

分析

  • 1为硬编码

  • 2可以做到在不同的BuildType使用不同的密钥

  • 3将配置写到脚本之外,方便管理查看

由于java的破解成本低,前3种从本质上讲明文或者密文区别不大,

因此,将密钥放在难以反编译的C/C++代码中,是一个解决的办法。

二、NDK秘钥安全保存

2.1、定义日志

Log.h日志打印通用模块

#include <jni.h>
#include <android/log.h>

#define LOG_TAG "myAnroidApp"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##args)
#define LOGW(fmt, args...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, fmt, ##args)
#define LOGF(fmt, args...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, fmt, ##args)

2.2、java初始化

这里我们通过包名、秘钥来校验包信息的正确,因为秘钥是app最安全的信息。

// 初始化校验
public static native boolean init();
// 获取秘钥
public static native String getKey();

这里之所以把秘钥的校验和获取分开,因为在so文件里面,反编译主要通过内存检测分析,对敏感信息使用越少越不容易被破解。

因此这里比较好的做法是把秘钥存放到app初始化的init函数里面,生命周期只有几毫秒,全局维护一个boolean值(c里面没办法直接看到,看到了也没办法改)

2.3、c++实现

获取秘钥部分参考java示例代码

public String getSignInfo() {
    try {
        PackageInfo packageInfo = getPackageManager().getPackageInfo(
                getPackageName(), PackageManager.GET_SIGNATURES);
        Signature[] signs = packageInfo.signatures;
        Signature sign = signs[0];
        return sign.toCharsString();
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

C++全部实现

#include <jni.h>
#include <string>
#include "Log.h"

const char *APP_PACKAGE_NAME = "com.example.secretkeydemo";
// 验证是否通过
static jboolean auth = JNI_FALSE;

/*
 * 获取全局 Application
 */
jobject getApplicationContext(JNIEnv *env) {
    jclass activityThread = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(activityThread,
                                                             "currentActivityThread",
                                                             "()Landroid/app/ActivityThread;");
    jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
    jmethodID getApplication = env->GetMethodID(activityThread, "getApplication",
                                                "()Landroid/app/Application;");
    return env->CallObjectMethod(at, getApplication);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_secretkeydemo_MainActivity_init(JNIEnv *env, jclass clazz) {
    jclass binderClass = env->FindClass("android/os/Binder");
    jclass contextClass = env->FindClass("android/content/Context");
    jclass signatureClass = env->FindClass("android/content/pm/Signature");
    jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
    jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");

    jmethodID packageManager = env->GetMethodID(contextClass, "getPackageManager",
                                                "()Landroid/content/pm/PackageManager;");
    jmethodID packageName = env->GetMethodID(contextClass, "getPackageName",
                                             "()Ljava/lang/String;");
    jmethodID toCharsString = env->GetMethodID(signatureClass, "toCharsString",
                                               "()Ljava/lang/String;");
    jmethodID packageInfo = env->GetMethodID(packageNameClass, "getPackageInfo",
                                             "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jmethodID nameForUid = env->GetMethodID(packageNameClass, "getNameForUid",
                                            "(I)Ljava/lang/String;");
    jmethodID callingUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");

    jint uid = env->CallStaticIntMethod(binderClass, callingUid);

    // 获取全局 Application
    jobject context = getApplicationContext(env);

    jobject packageManagerObject = env->CallObjectMethod(context, packageManager);
    jstring packNameString = (jstring) env->CallObjectMethod(context, packageName);
    jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, packageInfo,
                                                      packNameString, 64);
    jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures",
                                                "[Landroid/content/pm/Signature;");
    jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject,
                                                                     signaturefieldID);
    jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);
    jstring runningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, nameForUid,
                                                                 uid);

    if (runningPackageName) {// 正在运行应用的包名
        const char *charPackageName = env->GetStringUTFChars(runningPackageName, JNI_FALSE);
        LOGE("runningPackageName %s", charPackageName);
        if (strcmp(charPackageName, APP_PACKAGE_NAME) != 0) {
            return JNI_FALSE;
        }
        env->ReleaseStringUTFChars(runningPackageName, charPackageName);
    } else {
        return JNI_FALSE;
    }

    jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, toCharsString);
    const char *signature = env->GetStringUTFChars(
            (jstring) env->CallObjectMethod(signatureObject, toCharsString), NULL);

    env->DeleteLocalRef(binderClass);
    env->DeleteLocalRef(contextClass);
    env->DeleteLocalRef(signatureClass);
    env->DeleteLocalRef(packageNameClass);
    env->DeleteLocalRef(packageInfoClass);

    LOGE("current apk signature %s", signature);

    // 应用签名,通过 JNIDecryptKey.getSignature(getApplicationContext())
// 获取,注意开发版和发布版的区别,发布版需要使用正式签名打包后获取
    const char *SIGNATURE_KEY = "308202e4308201cc020101300d06092a864886f70d010105050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3230313130333032343932315a180f32303530313032373032343932315a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a02820101009b29ebff08a573108186765c74471d975476bfbf68edf54e090cc36ac392baecc07b5abf4055391839aab18e4ee479f9a10cbb3b33bffa4a8bf880dba9461491b74d6467175a3fdeed80ffa03e18da5f50cf231fe97e62cad7f770f2a3f5e0e5c38dbebfbc66306a601f1ecbdae4d1925b42c0e895194e4ceb9bb241160513ec5b2a1cd9deb74d49a4f8a6ed2650384931a43ff205b9e635676c1c647ea7893c7c6822575cdd0102b4ef99e5de196b35dcd5d8fe1f48aaa447443d68e5296fecca55f53054149c8c2bdddc56e8aa0262967fd66fedcac15cdef26efd4b7ab492aad06e9afd1042bca818e7432f61b7d6e803dc0b3461b8388e6bb1829a1a5c170203010001300d06092a864886f70d0101050500038201010060c75aafaf71933b4b09a28d2076de349625e3cf13ebf0d3354bb2ef38afdf8d07d396555ccd1f991638ec8c3d2201855cfdbab0cd0f895b9d3af43adfccff099b02bc06855563056b859a29fb997ab9cc6e80efff8a8c3fb1d66dfac21967cb07b315e25e1e9625b3b0f553e1c8c116bec526a642c6f5b0a3c8447606ea41d91979d75417abafa0034c1cbe51e21a2e85ba325c00a3d62f96d604d548add464b4d1df09af46f23ee64c0b79de6192a8532956b1066cacf62c9ed9b5f859ba9f85017c4b3d94ebb588a8f2c90b3693e0da9105a9d768e1bb65a2f79fecc7f9fbc0d986a71bad534ea387e9aef85af9b8c35b3ad0c6bed2b6f1c7fbc70f7777c2";
    if (strcmp(signature, SIGNATURE_KEY) == 0) {
        LOGE("verification passed");
        env->ReleaseStringUTFChars(signatureStr, signature);
        auth = JNI_TRUE;
        return JNI_TRUE;
    } else {
        LOGE("verification failed");
        auth = JNI_FALSE;
        return JNI_FALSE;
    }
    return auth;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_secretkeydemo_MainActivity_getKey(JNIEnv *env, jclass clazz) {
    const char *DECRYPT_KEY = "successful return 1232132131321!";
    if (auth) {
        return env->NewStringUTF(DECRYPT_KEY);
    } else {// 你没有权限,验证没有通过。
        return env->NewStringUTF("You don't have permission, the verification didn't pass.");
    }
}

三、效果与demo

演示效果:jni获取签名秘钥,在app初始化时校验正确so库就能正常使用返回正确的秘钥,校验失败则返回失败信息。本文demo

前几天linux服务器被攻击了,害得我重装了一遍系统。这里这个秘钥是debug秘钥,就不要想其他心思了。

image-20220127202810611
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流星雨在线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值