前两篇,我们谈到了使用SQLCipher和Conceal对本地数据进行加密。由于都两种方法都采用了对称加密,因此我们需要自己管理加密的秘钥。这时你会发现,虽然对我们的数据进行了加密,但是我们却引入了新的问题。我们的加密方法很容易通过反编译apk获取到,那么,我们就需要安全的维护这个秘钥了。但遗憾的是,本地数据存储方式我们都已经讲述,并没有一种一劳永逸的安全保存方法,那么,我们的秘钥存在哪里合适呢?这个时候你可能会想到本地不行那我们存到服务器上吧,通过https进行传输。这样当然可以,通过一定的算法为每个人配置一个秘钥,需要的时候请求网络获取,然后对本地数据进行解密。但是这样也存在一个问题:本地保存的数据如果不联网就无法打开。那么,还有更好的方案吗?今天给大家介绍JNI。
NDK,JNI对于刚接触android开发的攻城狮来说是有较大的门槛的,但是为了实现我们更安全的保存数据,他可能是不错的选择。反编译过别人家app的你可能都遇到过这样的情况:一个个没无法查看代码逻辑的so文件。这些文件我们可以使用java代码调用,实现一些我们不知道内部逻辑但是会给我们一个结果功能。那么,我们把密码放到这些so中就可以更进一步提高我们本地数据的安全级别。
下面通过一个加单的demo来看看JNI的实现。
首先创建一个工具类:
public class CipherUtil {
static {
System.loadLibrary("Cipher");
}
public static native String getCipherKey();
}
在这个类文件上点击右键,使用我们之前配置的javah工具生成头文件。之后我们会在和java目录同级的jni文件下看到一个.h文件,如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_ttdevs_ndk_CipherUtil */
#ifndef _Included_com_ttdevs_ndk_CipherUtil
#define _Included_com_ttdevs_ndk_CipherUtil
#ifdef __cplusplus
extern "C" {
#endif
/* * Class: com_ttdevs_ndk_CipherUtil * Method: getCipherKey * Signature: ()Ljava/lang/String; */
JNIEXPORT jstring JNICALL Java_com_ttdevs_ndk_CipherUtil_getCipherKey
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
然后在h文件的同级新建一个C++文件(右键>New>C/C++ Source file),内容如下:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "QiniuConfig.h"
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#include "com_ttdevs_ndk_CipherUtil.h"
JNIEXPORT jstring JNICALL Java_com_ttdevs_ndk_CipherUtil_getCipherKey(JNIEnv *env, jclass)
{
return (*env).NewStringUTF("Hello World! getCipherKey");
}
我们还需要创建两个文件,一个叫Android.mk,另一个叫Application.mk,他们都在jni目录下。
# http://developer.android.com/intl/zh-tw/ndk/guides/android_mk.html
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Cipher
LOCAL_SRC_FILES := Cipher.cpp
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
# http://developer.android.com/intl/zh-tw/ndk/guides/application_mk.html
# APP_STL := stlport_static
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -std=c++11
APP_CFLAGS += -Wno-error=format-security
APP_ABI := all
还没有完,我们还需要修改当前Project或者Module的gradle.build文件:
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
ndk {
moduleName "ndkutil"
}
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDir "libs"
}
...
}
完成这些,我们就可以进行编译了。在当前Project(Module)上点击右键,使用之前配置的ndk-build工具进行编译,如果没有问题,我们会在libs目录下看到生产的so文件。好了,最后我们可以编写测试代码了:在java直接调用刚才创建的CipherUtil即可:
Log.d(">>>>>", CipherUtil.getCipherKey());
运行上面代码,我们可以在log中看到输出的字符串:
Hello World! getCipherKey
上述demo中,我们只是简单的返回一个字符串,要实现更安全,我们可以将此方法写的更复杂,比如获取app的签名,获取设备的硬件信息进行复杂的组合,以保障最终生成的秘钥的唯一性和安全性(更难伪造),这里有一个demo可以参考。
写到这里,可能又有人会问到:其实so文件也不是很全,可以通过对汇编的分析得到里面的代码逻辑。当然,高手是可以做到对so文件进行分析的,但是so还是可以阻隔大部分的反编译人员。如果我们能把getCipherKey实现的更好,也会增加破解的成本。另外,so还有一个被盗用的问题,就是别人直接调用我们的so,这个问题也可以通过一定的代码逻辑来避免。之后会继续讲解。
最后再说一点,对于秘钥,我们最终还会被载入我们的内存,如果直接dump我们的内存,会是一个人什么样的结果呢?