一、秘钥存储方式分析通过native编码,在app初始化时校验app签名特征信息,以动态库so文件的方式实现秘钥的安全保存。
如果需要在本地存储一个密钥串,典型的方式有
- 直接写在java源代码中(明文或者密文区别不大)
- 写在gradle脚本中,使用BuildConfig读取
- 写在gradle.properties中,再到gradle脚本中读取,后面同第二点
- 使用native方法,读取存放在C/C++中的字段
二、NDK秘钥安全保存 2.1、定义日志分析
1为硬编码
2可以做到在不同的BuildType使用不同的密钥
3将配置写到脚本之外,方便管理查看
由于java的破解成本低,前3种从本质上讲明文或者密文区别不大,
因此,将密钥放在难以反编译的C/C++代码中,是一个解决的办法。
Log.h日志打印通用模块
#include2.2、java初始化#include #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)
这里我们通过包名、秘钥来校验包信息的正确,因为秘钥是app最安全的信息。
// 初始化校验 public static native boolean init(); // 获取秘钥 public static native String getKey();
2.3、c++实现这里之所以把秘钥的校验和获取分开,因为在so文件里面,反编译主要通过内存检测分析,对敏感信息使用越少越不容易被破解。
因此这里比较好的做法是把秘钥存放到app初始化的init函数里面,生命周期只有几毫秒,全局维护一个boolean值(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三、效果与demo#include #include "Log.h" const char *APP_PACKAGE_NAME = "com.example.secretkeydemo"; // 验证是否通过 static jboolean auth = JNI_FALSE; 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."); } }
演示效果:jni获取签名秘钥,在app初始化时校验正确so库就能正常使用返回正确的秘钥,校验失败则返回失败信息。本文demo
前几天linux服务器被攻击了,害得我重装了一遍系统。这里这个秘钥是debug秘钥,就不要想其他心思了。



