JNI调用c++实现AES加密解密
最近项目中用到数据加密解密的功能,由于Android、iOS以及服务器端都需要用到这个功能。而不同平台上加密出来的密文是不一样的,这样导致互相之间密文无法使用。于是决定使用C/C++完成加密解密,其他平台调用的方式进行处理。
- 加密解密实现
AES加密的具体实现过程本文暂不讨论,实现代码是直接从openssl源码中抽出来。
加密解密调用以下两个方法:
int aes_encrypt(char * in, char* key, char * out) 加密//明文,密钥,密文 int aes_decrypt(char * in, char* key, char * out) 解密//密文,密钥,明文
明文需要从外部获取;一般情况下key需要自定义,所以也是外部获取;out是加密或者解密的结果,需要返回给调用者。
所以接下来需要做的就是获取外部数据,交给加密解密方法处理,返回结果到外部。
- 编写java本地方法
java调用c/c++代码是通过JNI来实现,在java中需要声明native方法。
public class AESUtil{ public native String encrypt(String plainText, String key); public native String decrypt(String cipherText, String key); }
javac编译AESUtil.java生成class文件AESUtil.class。
javah AESUtil生成AESUtil.h,该文件将会作为头文件包含到c项目中去。
打开AESUtil.h,代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class AESUtil */ #ifndef _Included_AESUtil #define _Included_AESUtil #ifdef __cplusplus extern "C" { #endif /* * Class: AESUtil * Method: encrypt * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_AESUtil_encrypt (JNIEnv *, jobject, jstring, jstring); /* * Class: AESUtil * Method: decrypt * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_AESUtil_decrypt (JNIEnv *, jobject, jstring, jstring); #ifdef __cplusplus } #endif #endif
头文件中声明了两个方法Java_AESUtil_encrypt和Java_AESUtil_decrypt,分别对应java中的两个native方法encrypt和decrypt。而两个native方法中传的两个String类型的参数,在头文件中被转化为jstring类型。通过该类型我们可以实现java和c之间字符串的转换。
- 在c++中实现java中调用的方法
引入头文件后就需要实现这两个方法。新建AESUtil.cpp,引入AESUtil.h,实现声明的加密解密方法:
#define LEN 512 JNIEXPORT jstring JNICALL Java_AESUtil_encrypt (JNIEnv *env, jobject obj, jstring s, jstring k){ //将需要加密的字符串转化为const char*类型 const char* str = env->GetStringUTFChars(s, 0); //密钥字符串转化成char* char* key = (char *)env->GetStringUTFChars(k,0); int i; char source[LEN]; char dst[LEN]; memset((char*)source, 0 ,LEN); memset((char*)dst, 0 ,LEN); strcpy(source, str); if(!aes_encrypt(source,key,dst))//(in,key,out)//加密 { printf("encrypt error\n"); } char t[3]; string tempStr; int realLen=LEN; for(i=LEN-1;!dst[i];i--){// 加密结果中可能包含‘\0’,而‘\0’是C++中字符串的结尾标志,所以为了保证‘\0’之后的密文可以被取出,从数组尾部开始往前,第一个不是‘\0’的元素就是我们要取的最后一个值 realLen = i; } for(i= 0;i<=realLen-1;i+=1){//将加密结果转化为十六进制,拼接成字符串输出 sprintf(t, "%x", (unsigned char)dst[i]); if((unsigned char)dst[i]<=0x0f){ tempStr = tempStr+"0"+t; }else{ tempStr = tempStr+t; } } char *data=(char *)tempStr.data(); return env->NewStringUTF(data);//通过JNI提供的转化方法将char*转化为jstring作为结果返回 } JNIEXPORT jstring JNICALL Java_AESUtil_decrypt (JNIEnv *env, jobject obj, jstring s, jstring k){ const char* str = env->GetStringUTFChars(s, 0); char* key = (char *)env->GetStringUTFChars(k,0); int i; char source[LEN]; char dst[LEN]; memset((char*)source, 0 ,LEN); memset((char*)dst, 0 ,LEN); strcpy(dst,str); char data[LEN]; int j = 0; memset((char*)data, 0 ,LEN); int len=strlen(dst); for(i=0;dst[i];i++){ if((i+1)%2==0){//加密结果中字符串两两分隔组成十六进制转化为具体值存入数组以供解密 data[j] = ascii2hex(&((char)dst[i-1]),1)*16+ascii2hex(&((char)dst[i]),1); j++; } } if(!aes_decrypt(data,key,source)) { printf("decrypt error\n"); } return env->NewStringUTF(source); }
int aes_encrypt(char* in, char* key, char* out)
{
AES_KEY aes;
int len = strlen(in), en_len = 0;
if (!in || !key || !out) return 0;
if (AES_set_encrypt_key((unsigned char*)key, 128, &aes) < 0)
{
return 0;
}
while (en_len < len)
{
AES_encrypt((unsigned char*)in, (unsigned char*)out, &aes);
in += AES_BLOCK_SIZE;
out += AES_BLOCK_SIZE;
en_len += AES_BLOCK_SIZE;
}
return 1;
}
int aes_decrypt(char* in, char* key, char* out)
{
AES_KEY aes;
int len = MSG_LEN, en_len = 0;
for (size_t i = MSG_LEN - 1; !in[i]; i--)//修改计算in长度方式,以防出现加密内容包含'\0'的情况
{
len = i;
}
if (!in || !key || !out) return 0;
if (AES_set_decrypt_key((unsigned char*)key, 128, &aes) < 0)
{
return 0;
}
while (en_len < len)
{
AES_decrypt((unsigned char*)in, (unsigned char*)out, &aes);
in += AES_BLOCK_SIZE;
out += AES_BLOCK_SIZE;
en_len += AES_BLOCK_SIZE;
}
return 1;
}
可以看到生成的头文件中引入了jni.h,我们在进行java和c++之间字符串转换时使用的方法就是来自于该文件。jni.h可以从jdk安装目录\include下找到,同时引入jni_mod.h,可以从jdk安装目录\include\win32下找到。
java调用c++需要通过调用dll来完成,所以我们需要将c++生成dll,我这里是直接在vs2010中新建dll项目生成的,具体可以google。
生成dll后将其加入classpath,我是直接放入了jdk根目录\bin下面。也可以将dll所在目录加入环境变量。
- 调用java本地方法
dll生成了,java方法有了,下面就是调用方法进行测试啦~~
public class StringUtils { public static void main(String[] args) { System.loadLibrary("AESCPP");//加载dll,不需要包含.dll后缀名 AESUtil s = new AESUtil();//AESUtil中包含了native方法 String plain = "testdata@#&*99HUIWB1=-";//明文 System.out.println(plain); String key = "1234567890123456afhiu$^&682036490";//密钥 String str = s.encrypt(plain,key);//加密 System.out.println("\nhahaha:\n "+str);//打印结果 System.out.println("length: "+str.length()); System.out.println("@@@@@@@@@@@"); String ss = s.decrypt(str,key);//解密 System.out.println("----->-------->"); System.out.println(ss);//解密结果 } }
跑一下~
可以发现加密解密结果一致。
在c++代码中我们将得到的结果存储在dst[]数组中,我们所得到的密文正是数组中的值转化为十六进制输出后拼成的字符串。解密的时候我们需要将这些字符串两两拆开(62,f8,a8…)重新存入数组中进行解密。
windows下大概就是这样。
linux下由于无法调用dll文件,需要生成.so文件。另外引入jni.h和jni_mod.h的时候需要的是linux下jdk目录中的这两个文件。具体操作可以google,和windows下类似。
- Android中调用
之前在linux下生成.so是因为android中无法使用dll,但是linux下生成成功后放入android发现还是不可以使用,对android和linux也不是太了解,经过查阅发现linux是x86_64平台,而测试的Android手机是arm平台。。
这样有发现了个新玩意:NDK~~
具体NDK环境搭建就不赘述,我是按这个链接来搭建的http://my.oschina.net/lifj/blog/176916
环境搭建完毕,首先在eclipse下新建一个Android项目AESTest。
右击项目名选择Android Tools->Add Native Support.
打开jni目录下的Android.mk文件,可以看到如下代码:(没有可以自己加入)
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := AESUtil LOCAL_SRC_FILES := AESUtil.cpp include $(BUILD_SHARED_LIBRARY)
其中LOCAL_MODULE所对应的值就是加载库时所要用的字符串(System.loadLibrary("XXX");)
LOCAL_SRC_FILES所对应的值则代表了实现方法所在的文件。
jni目录下新建文件Application.mk,加入
APP_STL:=stlport_static
不加这个文件之前涉及到c++的方法都会报错,具体原因尚未探究,只是搜报错信息从stackoverflow上找到了这个方法。
在jni目录中加入我们之前生成dll时用到的头文件和实现文件.
打开Cygwin,进入项目根目录,输入$NDK/ndk-build就可以生成so文件了
可以看到so文件生成在libs/armeabi文件夹下,在android代码中调用该文件编译运行就可以了。
囧~第一次写博客~~下班=_=