Android使用C/C++來保存密鑰(轉載)

轉載自:《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,如果執行無誤的話,會如下圖所示。

    ndk-build

  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,調用方法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章