Android使用C/C++來保存密鑰
本文主要介紹如何通過native方法調用取出密鑰,以替代原本直接寫在Java中,或寫在gradle腳本中的不安全方式。
爲什麼要這麼做
如果需要在本地存儲一個密鑰串,典型的方式有
1. 直接寫在Java source code中
2. 寫在gradle腳本中,使用BuildConfig讀取
3. 寫在gradle.properties中,再到gradle腳本中讀取,後面同第二點
4. 使用native方法,讀取存放在C/C++中的字段
本質上來講方式1,2,3**沒有什麼區別**。1爲硬編碼,2可以做到在不同的BuildType使用不同的密鑰,3將配置寫到腳本之外,方便管理查看。
然而,在項目編譯之後,方式1,2,3都會把密鑰直接替換到字節碼文件中,對於反編譯如此方便的Android來說,無疑是將密鑰拱手讓人。
因此,將密鑰放在難以反編譯的C/C++代碼中,是一個解決的辦法。
怎麼做
java怎麼調用C/C++方法
如果想詳細的明白以下步驟,請查閱JNI相關的資料,此處僅列出大概步驟。
- 下載ndk
- 在類中聲明native方法。
public class A {
public native String nativeMethod();
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
-
在項目根目錄下新建一個名爲jni的目錄,並在其中新建三個文件,分別爲:
- Android.mk (名字固定)
- Application.mk (名字固定)
- Project.cpp (名字隨意)
-
Android.mk
文件的內容如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := project LOCAL_SRC_FILES := Project.cpp include $(BUILD_SHARED_LIBRARY)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
除了
LOCAL_MODULE
和LOCAL_SRC_FILES
之外,其它都是固定的。前者是這個庫的名稱,後者是cpp文件的路徑。 -
Application.mk
文件的內容如下:
APP_ABI := all
- 1
- 1
意思是生成所有平臺的so庫。
-
Project.cpp
#include <jni.h> #include <stdio.h> #include <string.h> #ifdef __cplusplus extern "C"{ #endif jstring Java_[ClassAPackage]_A_nativeMethod(JNIEnv *env,jobject thiz) { // 返回密鑰 return (env)->NewStringUTF("你的密鑰"); } #ifdef __cplusplus } #endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
ClassAPackage
爲類A
在java中的包名全稱,並將分隔的.
改成_
-
以上就把native的代碼寫好了,在第一步下載好的NDK裏面,使用解壓後目錄下的一個叫
ndk-build
的程序。cd
到jni
目錄下,執行ndk-build,如果執行無誤的話,會如下圖所示。 -
執行完上一步之後,會生成一個與
jni
同級的目錄libs
,將libs
下的文件拷貝到app/src/main/jniLibs
目錄下。 -
在類
A
中,加入以下靜態語句塊,引入編譯好的native庫。static { System.loadLibrary("project"); }
- 1
- 2
- 3
- 1
- 2
- 3
這裏的
"project"
就是在第4步中的LOCAL_MODULE
的值。 -
到了這一步,就可以拿到native代碼中保存的值了。
有啥問題不
肯定有啊。
試想,如果有人將我們的.so包拿到了(把apk解包就能拿到),然後自己聲明native方法,load本地庫,然後調用native方法,那麼我們做的這麼多是不是都白費了?是的,白費了。所以我們需要改進。
如何改進
有什麼東西,只有你自己知道,並且有的,但是別人不能模仿的?--應用簽名。
那麼,我們在native代碼裏面,先驗證一下應用的簽名是否是我們的,如果是,才返回正確的密鑰。
- 獲取簽名唯一字符串
將BuildVariants
切換到release
,也就是使用生產版本的簽名文件,然後將下面的代碼粘貼至任意一個Activity
內,在控制檯裏,可以獲取這個字符串。
public void getSignInfo() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
Signature sign = signs[0];
System.out.println(sign.toCharsString());
} catch (Exception e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 修改native方法的聲明,傳入
Context
對象。
public native String nativeMethod(Context context);
- 1
- 1
- 修改C++代碼,添加驗證邏輯。
#include <jni.h>
#include <stdio.h>
#include <string.h>
#ifdef __cplusplus
extern "C"{
#endif
static jclass contextClass;
static jclass signatureClass;
static jclass packageNameClass;
static jclass packageInfoClass;
/**
之前生成好的簽名字符串
*/
const char* RELEASE_SIGN = "第1步,生成好的字符串";
/*
根據context對象,獲取簽名字符串
*/
const char* getSignString(JNIEnv *env,jobject contextObject) {
jmethodID getPackageManagerId = (env)->GetMethodID(contextClass, "getPackageManager","()Landroid/content/pm/PackageManager;");
jmethodID getPackageNameId = (env)->GetMethodID(contextClass, "getPackageName","()Ljava/lang/String;");
jmethodID signToStringId = (env)->GetMethodID(signatureClass, "toCharsString","()Ljava/lang/String;");
jmethodID getPackageInfoId = (env)->GetMethodID(packageNameClass, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jobject packageManagerObject = (env)->CallObjectMethod(contextObject, getPackageManagerId);
jstring packNameString = (jstring)(env)->CallObjectMethod(contextObject, getPackageNameId);
jobject packageInfoObject = (env)->CallObjectMethod(packageManagerObject, getPackageInfoId,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);
return (env)->GetStringUTFChars((jstring)(env)->CallObjectMethod(signatureObject, signToStringId),0);
}
jstring Java_[ClassAPackage]_A_nativeMethod(JNIEnv *env,jobject thiz,jobject contextObject) {
const char* signStrng = getSignString(env,contextObject);
if(strcmp(signStrng,RELEASE_SIGN)==0)//簽名一致 返回合法的 api key,否則返回錯誤
{
return (env)->NewStringUTF("你的密鑰");
}else
{
return (env)->NewStringUTF("error");
}
}
/**
利用OnLoad鉤子,初始化需要用到的Class類.
*/
JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM* vm,void* reserved){
JNIEnv* env = NULL;
jint result=-1;
if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
return result;
contextClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/Context"));
signatureClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/Signature"));
packageNameClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/PackageManager"));
packageInfoClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/PackageInfo"));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
getSignString
方法也許看起很複雜,如果熟悉java反射的Api的話,其實很類似,就是拿到方法Id,調用方法。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
getSignString
方法也許看起很複雜,如果熟悉java反射的Api的話,其實很類似,就是拿到方法Id,調用方法。