开发过程当中经常涉及加密,通常直接在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。不然容易被利用修改