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
在项目根目录下新建一个名为jni的目录,并在其中新建三个文件,分别为:
- Android.mk (名字固定)
- Application.mk (名字固定)
- Project.cpp (名字随意)
Android.mk
文件的内容如下:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := project LOCAL_SRC_FILES := Project.cppinclude $(BUILD_SHARED_LIBRARY)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
除了
LOCAL_MODULE
和LOCAL_SRC_FILES
之外,其它都是固定的。前者是这个库的名称,后者是cpp文件的路径。Application.mk
文件的内容如下:
APP_ABI := all
- 1
意思是生成所有平台的so库。
Project.cpp
#include <jni.h>#include <stdio.h>#include <string.h>#ifdef __cplusplusextern "C"{#endifjstring 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
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
这里的
"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
- 修改native方法的声明,传入
Context
对象。
public native String nativeMethod(Context context);
- 1
- 修改C++代码,添加验证逻辑。
#include <jni.h>
#include <stdio.h>
#include <string.h>#ifdef __cplusplus
extern "C"{
#endifstatic 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
- 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,调用方法。
**以上就是本文的讨论内容,有些技术细节没有深入介绍,请自行查阅相关资料。
如果有不同的方式,欢迎讨论**