Android Ndk的应用

Posted by Codeboy on May 4, 2015

做android开发,或多或少应该对ndk有些了解。大家都知道,开发android应用很多部分是使用java完成的,但是java语言使用起来虽然简单,但是也比较容易进行反编译,尽管现在网络上有很多的加密工具。那怎么保护应用的一些隐私逻辑模块(加解密)的,ndk是一个很好的选择。

ndk使用c或者cpp完成代码的编写,使用c或者cpp可以将一些模块编译为链接库(so文件),这些文件反编译起来则非常的困难,同时使用c和cpp写出的代码在执行效率上会有所提升。本文将展示使用ndk技术将字符串的简单加解密方法写进so文件中。

考虑到编码等的原因,本文中的加密解密算法方式为:java中将字符串转为为byte数组,然后通过jni调用c语言加解密函数,同时将byte数据传递给c(中间有一部类型转化,将byte数组转化为char数组),c语言对char数组进行加解密后返回。有关ndk的一些简单使用,大家可以看一些麦子学院的教程,本文中只对使用的一些例子进行解释。对于字符串的加解密,按照本文的方式加解密应该是一种不错的方式,使用中您可能需要修改一下c语言中的加解密函数即可。

本文中c语言对char数组的加解密很简单,对每一个char进行拆分,char占8位,高4位与低4位拆成2个数值,然后根据数值从一个长度为16的钥匙串中拿出对应字符,这些字符对应的数组记为加密后的字符串。反向解密原理相似,只需要将字符数组中每2个字符抽取,计算出加密前的数值即可。下面看一下整体ndk的使用。

使用ndk前,需要从android官网上下载ndk组件,解压后大概3G左右,建议一个新项目(AndroidNDK),将新项目目录指定在ndk解压后的根目录(这样比较方便,不这样做也可以),我们需要在新项目中额外添加的只有一个jni目录,jni目录与src,res等是同一层的。目录内需要含有的文件如下所示:

mac:AndroidNDK YD$ tree  jni
jni
├── Android.mk
├── Application.mk
└── encrypt.c

其中encrypt.c即是我们的加解密函数所在的位置,Android.mk与Application.mk为配置文件,内容很固定。可以看下各个文件的内容:

Application.mk

APP_ABI := armeabi,armeabi-v7a

Application.mk中还有其他的一些配置,大家可以去官网或者google一下,常用的配置是App_ABI,指定生成对应cpu架构的库文件。

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := codeboy_encrypt
LOCAL_SRC_FILES := encrypt.c
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)

Android.mk中需要修改的内容只有LOCAL_MODULE和LOCAL_SRC_FILES,前者指定生成的连接库名称,后者指定要编译的c语言或者cpp的文件名字,其他的保持不变即可。

encrypt.c

#include<string.h>
#include<jni.h>
#include<android/log.h>

//宏定义打印函数,使用方法 LOGI("hello") 或者 LOGI("money %d",15)
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "native", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "native", __VA_ARGS__))

const char key[] = "abcdefghijklmnop"; //16个字符
int len = 0;

//计算字符对应的byte值
unsigned char getByteNumber(unsigned char first, unsigned char end) {
    int firstPosition = 0, endPosition = 0;
    int position = 0;
    for (; position < 16; position++) {
        if (key[position] == first) {
            firstPosition = position;
        }
        if (key[position] == end) {
            endPosition = position;
        }
    }
    return (firstPosition << 4) | (endPosition);
}

//加密函数
void encrypt(unsigned char p[], unsigned char res[]) {
    int i = 0;
    for (; i < len; i++) {
        res[2 * i] = key[p[i] / 16];
        res[2 * i + 1] = key[p[i] % 16];
    }
}

//解密函数
void decrypt(unsigned char p[], char res[]) {
    int i;
    for (i = 0; i < len; i++) {
        res[i] = getByteNumber(p[i * 2], p[i * 2 + 1]);
    }
}

//java中生命的native函数,函数名称格式Java_包名(点换下划线)_类名_函数名
//前两个参数JNIEnv *env, jclass this比较固定,其中第二个参数jclass代表方法是静态的,仅仅是个表示,如果方法不是静态的话,jclass换成jobject
//后续的参数是函数要传进来的参数
//java中的byte数组对应jni中的jbyteArray,jni中的jbyteArray可以通过jni中的函数转换为char数组
jstring Java_me_codeboy_encrypt_EncryptUtil_encrypt(JNIEnv *env, jclass this,
        jbyteArray src) {
    unsigned char *buff = (char*) (*env)->GetByteArrayElements(env, src, NULL);
    len = (*env)->GetArrayLength(env, src);
    //加密后长度变为原先的2倍
    unsigned char res[len * 2];
    encrypt(buff, res);
    //此步骤很重要,标志结束
    res[len * 2] = '\0';

    //使用完毕释放src数组,因为src数组的存在jvm中
    (*env)->ReleaseByteArrayElements(env, src, buff, 0);

    //jni中函数将char数组转变为字符串,jni中字符串为jstring,对应java中的String
    jstring resStr = (*env)->NewStringUTF(env, res);
    return resStr;
}

//和加密类似
jstring Java_me_codeboy_encrypt_EncryptUtil_decrypt(JNIEnv *env, jclass this,
        jbyteArray src) {
    unsigned char *buff = (char*) (*env)->GetByteArrayElements(env, src, NULL);
    len = (*env)->GetArrayLength(env, src);
    //解密后长度变为原先的1/2
    len = len / 2;
    signed char res[len];
    decrypt(buff, res);
    //此步骤很重要,标志结束
    res[len] = '\0';

    //使用完毕释放src数组,因为src数组的存在jvm中
    (*env)->ReleaseByteArrayElements(env, src, buff, 0);

    jstring resStr = (*env)->NewStringUTF(env, res);
    return resStr;
}

这样我们的ndk相关的文件就写好了,下面在终端下切换到AndroidNDK目录下,运行命令即可:

../ndk-build

运行结果如下:

mac:AndroidNDK YD$ ../ndk-build
Android NDK: WARNING: APP_PLATFORM android-21 is larger than android:minSdkVersion 14 in ./AndroidManifest.xml    
[armeabi] Compile thumb  : codeboy_encrypt <= encrypt.c
[armeabi] SharedLibrary  : libcodeboy_encrypt.so
[armeabi] Install        : libcodeboy_encrypt.so => libs/armeabi/libcodeboy_encrypt.so
[armeabi-v7a] Compile thumb  : codeboy_encrypt <= encrypt.c
[armeabi-v7a] SharedLibrary  : libcodeboy_encrypt.so
[armeabi-v7a] Install        : libcodeboy_encrypt.so => libs/armeabi-v7a/libcodeboy_encrypt.so

执行完成后,我们可以看一下libs文件夹,多了一些so文件,如下:

mac:AndroidNDK YD$ tree libs
libs
├── android-support-v4.jar
├── armeabi
│   └── libcodeboy_encrypt.so
└── armeabi-v7a
    └── libcodeboy_encrypt.so

2 directories, 3 files

下面我们就开始写对应的java代码了,将调用c函数的加解密函数抽象到一个类中即可

EncryptUtil.java

package me.codeboy.encrypt;

public class EncryptUtil {
    public native static String encrypt(byte[] src); // 加密函数

    public native static String decrypt(byte[] src); // 解密函数

    static {
        System.loadLibrary("codeboy_encrypt");
    }

    /**
     * 加密函数
     * 
     * @param src
     * @return
     */
    public static String encrypt(String src) {
        return encrypt(src.getBytes());
    }

    /**
     * 解密函数
     * 
     * @param src
     * @return
     */
    public static String decrypt(String src) {
        return decrypt(src.getBytes());
    }
}

注意C语言中的函数名称中的包名类名函数名要与该类统一,还有对应的链接库名称。

做好了这些以后,我们就可以使用了。在我们的Activity中简单的定义一个按钮,点击后对一个字符串进行加密打印,之后进行解密打印,Activity中的代码如下:

package me.codeboy.ndk.ui;

import me.codeboy.encrypt.EncryptUtil;
import me.codeboy.ndk.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.main_ui);
        super.onCreate(savedInstanceState);

        Button btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                String src = "我是玄恒,欢迎访问我的网站codeboy.me";
                String res = EncryptUtil.encrypt(src);
                System.out.println("------>" + res);
                res = EncryptUtil.decrypt(res);
                System.out.println("--->" + res);
            }
        });
    }
}

点击按钮后可以看到点击button打印的结果:

--->ogiijbogjikpohioieogibjcoplmimogkmkcoilpiooikolpojjhkoogiijbohjkieohlnjbohkljjgdgpgegfgcgphjcogngf
--->我是玄恒,欢迎访问我的网站codeboy.me

这样下来我们就完成了简单的加解密操作,也对ndk有了一个初步的了解。

如有任何知识产权、版权问题或理论错误,还请指正。

转载请注明原作者及以上信息。