DRY

Web関連の技術の事、食事/お酒の事、旅行の事など

JNIでAESの暗号化/復号化する方法

Android上で、KEYとIVを用いてAESで暗号化された2Mぐらいのファイルを復号化したのですが、スゴイ時間が掛かるんです。。。
私のエミュレータ上で16000(m/s)ぐらい。

ですのでJNIで復号化する処理を作成しました。結果的にはまったく同じファイルで7000(m/s)ぐらいになりました。
その差23倍ですが、まあやっぱり計算系やらせると速いのかなと。

JNI側では復号化はopensslのライブラリを呼んでいます。
その辺りは以前のエントリを見て頂ければ。


まずは、単純にJavaで復号化するソースです


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import android.util.Log;


public class Decrypt
{
public void main (String key, String iv, String file_path)
{
FileInputStream fis = null;
try
{
// 受け取ったファイルパスを元にデータをバッファの格納
File file = new File (file_path);
fis = new FileInputStream (file);

int available = fis.available ();
byte[] tmp = new byte[available];
fis.read (tmp);

try
{
// key, ivを用いて復号化
byte[] decrypt_contents_info = decrypt (key.getBytes (), key.getBytes (), tmp);
}
catch (Exception e)
{
Log.e ("Decrypt", e.getMessage ());
}
}
catch (IOException e)
{
Log.e ("Decrypt", e.getMessage ());
}
finally
{
try
{
fis.close ();
}
catch (Exception e)
{}
}
}

private static byte[] decrypt (byte[] key, byte[] iv, byte[] input)
throws
NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
InvalidAlgorithmParameterException,
IllegalBlockSizeException,
BadPaddingException
{
final SecretKey secret_key = new SecretKeySpec (key, "AES");
final Cipher cipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
cipher.init (Cipher.DECRYPT_MODE, secret_key, new IvParameterSpec (iv));
return cipher.doFinal (input);
}
}

ソース的にはこれだけなんですが、処理にすごく時間が掛かるのでJNIに変更します。

JNIを用いた場合は以下のようになります。復号化したデータの配列を戻します。
Java側はこちら


import java.io.File;
import java.io.FileInputStream;

import android.util.Log;


public class DecryptNative
{
static
{
// JNIライブラリのロード
System.loadLibrary ("DecryptNative");
}

// JNIメソッドの定義
public native byte[] decrypt_native (String key, String iv, byte[] data);

public void main (String key, String iv, String file_path)
{
FileInputStream fis = null;
byte[] ret = null;
try
{
File file = new File (file_path);
fis = new FileInputStream (file);

int available = fis.available ();
byte[] tmp = new byte[available];
fis.read (tmp);

// JNIで処理し、復号化したデータ配列を受け取る
ret = decrypt_native (key, iv, tmp);
}
catch (Exception e)
{
Log.e ("DecryptNative", e.getMessage ());
}
finally
{
try
{
fis.close ();
}
catch (Exception e)
{}
}
}
}

JNI側(C++)のソースはこちら
#jp_k16_DecryptNative.h


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

#include

#include
#include

#include "aes.h"


/* Header for class jp_k16_DecryptNative */

#ifndef _Included_jp_k16_DecryptNative
#define _Included_jp_k16_DecryptNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jp_k16_DecryptNative
* Method: decrypt_native
* Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jbyteArray JNICALL jp_k16_DecryptNative_decrypt_1native (JNIEnv *, jobject, jstring, jstring, jbyteArray);

#ifdef __cplusplus
}
#endif
#endif


#jp_k16_DecryptNative.cpp


#include "jp_k16_DecryptNative.h"


jbyteArray
jp_k16_DecryptNative_decrypt_1native (JNIEnv* env, jobject thiz,
jstring _key, jstring _iv, jbyteArray _data)
{
const char* key = env->GetStringUTFChars (_key, NULL);
__android_log_print (ANDROID_LOG_DEBUG, LOG_TAG, "key = %s", key);

const char* iv = env->GetStringUTFChars (_iv, NULL);
__android_log_print (ANDROID_LOG_DEBUG, LOG_TAG, "iv = %s", iv);

// Decryptするデータの正確なサイズを確認
int length = env->GetArrayLength (_data);
__android_log_print (ANDROID_LOG_DEBUG, LOG_TAG, "length = %d", length);
if (length == 0)
{
// InputDataのサイズが"0"とか意味不明
__android_log_write (ANDROID_LOG_ERROR, LOG_TAG, "The object data is empty.");
return NULL;
}

// InputeData配列をjbyte*のバッファにコピー
jbyte* input_data = NULL;
input_data = (jbyte*)malloc (length);
env->GetByteArrayRegion (_data, 0, length, input_data);

jbyte* out_data = NULL;
out_data = (jbyte*)malloc (length);

// opensslの関数を使ってDecrypt開始
AES_KEY dec_key;
::memset (&dec_key, 0, sizeof (AES_KEY));
AES_set_decrypt_key ((unsigned char*)key, 128, &dec_key);
AES_cbc_encrypt ((unsigned char*)input_data, (unsigned char*)out_data, length, &dec_key, (unsigned char*)iv, AES_DECRYPT);

jbyteArray result = env->NewByteArray (length);
if (result == NULL)
{
// 戻り値の配列がアロケートできないなんて。。。
__android_log_write (ANDROID_LOG_ERROR, LOG_TAG, "Couldn't allocate array 'result' for parameter data.");
return NULL;
}

void* decrypted_data = env->GetPrimitiveArrayCritical (result, NULL);
::memcpy (decrypted_data, out_data, (length * sizeof (unsigned char)));
env->ReleasePrimitiveArrayCritical (result, decrypted_data, 0);

// Decryptしたデータの配列を返す
return result;
}


これで冒頭書いたぐらいのパフォーマンスは得られました。実機ならもう少し早くなるかな〜?とも思います。

ちなみにencryptしたければ、openssl関数呼び出しの部分を以下のようにencrypt用に変えればいいはずです。


AES_set_encrypt_key ((unsigned char*)key, 128, &dec_key);
AES_cbc_encrypt ((unsigned char*)input_data, (unsigned char*)out_data, length, &dec_key, (unsigned char*)iv, AES_ENCRYPT);


JNIはWeb/本ともにそんなに資料が多くないので、この本は重宝しました。