開發過程當中經常涉及加密,通常直接在java層對參數進行加密,當app被反編譯時,對方能夠拿到咱們的代碼,能夠看到咱們加密的方式從而讓對方找到破解密文的方法,很不安全;java
那麼是否能夠防止這種反編譯的破解呢,因此便有了在c層處理加密的方法,經過jni將加密方法打包到so庫中,能夠防止對方反編譯看到咱們的加密條件,可是這樣也不安全,對方只須要反編譯apk後獲得 應用的包名 你的so庫 你的native方法,就能夠建立包名相同方法名相同的一個應用,把so放進去,而後就能夠繞過密鑰檢查去調用你的接口,因此咱們還須要在so庫中加入簽名驗證,當調用加密方法對操做參數的時候,驗證此時應用簽名是不是咱們本應用的,若是不是,則表示當前應用是僞應用,簽名和包名必須得要一致,就算遇到逆向工程師,要破解咱們的app也是有必定難度了android
做爲一個初學jni的猿類,註釋通常比較多,也沒使用第三方c庫,當本身練手git
androidstudio編寫c仍是挺方便的,jni使用 CMakeLists 構建,CMake是一種跨平臺編譯工具,比make更爲高級,使用起來要方便得多。CMake主要是編寫CMakeLists.txt文件,而後用cmake命令將CMakeLists.txt文件轉化爲make所須要的makefile文件,最後用make命令編譯源碼生成可執行程序或共享庫github
新建一個C++project,你會發現自動給你配置好了,能夠直接運行,裏面有個默認的方法,咱們能夠依照此爲基礎,省去一些小麻煩數組
此處項目爲Kotlin,不是Java,流程大體類似安全
能夠看到在project中有個cpp文件夾,裏面就是編寫c層代碼的,在MainActivity裏面加載了這個庫文件,調用了c庫中的方法,這只是官方自動生成的一個例子,這裏就能夠直接去建立文件編寫加密方法了app
1.新建一個Encrypt.cpp文件,編寫加密解密方法ide
2.在 CMakeLists.txt 文件中加入cpp文件工具
3.在應用層定義好加密解密方法,調用方法測試
首先要在 CMakeLists.txt 里加入cpp構建好才能開始編寫,否則會編譯報錯,識別不了,因此通常先建立文件,而後同步在CMakeLists.txt里加入咱們新建的cpp,運行的適合會生成so
爲了方便查看,經過查看日誌缺認程序狀態,要打印日誌,還須要配置設置log庫到咱們的動態庫中,否則會拋異常
若是不想直接加載在MainActivity裏面也能夠新建一個Utils類去實現,在Utils裏定義好咱們的加密解密方法,以前看過一些資料,我也跟他們同樣,用三個測試方法去測試,建立一個原始文件,而後經過運算加密,生成一個加密文件,而後在解密,生成一個解密文件,三個文件對比差別,因此須要三個方法,createFile,encryption,decryption
對應的,咱們須要在cpp文件裏也定義三個名稱同樣的方法,注意包名別錯了
我這裏看着麻煩,直接寫了一個LogUtils的頭文件,避免重複代碼
這樣後面須要引用就好了,雖然也就一行代碼,不過有強迫症,不喜歡重複去寫這樣的代碼
定義好了方法能夠開始具體實現,首先須要先建立文件,建立文件須要傳入地址,此處傳入根目錄,調用 fputs 方法寫入測試文字
/** createFile */ extern "C" JNIEXPORT void JNICALL Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_createFile(JNIEnv *env, jobject type, jstring path_) { Logger("createFile path = %s", path_); //獲得一個UTF-8編碼的字符串(java使用 UTF-16 編碼的,中文英文都是2字節,jni內部使用UTF-8編碼,ascii字符是1字節,中文是3字節) const char *normalPath = env->GetStringUTFChars(path_, nullptr); if (normalPath == NULL) { return; } //wb:打開或新建一個二進制文件;只容許寫數據 FILE *fp = fopen(normalPath, "wb"); //把字符串寫入到指定的流 stream 中,但不包括空字符。 fputs("帳號:123\n密碼:123;\n帳號:456\n密碼:456;\n", fp); //關閉流 fp。刷新全部的緩衝區 fclose(fp); //釋放JVM保存的字符串的內存 env->ReleaseStringUTFChars(path_, normalPath);//ReleaseStringUTFChars : 表示此內存不在使用,通知JVM回收,用了GetXXX就必須調用ReleaseXXX }
由於是初學,C 已經忘的差很少了,因此註釋也比較詳細,看着註釋大體的意思也就懂了
下面是加密解密方法
/** encryption */ extern "C" JNIEXPORT void JNICALL Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_encryption(JNIEnv *env, jclass type, jstring normalPath_, jstring encryptPath_) { //獲取字符串保存在JVM中內存中 const char *normalPath = env->GetStringUTFChars(normalPath_, nullptr); const char *encryptPath = env->GetStringUTFChars(encryptPath_, nullptr); Logger("normalPath = %s, encryptPath = %s", normalPath, encryptPath); //rb:只讀打開一個二進制文件,容許讀數據。 //wb:只寫打開或新建一個二進制文件;只容許寫數據 FILE *normal_fp = fopen(normalPath, "rb"); FILE *encrypt_fp = fopen(encryptPath, "wb"); if (normal_fp == nullptr) { Logger("%s", "文件打開失敗"); return; } if(encrypt_fp == NULL) { Logger("%s","沒有寫權限") ; } //一次讀取一個字符 int ch = 0; int i = 0; size_t pwd_length = strlen(password);//計數器 while ((ch = fgetc(normal_fp)) != EOF) {//讀取文件中的字符 //寫入(異或運算) /** ^(相同爲0,不一樣爲1) int a=3=011 int b=6=110 result : a^b=101=5 */ fputc(ch ^ password[i % pwd_length], encrypt_fp); i++; } //關閉流 normal_fp和encrypt_fp。刷新全部的緩衝區 fclose(normal_fp); fclose(encrypt_fp); //釋放JVM保存的字符串的內存 env->ReleaseStringUTFChars(normalPath_, normalPath); env->ReleaseStringUTFChars(encryptPath_, encryptPath); }
/** decryption */ extern "C" JNIEXPORT void JNICALL Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_decryption(JNIEnv *env, jclass type, jstring encryptPath_, jstring decryptPath_) { //獲取字符串保存在JVM中內存中 const char *encryptPath = env->GetStringUTFChars(encryptPath_, nullptr); const char *decryptPath = env->GetStringUTFChars(decryptPath_, nullptr); Logger("encryptPath = %s, decryptPath = %s", encryptPath, decryptPath); //rb:只讀打開一個二進制文件,容許讀數據。 //wb:只寫打開或新建一個二進制文件;只容許寫數據 FILE *encrypt_fp = fopen(encryptPath, "rb"); FILE *decrypt_fp = fopen(decryptPath, "wb"); if (encrypt_fp == nullptr) { Logger("%s", "加密文件打開失敗"); return; } int ch; int i = 0; size_t pwd_length = strlen(password); while ((ch = fgetc(encrypt_fp)) != EOF) { fputc(ch ^ password[i % pwd_length], decrypt_fp); i++; } //關閉流 encrypt_fp 和 decrypt_fp。刷新全部的緩衝區 fclose(encrypt_fp); fclose(decrypt_fp); //釋放JVM保存的字符串的內存 env->ReleaseStringUTFChars(encryptPath_, encryptPath); env->ReleaseStringUTFChars(decryptPath_, decryptPath); }
C層代碼編寫完畢後,直接在應用層調用測試
首先在Utils裏面編寫一個測試方法
寫入文件別忘記了權限申請
測試方法能夠看到,連續調用了三個方法,首先先建立文件,建立好測試文件調用加密解密方法生成結果文件,或者查看日誌
文件生成,裏面的內容即是默認寫入的測試數據,以及加密解密的結果,下面分別是打開後默認的文件,加密後的文件以及解密後的文件
此時加密完成,可是還缺乏上面說的包名簽名驗證,否則仍是很容易就能破解,先定義兩個變量,一個包名一個簽名,用做判斷,簽名方法須要用到一些獲取安卓系統的Context等方法,此處能夠分離出一個系統Utils,方面複用
而後根據加密的方法,新建一個簽名驗證的cpp文件,而後構建好,跟應用層對應
值得一說的就是經過C代碼,獲取到Java層的代碼調用方法,而且經過應用層的方法獲取簽名,對比包名和簽名,是否一致,以此加固安全
jstring getSignature(JNIEnv *env, jobject obj) { jclass native_class = env->GetObjectClass(obj); jmethodID pm_id = env->GetMethodID(native_class, "getPackageManager", "()Landroid/content/pm/PackageManager;"); jobject pm_obj = env->CallObjectMethod(obj, pm_id); jclass pm_clazz = env->GetObjectClass(pm_obj); // 獲得 getPackageInfo 方法的 ID jmethodID package_info_id = env->GetMethodID(pm_clazz, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); jstring pkg_str = getPackname(env, obj); Logger("getPackname: %d", pkg_str); // 得到應用包的信息 jobject pi_obj = env->CallObjectMethod(pm_obj, package_info_id, pkg_str, 64); // 得到 PackageInfo 類 jclass pi_clazz = env->GetObjectClass(pi_obj); // 得到簽名數組屬性的 ID jfieldID signatures_fieldId = env->GetFieldID(pi_clazz, "signatures", "[Landroid/content/pm/Signature;"); jobject signatures_obj = env->GetObjectField(pi_obj, signatures_fieldId); jobjectArray signaturesArray = (jobjectArray)signatures_obj; // jsize size = env->GetArrayLength(signaturesArray); jobject signature_obj = env->GetObjectArrayElement(signaturesArray, 0); jclass signature_clazz = env->GetObjectClass(signature_obj); jmethodID string_id = env->GetMethodID(signature_clazz, "toCharsString", "()Ljava/lang/String;"); jstring str = static_cast<jstring>(env->CallObjectMethod(signature_obj, string_id)); // char *c_msg = (char*)env->GetStringUTFChars(str,0); // Logger("signsture: %s", c_msg); return str; } /** 驗證程序包和簽名 */ jboolean checkSignature(JNIEnv *env, jobject context){ //根據傳入的context對象getPackageName jstring pkg_str = getPackname(env, context); const char *pkg = env->GetStringUTFChars(pkg_str, NULL); //對比 if (strcmp(package_name, pkg) != 0) { Logger("程序包驗證失敗:%s",pkg); return false; } Logger("程序包驗證成功:%s",pkg); //調用String的toCharsString jstring signature_string = getSignature(env,context); //轉換爲char* const char *signature_char = env->GetStringUTFChars(signature_string, NULL); Logger("app signature:%s\n", signature_char); Logger("cpp signature:%s\n", app_signature); //對比簽名 if (strcmp(signature_char, app_signature) == 0) { Logger("程序簽名驗證經過"); return true; } else { Logger("程序簽名驗證失敗"); return false; } }
此時寫好的LogUtils又在簽名此處能夠複用
接下來就是跟加密方法同樣的套用,此處在點擊事件中觸發,而後查看打印結果是否爲一致
已經Success了,上圖日誌首先是驗證的包名,包名驗證須要獲取application中context,還能防止java層傳入惡意的context對象,若是是惡意的context,獲取時會爲null。不然容易被利用修改