NDK開發基礎②文件加密解密與分割合并

接續上篇NDK開發基礎①使用Android Studio編寫NDK

前情提要

隨著Android Studio的越來越完善 , 我們編寫NDK就會越來越方便,使用Android Studio 2.2 RC2 , 編寫NDK的時候 , 不需要使用javah命令來生成頭文件 , 創建一個native方法 , 使用alt + enter會提示要你創建一個JNI函數 , C/C++語法提示也相對比較完善了, 減少了一些重復代碼的編寫,相信使用Android Studio 2.2編寫NDK, 不會讓你失望 。

Part 1 , -- 文件的加密解密

C語言基礎系列第八章文件IO,介紹了文件的加密解密 , 開發工具使用的是VS , 在上篇NDK開發基礎①使用Android Studio編寫NDK簡單介紹了Android Studio來搭建NDK開發環境,今天我們使用Android Studio來玩一把 。

界面 與Project目錄結構

UI

就幾個按鈕 , 布局文件就不貼代碼了 , 讀者也可以整個輸入框,可以輸入加密的密碼 。

project directory

從目錄上可以看出ndk包下面的 , 就是一個native方法的類, cpp目錄下的就是JNI C文件 , CMakeLists.txt配置我們的編譯流程,以及生成庫與添加庫的配置。

Java native code

    /**
     * 帶密碼的文件加密
     * @param normalFilePath 要加密的文件路徑
     * @param encryptFilePath 加密之后的文件路徑
     * @param password 加密密碼
     */
    public static native void fileEncrypt(String normalFilePath,String encryptFilePath,String password) ;

    /**
     * 帶密碼的文件解密
     * @param encryptFilePath 要解密的文件路徑
     * @param encryptFilePath 解密之后的文件路徑
     * @param password 加密密碼
     */
    public static native void fileDecrypt(String encryptFilePath,String decryptFilePath,String password) ;

簡要提示:文件的加密解密 , 實質上在復制文件的時候進行^運算,在計算機中 , 所有的文件都是以二進制存儲的 , 所有可以進行運算來進行文件的加密解密 , 當然 , 密碼的算法是可逆的 , 也就是可解密的 。

use java native code

    // SD_CARD 根路徑
    private static final String SD_CARD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() ;

    /*文件加密*/
    private void fileEncrypt() {
        String normalFilePath = SD_CARD_PATH+ File.separatorChar+"neck.jpg" ;
        String encryptFilePath = SD_CARD_PATH+File.separatorChar+"neck_encrypt.jpg" ;
        Log.e(TAG, "fileEncrypt: normalFilePath = "+ normalFilePath + " encryptFilePath = "+encryptFilePath);

        try{
            HelloNDK.fileEncrypt(normalFilePath,encryptFilePath,"xj");
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

    /*文件解密*/
    private void fileDecrypt() {

        String encryptFilePath = SD_CARD_PATH+File.separatorChar+"neck_encrypt.jpg" ;
        String decryptFilePath = SD_CARD_PATH+File.separatorChar+"neck_decrypt.jpg" ;

        try{
            HelloNDK.fileDecrypt(encryptFilePath,decryptFilePath,"xj");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

文件加密解密實現

/*加密文件*/
JNIEXPORT void JNICALL
Java_com_zeno_encryptanddecrypt_ndk_HelloNDK_fileEncrypt(JNIEnv *env, jclass type,
                                                         jstring normalFilePath_,
                                                         jstring encryptFilePath_,
                                                         jstring password_) {
    const char *normalFilePath = (*env)->GetStringUTFChars(env, normalFilePath_, 0);
    const char *encryptFilePath = (*env)->GetStringUTFChars(env, encryptFilePath_, 0);
    const char *password = (*env)->GetStringUTFChars(env, password_, 0);


    int passwordLen = strlen(password);

    LOGE("MainActivity","path1 == %s , path2 == %s",normalFilePath , encryptFilePath) ;

     // 讀文件指針
    FILE* frp = fopen(normalFilePath,"rb") ;
    // 寫文件指針
    FILE* fwp = fopen(encryptFilePath,"wb") ;

    if(frp == NULL) {
        LOGE("MainActivity","%s","文件不存在") ;
        return;
    }
    if(fwp == NULL) {
        LOGE("MainActivity","%s","沒有寫權限") ;
    }

    // 邊讀邊寫邊加密
    int buffer ;
    int index = 0 ;
    while((buffer = fgetc(frp)) != EOF) {

        // write
        fputc(buffer ^ *(password+(index % passwordLen)),fwp) ;
        index++ ;
    }


    // 關閉文件流
    fclose(fwp);
    fclose(frp);

    LOGE("MainActivity","%s","文件加密成功") ;

    (*env)->ReleaseStringUTFChars(env, normalFilePath_, normalFilePath);
    (*env)->ReleaseStringUTFChars(env, encryptFilePath_, encryptFilePath);
    (*env)->ReleaseStringUTFChars(env, password_, password);
}

簡要提示:加密解密算法,在C語言:文件IO里面就介紹了 , 這里就不加贅述了,加密與解密共用的是一套算法, 解密的就不貼代碼了 。在使用*password指針的時候需要注意,password+1可以得到下一個字符,但是password+0的時候不會得到第一個字符,會輸出整個字符指針,所有在使用字符指針的時候一個一個取字符 , 需要使用*(password+0) ,*(password+1)取第一個第二字符 。如果您對C語言以及JNI開發都不了解的話 , 可以關注的我的專題Android NDK開發之旅

Part 2 , 文件分割與合并

使用場景:大文件上傳 , 如視頻文件 , 進行文件拆分 , 分批上傳 。

Java native code

    /**
     * 文件分割
     * @param splitFilePath 要分割文件的路徑
     * @param suffix 分割文件拓展名
     * @param fileNum 分割文件的數量
     */
    public static native void fileSplit(String splitFilePath,String suffix,int fileNum);

    /**
     * 文件合并
     * @param splitFilePath 分割文件的路徑
     * @param splitSuffix 分割文件拓展名
     * @param mergeSuffix 合并文件的拓展名
     * @param fileNum 分割文件的數量
     */
    public static native void fileMerge(String splitFilePath,String splitSuffix,String mergeSuffix,int fileNum);

use java native code

    /*分割文件*/
    private void fileSplit() {
        String splitFilePath = SD_CARD_PATH+File.separatorChar+"neck.jpg" ;
        String suffix = ".b";
        try{
            HelloNDK.fileSplit(splitFilePath,suffix,3);
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

    /*文件合并*/
    private void fileMerge() {
        String splitFilePath = SD_CARD_PATH+File.separatorChar+"neck.jpg" ;
        String splitSuffix = ".b";
        String mergeSuffix = ".jpeg";
        try{
            HelloNDK.fileMerge(splitFilePath,splitSuffix,mergeSuffix,3);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

文件分割實現

算法分析圖:

file split

偽代碼:

// 平均分配文件大小
if(fileSize % fileNum) {
    int i= 0 ; 
    for(; i < fileNum ; i++) {
        // 創建分割文件
        int j= 0 ;
        for(;j < partFileSize ; j++) {
            // 讀寫數據
        }
        
    }
}
// 不可平均分配大小
else {
    // 前面的文件平分文件大小 , 將剩下的大小全部給最后一個文件
    if(fileSize % (fileNum-1)){
        int i= 0 ; 
        for(; i < (fileNum-1) ; i++) {
            // 創建分割文件
            int j= 0 ;
            for(;j < partFileSize ; j++) {
                // 讀寫數據
            }
    }
    
    }else {
        // 將剩余大小fileSize % (fileNum-1)寫入到最后一個文件中 
        int j = 0 ;
        for(;j < fileSize % (fileNum-1)){
            // 讀寫數據
        }
    
    }
}

文件分割算法就分析到這里 , 下面我們來看看具體的實現:

java native code

    /*分割文件*/
    private void fileSplit() {
        String splitFilePath = SD_CARD_PATH+File.separatorChar+"neck.jpg" ;
        String suffix = ".b";
        try{
            HelloNDK.fileSplit(splitFilePath,suffix,3);
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

    /*文件合并*/
    private void fileMerge() {
        String splitFilePath = SD_CARD_PATH+File.separatorChar+"neck.jpg" ;
        String splitSuffix = ".b";
        String mergeSuffix = ".jpeg";
        try{
            HelloNDK.fileMerge(splitFilePath,splitSuffix,mergeSuffix,3);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

具體實現:

/*文件分割*/
JNIEXPORT void JNICALL
Java_com_zeno_encryptanddecrypt_ndk_HelloNDK_fileSplit(JNIEnv *env, jclass type,
                                                       jstring splitFilePath_,
                                                       jstring suffix_,
                                                       jint fileNum) {
    const char *splitFilePath = (*env)->GetStringUTFChars(env, splitFilePath_, 0);
    const char *suffix = (*env)->GetStringUTFChars(env, suffix_, 0);

    // 要分割文件 , 首先要得到分割文件的路徑列表 ,申請動態內存存儲路徑列表
    char** split_path_list = (char**)malloc(sizeof(char*) * fileNum);

    // 得到文件大小
    long file_size = getFileSize(splitFilePath);

    // 得到路徑字符長度
    int file_path_str_len = strlen(splitFilePath);

    // 組合路徑
    char file_path[file_path_str_len + 5] ;
    strcpy(file_path,splitFilePath);
    strtok(file_path,".");
    strcat(file_path,"_%d");
    strcat(file_path,suffix);

    int i=0 ;
    for (; i < fileNum; ++i) {

        // 申請單個文件的路徑動態內存存儲
        split_path_list[i] = (char*)malloc(sizeof(char) * 128);
        // 組合分割的單個文件路徑
        sprintf(split_path_list[i],file_path,(i+1)) ;

        LOGE("MainActivity","%s",split_path_list[i]);
    }



    LOGE("MainActivity","文件大小 == %ld",file_size);
    LOGE("MainActivity","文件路徑 == %s",splitFilePath);
    // 讀文件
    FILE* fp = fopen(splitFilePath,"rb");
    if(fp == NULL) {
        LOGE("MainActivity","%s","文件不存在,或文件不可讀");
        return;
    }

    // 整除 , 說明各個文件劃分大小一致
    if (file_size % fileNum) {
        // 單個文件大小
        int part_file_size = file_size/fileNum ;
        LOGE("MainActivity","單個文件大小 == %d",part_file_size);
        int i = 0 ;
        // 分割多少個文件就分段讀多少次
        for (; i < fileNum; i++) {
            // 寫文件
            FILE* fwp = fopen(split_path_list[i],"wb");
            if(fwp == NULL) {
                LOGE("MainActivity","%s","沒有文件寫入權限");
                return;
            }
            int j = 0 ;
            // 單個文件有多大 , 就讀寫多少次
            for (; j < part_file_size; j++) {
                // 邊讀邊寫
                fputc(fgetc(fp),fwp) ;
            }

            // 關閉文件流
            fclose(fwp);
        }
    }
    /*文件大小不整除*/
    else{
        // 不整除
        int part_file_size = file_size / (fileNum -1 ) ;
        LOGE("MainActivity","單個文件大小 == %d",part_file_size);
        int i = 0 ;
        for (; i < (fileNum - 1); i++) {
            // 寫文件
            FILE* fwp = fopen(split_path_list[i],"wb");

            if(fwp == NULL) {
                LOGE("MainActivity","%s","沒有文件寫入權限") ;
                return;
            }

            int j = 0 ;
            for (; j < part_file_size; j++) {
                // 邊讀邊寫
                fputc(fgetc(fp),fwp);
            }

            // 關閉流
            fclose(fwp);
        }

        // 剩余部分
        FILE* last_fwp = fopen(split_path_list[fileNum - 1],"wb") ;
        i= 0 ;
        for (; i < file_size % (fileNum -1); i++) {
                fputc(fgetc(fp),last_fwp) ;
        }

        // 關閉流
        fclose(last_fwp);

    }


    // 關閉文件流
    fclose(fp);

    // 釋放動態內存
    i= 0 ;
    for (; i < fileNum ; i++) {
        free(split_path_list[i]) ;
    }
    
    free(split_path_list);

    (*env)->ReleaseStringUTFChars(env, splitFilePath_, splitFilePath);
    (*env)->ReleaseStringUTFChars(env, suffix_, suffix);
}

簡要提示:組合路徑的時候,使用了strtok來進行文件路徑的分割,將.jpg去掉,用strcat拼接了_%d,最后拼接了傳入的文件拓展名,這樣設計是為了讓分割的文件 , 不那么見名知意 。
LOGE定義的一個預處理函數,在C語言基礎系列C語言基礎及指針⑩預編譯及jni.h分析有著詳細說明。

#define LOGE(TAG,FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,TAG,FORMAT,__VA_ARGS__)

文件合并

文件的分割 , 是件一個文件分割成多個子文件 。文件的合并 , 則是將多個子文件合并成一個文件 。

算法分析圖:

merge file

具體實現



/*合并文件*/
JNIEXPORT void JNICALL
Java_com_zeno_encryptanddecrypt_ndk_HelloNDK_fileMerge(JNIEnv *env, jclass type,
                                                       jstring splitFilePath_,
                                                       jstring splitSuffix_,
                                                       jstring mergeSuffix_, jint fileNum) {
    const char *splitFilePath = (*env)->GetStringUTFChars(env, splitFilePath_, 0);
    const char *splitSuffix = (*env)->GetStringUTFChars(env, splitSuffix_, 0);
    const char *mergeSuffix = (*env)->GetStringUTFChars(env, mergeSuffix_, 0);

    // 1. 申請split文件路徑列表動態內存
    char** split_path_list = (char**)malloc(sizeof(char*) * fileNum) ;


    // 2. 組裝split文件路徑
    int split_file_path_len = strlen(splitFilePath) ;
    int split_file_path_suffix_len = strlen(splitSuffix);
    char split_file_path[split_file_path_len + split_file_path_suffix_len] ;
    strcpy(split_file_path,splitFilePath);
    strtok(split_file_path,".");
    strcat(split_file_path,"_%d");
    strcat(split_file_path,splitSuffix);

    // 3. 組裝merge文件路徑
    int merge_file_path_len = strlen(mergeSuffix);
    char merge_file_path[split_file_path_len + merge_file_path_len] ;
    strcpy(merge_file_path,splitFilePath);
    strtok(merge_file_path,".");
    strcat(merge_file_path,mergeSuffix);

    LOGE("MainActivity","merge 文件路徑 = %s",merge_file_path) ;

    // 4. 循環得到split文件路徑列表
    int file_path_str_len = strlen(split_file_path);
    int i= 0;
    for (; i < fileNum; i++) {
        split_path_list[i] = (char*)malloc(sizeof(char) * file_path_str_len) ;

        sprintf(split_path_list[i],split_file_path,(i+1)) ;

        LOGE("MainActivity","split文件路徑列表 = %s",split_path_list[i]) ;
    }

    // 5. 創建并打開 merge file
    FILE* merge_fwp = fopen(merge_file_path,"wb") ;

    // 6. 邊讀邊寫 , 讀多個文件,寫入一個文件
    i = 0 ;
    for (; i < fileNum ; i++) {

        FILE* split_frp = fopen(split_path_list[i],"rb") ;
        if(split_frp == NULL) {
            LOGE("MainActivity","%s","文件不存在,或沒有讀文件權限");
            return;
        }
        long part_split_file_size = getFileSize(split_path_list[i]);
        int j = 0 ;
        for (; j < part_split_file_size; j++) {
            fputc(fgetc(split_frp),merge_fwp);
        }

        // 關閉流
        fclose(split_frp) ;

        // 每合并一個文件 ,就刪除它
        remove(split_path_list[i]) ;
    }

    // 關閉文件流
    fclose(merge_fwp);

    // 釋放動態內存
    i = 0 ;
    for (; i < fileNum; i++) {
        free(split_path_list[i]) ;
    }

    free(split_path_list);

    LOGE("MainActivity","%s","文件合并完成") ;

    (*env)->ReleaseStringUTFChars(env, splitFilePath_, splitFilePath);
    (*env)->ReleaseStringUTFChars(env, splitSuffix_, splitSuffix);
    (*env)->ReleaseStringUTFChars(env, mergeSuffix_, mergeSuffix);
}

簡要提示:在編寫文件分割與合并的時候 , 文件的路徑千萬不要弄混了,筆者有有時候也會因為變量的提示沒細看 , 而將分割的子文件路徑與目標文件路徑弄混 ,在命名的時候 , 盡量規范 , 見名知意 。在發生錯誤的時候 , 一步一步的調試 , 首先猜測哪里發生異常的可能性比較大 , 再進行log輸出查看 。

結語

文件的加密解密 , 可以引申為對數據的加密解密 , 可以使用C/C++進行加密解密, 這樣對反編譯APK增加了難度 , 也多了一層防范 , 因為java實在是太容易反編譯了, 即使不反編譯成java代碼, 通過smail文件也可以看個大概 , 還是很危險的 。

文件的分割與合并 , 對文件數據傳輸有很大益處 。

如果想了解更多關于Android NDK的知識 , 歡迎關注我的專題Android NDK 開發之旅 , 從零開始 , 帶你一步一步進入NDK開發的世界 。

參考

Standard C 語言標準函數庫速查

項目地址 : https://github.com/zhuyongit/EncryptAndDecrypt

贊助
本文及本系列,由老司機學院動腦學院特約贊助。

做一家受人尊敬的企業,做一位令人尊敬的老師

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容