通过native编码,在app初始化时校验app签名特征信息,以动态库so文件的方式实现秘钥的安全保存。
一、秘钥存储方式分析
如果需要在本地存储一个密钥串,典型的方式有
- 直接写在java源代码中(明文或者密文区别不大)
- 写在gradle脚本中,使用BuildConfig读取
- 写在gradle.properties中,再到gradle脚本中读取,后面同第二点
- 使用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秘钥,就不要想其他心思了。