最近項目中用到數據加密解密的功能,由於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代碼中調用該文件編譯運行就可以了。
囧~第一次寫博客~~下班=_=