Android使用C/C++来保存密钥(转载)

时间:2022-05-05 03:29:55

转载自:《Android使用C/C++来保存密钥》


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相关的资料,此处仅列出大概步骤。

  1. 下载ndk
  2. 在类中声明native方法。
  public class A {

public native String nativeMethod();

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 在项目根目录下新建一个名为jni的目录,并在其中新建三个文件,分别为:

    • Android.mk (名字固定)
    • Application.mk (名字固定)
    • Project.cpp (名字随意)
  2. Android.mk

    文件的内容如下:

    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)

    LOCAL_MODULE := project
    LOCAL_SRC_FILES := Project.cpp

    include $(BUILD_SHARED_LIBRARY)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    除了LOCAL_MODULELOCAL_SRC_FILES之外,其它都是固定的。前者是这个库的名称,后者是cpp文件的路径。

  3. Application.mk

    文件的内容如下:

    APP_ABI := all
       
       
    • 1
    • 1

    意思是生成所有平台的so库。

  4. Project.cpp


    #include <jni.h>


    #include <stdio.h>


    #include <string.h>



    #ifdef __cplusplus

    extern "C"{

    #endif


    jstring 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
    • 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中的包名全称,并将分隔的.改成_

  5. 以上就把native的代码写好了,在第一步下载好的NDK里面,使用解压后目录下的一个叫ndk-build的程序。cdjni目录下,执行ndk-build,如果执行无误的话,会如下图所示。

    Android使用C/C++来保存密钥(转载)

  6. 执行完上一步之后,会生成一个与jni同级的目录libs,将libs下的文件拷贝到app/src/main/jniLibs目录下。

  7. 在类A中,加入以下静态语句块,引入编译好的native库。

    static {
    System.loadLibrary("project");
    }
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    这里的"project"就是在第4步中的LOCAL_MODULE的值。

  8. 到了这一步,就可以拿到native代码中保存的值了。

有啥问题不

肯定有啊。

试想,如果有人将我们的.so包拿到了(把apk解包就能拿到),然后自己声明native方法,load本地库,然后调用native方法,那么我们做的这么多是不是都白费了?是的,白费了。所以我们需要改进。

如何改进

有什么东西,只有你自己知道,并且有的,但是别人不能模仿的?--应用签名。

那么,我们在native代码里面,先验证一下应用的签名是否是我们的,如果是,才返回正确的密钥。

  1. 获取签名唯一字符串 
    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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. 修改native方法的声明,传入Context对象。
public native String nativeMethod(Context context);
 
 
  • 1
  • 1
  1. 修改C++代码,添加验证逻辑。
#include <jni.h>
#include <stdio.h>
#include <string.h>

#ifdef __cplusplus
extern "C"{
#endif

static 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
getSignString方法也许看起很复杂,如果熟悉java反射的Api的话,其实很类似,就是拿到方法Id,调用方法。
  • 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
  • 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,调用方法。