加密ArcGIS离线地图及其在Android上的实现

    技术2022-05-13  15

     

    加密ArcGIS离线地图的思路

    目前,使用Compact格式的切片文件是离线地图的一个很好的方案,但是,如果我们可能会希望限制离线地图不被第三方程序使用;或者,希望限制离线地图只被经过授权的设备使用。在这样的需求下,我们必须保护好部署在智能设备上的离线地图数据,因此,需要对离线地图数据进行加密。

    在这里,我使用了这样的一个思路,其中包含以下主要环节:

    1. 经授权的设备序号+保密的标识符再经过MD5生成校验值。

    2. MD5校验值与加密的离线数据一起分发,由于第三方程序无法得知保密的标识符,因此无法生成正确的校验值。

    3. 离线数据的加密通过加密索引文件实现,加密通过字节交换实现,这样可以基本不影响性能。

    4. 读取加密文件的算法封装在动态连接库中,确保第三方无法通过反编译手段获得算法。

    下面详细叙述各个环节的实现。

    设备唯一身份的确认

    设备的唯一序号可能在不同种类的系统上都有不同获取的方法,通过CPU序号、IMEI编号、MAC地址等多种途径的组合可以生成每个设备都不同的标识符,比如在Android中,可以以IMEI和IMSI的组合生成一个序号:

    TelephonyManager tm = (TelephonyManager) this

    .getSystemService(Context.TELEPHONY_SERVICE);

    String imei = tm.getDeviceId();

    String imsi = tm.getSubscriberId();

    deviceId = String.format("%s-%s", imei, imsi);

    比如我这里得到一个设备标识“000000000000000-310260000000000”,下面根据不同情况,对上述的设备标识附加一个保密的标识符,再计算其MD5校验值:

    String id = String.format("%s-%s", deviceId, "wuyf_qwert");

    MessageDigest md = MessageDigest.getInstance("MD5");

    byte[] bytes = md.digest(id.getBytes());

    result = StringUtil.bytesToHexString(bytes);

    这里的“wuyf_qwert”就是自己定义的保密标识符,这个保密标识符只有数据的发布者才知道,因此第三方无法通过设备标识符自行生成校验值。最后,可以将校验值保存在一个以设备序号命名的文件中,和数据一起发布(多个设备使用多个校验文件,增加删除都很方便)。

    图 1 与数据一同部署的校验文件

    离线地图的加密

    考虑性能的影响,对离线地图的加密需要使用尽可能简单的加密方法,因此,这里使用对离线地图的索引文件进行加密的方法。对于原始的索引数据文件,我们只需要对若干字节进行交换即可,用户可以根据需要改变自己的加密方法,同样,这个加密方法只有数据的发布者才知道:

    static public void encrypt(String inPath, String outPath) {

    FileInputStream in = null;

    FileOutputStream out = null;

    try {

    File inFile = new File(inPath);

    File outFile = new File(outPath);

    in = new FileInputStream(inFile);

    out = new FileOutputStream(outFile, false);

    int read;

    read = in.read();

    int count = 0;

    while (read != -1) {

    byte b = (byte) read;

    // 此处可以增加置换对数以增加文件的复杂度

    if(read==3){

    b = (byte)37;

    count++;

    }else if(read==37){

    b = (byte)3;

    count++;

    }

    out.write(b);

    read = in.read();

    }

    out.flush();

    System.out.println(count);

    } catch (Exception ex) {

    ex.printStackTrace();

    } finally {

    try {

    in.close();

    out.close();

    } catch (Exception ex) {}

    }

    }

    在设备上读取加密的离线地图

    设备上读取加密的离线地图分为2步:校验设备的身份和获取加密的数据,这两步都必须封装在动态连接库中确保算法的保密。在Android上,需要通过JNI实现,我们可以把这两步都封装在一个C函数中:

    JNIEXPORT jbyteArray JNICALL Java_com_esri_wuyf_JNI_getEncryptTile(JNIEnv* env,

    jobject obj, jstring strDeviceId, jstring strLocation,

    jstring strBundleBase, jint level, jint row, jint col) {

    jbyteArray result = 0;

    const char* deviceId = (*env)->GetStringUTFChars(env, strDeviceId, 0);

    const char* location = (*env)->GetStringUTFChars(env, strLocation, 0);

    const char* bundleBase = (*env)->GetStringUTFChars(env, strBundleBase, 0);

    __android_log_write(ANDROID_LOG_INFO, "JNI 设备编号", deviceId);

    __android_log_write(ANDROID_LOG_INFO, "JNI 数据位置", location);

    __android_log_write(ANDROID_LOG_INFO, "JNI 数据位于", bundleBase);

    // 生成一些路径

    const char* sValid = my_strcat(location, deviceId);

    const char* sIndex = my_strcat(my_strcat(location, "_alllayers/"),

    my_strcat(bundleBase, ".bundly"));

    const char* sTile = my_strcat(my_strcat(location, "_alllayers/"),

    my_strcat(bundleBase, ".bundle"));

    // 设备标识需要连接一个秘密的字符串

    const char* security = "-wuyf_qwert";

    const char* s = my_strcat(deviceId, security);

    // 生成MD5校验值,MD5结果全部使用小写

    struct MD5Context md5c;

    MD5Init(&md5c);

    MD5Update(&md5c, s, strlen(s));

    unsigned char ss[16];

    MD5Final(ss, &md5c);

    // 检查MD5校验是不是满足,如果不满足则立即返回,不进行后续处理

    int valid = 0;

    FILE* fValid;

    if ((fValid = fopen(sValid, "rb")) != NULL) {

    char str[32];

    fread(str, 32, 1, fValid);

    int i;

    int hasError = 0;

    for (i = 0; i < 16; i++) {

    unsigned int s1 = ss[i];

    int s2 = str[2 * i];

    if (s2 >= 48 && s2 <= 57)

    s2 -= 48;

    else if (s2 >= 97 && s2 <= 102)

    s2 -= 87;

    int s3 = str[2 * i + 1];

    if (s3 >= 48 && s3 <= 57)

    s3 -= 48;

    else if (s3 >= 97 && s3 <= 102)

    s3 -= 87;

    if (s1 != 16 * s2 + s3) {

    hasError = 1;

    break;

    }

    }

    if (hasError == 0) {

    valid = 1;

    }

    }

    fclose(fValid);

    if (valid == 1) {

    __android_log_write(ANDROID_LOG_INFO, "JNI", "设备身份校验通过");

    // 校验无误,开始获取切片

    int rGroup = 128 * (row / 128);

    int cGroup = 128 * (col / 128);

    int index = 128 * (col - cGroup) + (row - rGroup);

    __android_log_write(ANDROID_LOG_INFO, "JNI 开始读取加密索引", sIndex);

    FILE* fIndex;

    long offset = -1;

    if ((fIndex = fopen(sIndex, "rb")) != NULL) {

    fseek(fIndex, 16 + 5 * index, SEEK_SET);

    char buffer[5];

    fread(buffer, 5, 1, fIndex);

    int i;

    for (i = 0; i < 5; i++) {

    if (buffer[i] == 3) {

    buffer[i] = 37;

    } else if (buffer[i] == 37) {

    buffer[i] = 3;

    }

    }

    offset = (long) (buffer[0] & 0xff) + (long) (buffer[1] & 0xff)

    * 256 + (long) (buffer[2] & 0xff) * 65536

    + (long) (buffer[3] & 0xff) * 16777216 + (long) (buffer[4]

    & 0xff) * 4294967296;

    }

    fclose(fIndex);

    __android_log_write(ANDROID_LOG_INFO, "JNI 开始读取数据", sTile);

    FILE* fTile;

    if ((fTile = fopen(sTile, "rb")) != NULL) {

    fseek(fTile, offset, SEEK_SET);

    char lengthBytes[4];

    fread(lengthBytes, 4, 1, fTile);

    int length = (int) (lengthBytes[0] & 0xff) + (int) (lengthBytes[1]

    & 0xff) * 256 + (int) (lengthBytes[2] & 0xff) * 65536

    + (int) (lengthBytes[3] & 0xff) * 16777216;

    char* tile = malloc(sizeof(char) * length);

    fread(tile, length, 1, fTile);

    __android_log_write(ANDROID_LOG_INFO, "JNI", "获取数据成功");

    result = (*env)->NewByteArray(env, length);

    (*env)->SetByteArrayRegion(env, result, 0, length, tile);

    free(tile);

    }

    fclose(fTile);

    }

    free((void*) s);

    free((void*) sValid);

    free((void*) sIndex);

    free((void*) sTile);

    return result;

    }

    上述代码中高亮的2段分别对应了校验设备和解密数据的关键,可以看到这和前面的算法是可以对应起来的,当然,这个算法只有数据的发布者掌握。

    在Android程序中,获取地图数据只需要调用一个Java方法就可以:

    result = jni.getEncryptTile(deviceId, location, bundleBase, level, row, col);

    现在,即使反编译了Android程序中的dex文件,你也无法知道这句代码背后调用的动态链接库中实际的算法。

    下面是加密ArcGIS离线地图在Android上的效果:

    图 2 Android上显示加密离线地图的效果

    附录:Android上进行JNI开发的步骤

    准备开发环境

    1. 下载安装Cygwin:http://cygwin.com/setup.exe,注意安装时需要选择Devel工具包以及vim(用以编辑环境变量)。

    2. 下载Android NDK,我使用的是r5版本:http://dl.google.com/android/ndk/android-ndk-r5-windows.zip,解压到本地磁盘。

    新建一个Java类

    在Android工程中新建一个Java类,注意”native”标记:

    package com.esri.wuyf;

    public class JNI {

    public native byte[] getEncryptTile(String deviceId, String location, String bundleBase, int level, int row, int col);

    }

    使用Java工具生成JNI头文件

    在Android工程的bin目录下运行命令行:

    Microsoft Windows [版本 6.1.7600]

    版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

    D:/wuyf/Workspace/Android/AgsEncryptTiles/bin>javah -jni com.esri.wuyf.JNI

    执行成功后生成一个com_esri_wuyf_JNI.h文件,现在在Android工程的根目录下新建一个“jni”文件夹,并将生成的这个C头文件拷贝到该目录中:

    /* DO NOT EDIT THIS FILE - it is machine generated */

    #include

    /* Header for class com_esri_wuyf_JNI */

    #ifndef _Included_com_esri_wuyf_JNI

    #define _Included_com_esri_wuyf_JNI

    #ifdef __cplusplus

    extern "C" {

    #endif

    /*

    * Class: com_esri_wuyf_JNI

    * Method: getEncryptTile

    * Signature: (Ljava/lang/String;III)[B

    */

    JNIEXPORT jbyteArray JNICALL Java_com_esri_wuyf_JNI_getEncryptTile

    (JNIEnv *, jobject, jstring, jstring, jstring, jint, jint, jint);

    #ifdef __cplusplus

    }

    #endif

    #endif

    实现JNI的具体方法

    在Android工程的“jni”目录下新建一个com_esri_wuyf_JNI.c文件,并实现头文件中的函数:

    #include "com_esri_wuyf_JNI.h"

    #include "md5.h"

    JNIEXPORT jbyteArray JNICALL Java_com_esri_wuyf_JNI_getEncryptTile(JNIEnv* env,

    jobject obj, jstring strDeviceId, jstring strLocation,

    jstring strBundleBase, jint level, jint row, jint col) {

    ……

    return result;

    }

    新建Android的makefile文件

    在“jni”目录下新建一个文件Android.mk文件,它是Android的makefile:

    LOCAL_PATH:= $(call my-dir)

    include $(CLEAR_VARS)

    LOCAL_SRC_FILES:= com_esri_wuyf_JNI.c md5.c

    LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)

    LOCAL_LDLIBS := -llog

    LOCAL_PRELINK_MODULE := false

    LOCAL_MODULE := JNI

    include $(BUILD_SHARED_LIBRARY)

    编译JNI链接库

    从开始菜单进入Cygwin Bash Shell,首先在当前用户的.bash_profile中添加一个$NDK环境变量(指向Android NDK的解压目录),让我们可以更加方便地编译Android JNI代码:

    $ vi ~/.bash_profile

    export NDK=/cygdrive/d/Software/Develop/Android/android-ndk-windows

    下面,在Cygwin Bash Shell中进入Android工程目录(/cygdrive/d表示Windows中的D盘),并执行NDK的编译命令:

    这个libJNI.so会生成在Android工程的libs/armeabi目录下,注意,调试时这个链接库不会自动更新到Android设备上,因此一旦重新编译这个链接库,需要手动push到设备的相应目录(这里是/data/data/com.esri.wuyf/lib)下:

    在Java代码中调用JNI函数

    在使用JNI函数的Java类中静态加载JNI库,然后新建JNI对象并调用其相应的方法:

    static {

    System.loadLibrary("JNI");

    }

    JNI jni = new JNI();

    result = jni.getEncryptTile(deviceId, location, bundleBase, level, row, col);

    将离线数据push到设备中

    开发中还需要将离线数据批量push到设备中,这需要用Android的adb工具:

    D:/Software/Develop/Android/android-sdk-windows/platform-tools>adb push D:/Temp /sdcard

    这表示要将D:/Temp下所有内容push到设备的SD卡中。注意,这个Temp目录并不会出现在SD卡中。

    另外,如果需要从SD卡批量删除文件必须进入shell执行Linux的rm命令:

    >adb remount

    >adb shell

    # rm -R /sdcard/xxx


    最新回复(0)