jni 文件加密

  開發過程當中經常涉及加密,通常直接在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);
}
View Code
/** 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);
}
View Code

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;
    }
}
View Code

此時寫好的LogUtils又在簽名此處能夠複用

 

接下來就是跟加密方法同樣的套用,此處在點擊事件中觸發,而後查看打印結果是否爲一致

 

 

已經Success了,上圖日誌首先是驗證的包名,包名驗證須要獲取application中context,還能防止java層傳入惡意的context對象,若是是惡意的context,獲取時會爲null。不然容易被利用修改

Github:https://github.com/1024477951/KotlinStrong

相關文章
相關標籤/搜索