この記事は、インテル® デベロッパー・ゾーンに公開されている「Encryption/Decryption – invoking OpenSSL API through JNI calls」(https://software.intel.com/en-us/blogs/2014/12/05/encryptiondecryption-invoking-openssl-through-jni-calls) の日本語参考訳です。
この記事の PDF 版はこちらからご利用になれます。
ここでは、OpenSSL* ライブラリーを使用して Android* アプリケーションにインテル® AES-NI 命令を統合する手順を紹介します。手順に従うことで、インテル® AES-NI 命令によるアクセラレーションを利用する JNI アプリケーションをビルドできます。
インテル® AES New Instructions (インテル® AES-NI)
2008 年 3 月に発表されたインテル® AES-NI は、インテル® マイクロプロセッサー向けの x86 命令セット・アーキテクチャーの拡張です。この命令セットは、Advanced Encryption Standard (AES) を使用して暗号化/復号化を行うアプリケーションのパフォーマンス、セキュリティー、電力効率を向上することを目的としています。
Android* でインテル® AES-NI を使用する
OpenSSL* ライブラリーの AES アルゴリズムは、ネイティブ Java* プロバイダーと比べてパフォーマンスを大幅に向上します。これは、OpenSSL* ライブラリーはインテル® プロセッサー向けに最適化されており、インテル® AES-NI 命令を使用するためです。以下に、OpenSSL* プロバイダーを使用してファイルを暗号化する手順を示します。
Android* 4.3 から、Android* オープンソース・プロジェクト (AOSP) の OpenSSL* はインテル® AES-NI をサポートするようになったため、適切な設定でコンパイルするだけで済みます。また、公式 Web サイトからソースをダウンロードしてコンパイルすると、プロジェクト・ディレクトリーで *.a/*.so を利用できるようになります。暗号化ライブラリーを入手する方法は 2 つあります。
AOSP ソースを保持している場合は、http://www.openssl.org/source/ (英語) から OpenSSL* をダウンロードできます。最新のバージョンを使用することで、openssl の以前のバージョンで見つかった脆弱性を回避することができます。AOSP には openssl ライブラリーが含まれており、このライブラリーをアプリケーションの jni フォルダーに配置することで、インクルード・ディレクトリーにアクセスできるようになります。
openssl ソースをダウンロードしてクロスコンパイルし、ライブラリーを作成する場合は、次の操作を行います。
ソースコードをダウンロードします。
wget https://www.openssl.org/source/openssl-1.0.1j.tar.gz
コンソールで次のコマンドを実行して、コンパイルします (NDK 変数は、使用しているディストリビューションのフルパスに設定します)。
export NDK=~/android-ndk-r9d export TOOL=arm-linux-androideabi export NDK_TOOLCHAIN_BASENAME=${TOOLCHAIN_PATH}/${TOOL} export CC=$NDK_TOOLCHAIN_BASE-gcc export CXX=$NDK_TOOLCHAIN_BASENAME-g++ export LINK=${CXX} export LD=$NDK_TOOLCHAIN_BASENAME-ld export AR=$NDK_TOOLCHAIN_BASENAME-ar export STRIP=$NDK_TOOLCHAIN_BASENAME-strip export ARCH_FLAGS=”-march=armv7-a –mfloat-abi=softfp –mfpu=vfpv3-d16” export ARCH_LINK=”-march=armv7-a –Wl, --flx-cortex-a” export CPPFLAGS=”${ARCH_FLAGS} –fpic –ffunction-sections –funwind-tables –fstack-protector –fno-strict-aliasing –finline-limited=64” export LDFLAGS=”${ARCH_LINK”} export CXXFLAGS=”${ ARCH_FLAGS} –fpic –ffunction-sections –funwind-tables –fstack-protector –fno-strict-aliasing –finline-limited=64 –frtti –fexceptions” cd $OPENSSL_SRC_PATH export CC=”$STANDALONE_TOOCHAIN_PATH/bin/i686-linux-android-gcc –mtune=atome –march=atom –sysroot=$STANDALONE_TOOCHAIN_PATH/sysroot” export AR=$STANDALONE_TOOCHAIN_PATH/bin/i686-linux-android-ar export RANLIB=$STANDALONE_TOOCHAIN_PATH/bin/i686-linux-android-ranlib ./Configure android-x86 –DOPENSSL_IA32_SSE2 –DAES_ASM –DVPAES_ASM make
トップ・ディレクトリーに libcrypto.a が作成されます。*.so ファイルを使用する場合は、 “Configure shared android-x86 ***” と入力します。
AOSP ソースコードを保持している場合、ndk ツールチェーンは不要です。
source build/envsetiup.sh lunch <options> make –j8 cd external/openssl mm
このコマンドを実行すると、libcrypto.a が out/host/linux_x86/bin に生成されます。
Android* プロジェクトで NDK から OpenSSL* を使用する
- Android* プロジェクトを作成し、任意の IDE でファイルを暗号化します。ここでは Eclipse* IDE を使用します。
- Android.mk で、OpenSSL* 関連の関数をネイティブ関数として宣言子します。
- ソースの Android* プロジェクトで jni フォルダーを作成します。
- jni フォルダー以下にプリコンパイル済みのインクルード・ディレクトリーを作成します。
- jni フォルダー以下の <OpenSSL source/include/> にある openssl ライブラリー・フォルダーをインクルードします。
- jni/*.c で暗号化を行う C 関数を記述し、暗号化を実装します。その後、*.a/*.so とヘッダーファイルをプロジェクトにコピーします。
- ステップ 1 でシステム・ライブラリーとして作成した Android* クラス関数で、jni フォルダーにあるライブラリーと C 実装をロードします。
以下に、アプリケーションに openssl ライブラリーをインクルードして、Java* クラスで呼び出す方法を示します。
新しいプロジェクトを作成します。ここでは、Eclipse* で EncryptFileOpenSSL というプロジェクトを作成します。Eclipse* (Project Explorer でプロジェクト名を右クリック) またはターミナルで、jni ディレクトリーを作成し、次に pre-compiled サブディレクトリーと include サブディレクトリーを作成します。
ターミナルを使用する場合:
cd <workspace/of/Project> mkdir jni/pre-compiled/ mkdir jni/include cp $OPENSSL_PATH/libcrypto.a jni/pre-compiled cp –L -rf $OPENSSL_PATH/include/openssl jni/include gedit jni/Android.mk
次の行を jni/Android.mk ファイルに追加します。
… LOCAL_MODULE := static LOCAL_SRC_FILES := pre-compiled/libcrypto.a … LOCAL_C_INCLUDES := include LOCAL_STATIC_LIBRARIES := static –lcrypto …
OpenSSL* により提供される関数を利用して encrypt/decrypt/SSL 関数を実装します。インテル® AES-NI を使用するには、次に示すように、一連の EVP_* 関数を利用します。そうすることで、CPU でサポートされる場合、自動的にインテル® AES-NI による AES 暗号化/復号化のアクセラレーションが行われます。例えば、OpenSSL* プロバイダーを利用してファイルを暗号化するクラスを記述する場合、*.java クラスの暗号化関数は次のようになります (このソースコードは、Christopher Bird の「サンプルコード: データ暗号化アプリケーション」 からの引用です)。
public long encryptFile(String encFilepath, String origFilepath) { File fileIn = new File(origFilepath); if (fileIn.isFile()) { ret = encodeFileFromJNI(encFilepath, origFilepath); } else { Log.d(TAG, "ERROR*** File does not exist:" + origFilepath); seconds = -1; } if (ret == -1) { throw new IllegalArgumentException("encrypt file execution did not succeed."); } } // encodeFile ライブラリーにあるネイティブ関数 public native int encodeFileFromJNI(String fileOut, String fileIn); public native void setBlocksizeFromJNI(int blocksize); public native byte[] generateKeyFromJNI(int keysize); /* アプリケーションの起動時に暗号化を行うライブラリー (encodeFile) を * ロードします。 * ライブラリーは、パッケージ・マネージャーにより、インストール時に * /data/data/com.example.openssldataencryption/lib/libencodeFile.so * に展開されています。 */ static { System.loadLibrary("crypto"); System.loadLibrary("encodeFile"); }
System.loadLibrary を使用してロードした encodeFile.cpp にある暗号化関数は、次のようになります。
int encodeFile(const char* filenameOut, const char* filenameIn) { int ret = 0; int filenameInSize = strlen(filenameIn)*sizeof(char)+1; int filenameOutSize = strlen(filenameOut)*sizeof(char)+1; char filename[filenameInSize]; char encFilename[filenameOutSize]; // 初期化されていない場合はキーを作成 int seedbytes = 1024; memset(cKeyBuffer, 0, KEYSIZE ); if (!opensslIsSeeded) { if (!RAND_load_file("/dev/urandom", seedbytes)) { //__android_log_print(ANDROID_LOG_ERROR, TAG, // "Failed to seed OpenSSL RNG"); return -1; } opensslIsSeeded = 1; } if (!RAND_bytes((unsigned char *)cKeyBuffer, KEYSIZE )) { //__android_log_print(ANDROID_LOG_ERROR, TAG, // "Faled to create OpenSSSL random integers: %ul", // ERR_get_error); } strncpy(encFilename, filenameOut, filenameOutSize); encFilename[filenameOutSize-1]=0; strncpy(filename, filenameIn, filenameInSize); filename[filenameInSize-1]=0; EVP_CIPHER_CTX *e_ctx = EVP_CIPHER_CTX_new(); FILE *orig_file, *enc_file; printf ("filename: %s\n" ,filename ); printf ("enc filename: %s\n" ,encFilename ); orig_file = fopen( filename, "rb" ); enc_file = fopen ( encFilename, "wb" ); unsigned char *encData, *origData; int encData_len = 0; int len = 0; int bytesread = 0; /** * 暗号化 */ //if (!(EVP_EncryptInit_ex(e_ctx, EVP_aes_256_cbc(), NULL, key, iv ))) { if (!(EVP_EncryptInit_ex(e_ctx, EVP_aes_256_cbc(), NULL, cKeyBuffer, iv ))) { ret = -1; printf( "ERROR: EVP_ENCRYPTINIT_EX\n"); } // 暗号化するファイルの準備 if ( orig_file != NULL ) { origData = new unsigned char[aes_blocksize]; encData = new unsigned char [aes_blocksize+EVP_CIPHER_CTX_block_size(e_ctx)]; // 暗号化でオリジナルよりも 16 バイト長くなる可能性がある printf( "Encoding file: %s\n", filename); bytesread = fread(origData, 1, aes_blocksize, orig_file); // ファイルからバイトを読み込んで暗号化ルーチンに渡す while ( bytesread ) { if (!(EVP_EncryptUpdate(e_ctx, encData, &len, origData, bytesread))) { ret = -1; printf( "ERROR: EVP_ENCRYPTUPDATE\n"); } encData_len = len; fwrite(encData, 1, encData_len, enc_file ); // 次のバイトを読み込む bytesread = fread(origData, 1, aes_blocksize, orig_file); } // 暗号化の最終ステップ if (!(EVP_EncryptFinal_ex(e_ctx, encData, &len))) { ret = -1; printf( "ERROR: EVP_ENCRYPTFINAL_EX\n"); } encData_len = len; fwrite(encData, 1, encData_len, enc_file ); // 暗号化ルーチンを解放 EVP_CIPHER_CTX_free(e_ctx); // ファイルを閉じる printf( "\t>>\n"); fclose(orig_file); fclose(enc_file); } else { printf( "Unable to open files for encoding\n"); ret = -1; return ret; } return ret; }
次に、ndk-build を使用して <source of Application> でコンパイルします。
/<path to android-ndk>/ndk-build APP_ABI=x86
/<PATH\TO\OPENSSL>/include/openssl ディレクトリーを </PATH\to\PROJECT\workspace>/jni/ 以下にコピーします。
*.so/*.a は、/</PATH\to\PROJECT\workspace>/libs/x86/ または /</PATH\to\PROJECT\workspace>/libs/armeabi/ に配置します。
暗号化/複合化に使用する encode.cpp ファイルは、</PATH\to\PROJECT\workspace>/jni/ に配置します。
パフォーマンス解析
次の関数を使用して、CPU 使用状況、メモリー使用状況、ファイルの暗号化にかかった時間を解析することができます。ここで使用するソースコードも Christopher Bird のブログからの引用です。
CPU 使用状況:
以下のコードは、/proc/stat にある情報から CPU 使用状況を読み取ります。
public float readCPUusage() { try { RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r"); String load = reader.readLine(); String[] toks = load.split(" "); long idle1 = Long.parseLong(toks[5]); long cpu1 = Long.parseLong(toks[2]) + Long.parseLong(toks[3]) + Long.parseLong(toks[4]) + Long.parseLong(toks[6]) + Long.parseLong(toks[7]) +Long.parseLong(toks[8]); try { Thread.sleep(360); } catch (Exception e) { } reader.seek(0); load = reader.readLine(); reader.close(); toks = load.split(" "); long idle2 = Long.parseLong(toks[5]); long cpu2 = Long.parseLong(toks[2]) + Long.parseLong(toks[3]) + Long.parseLong(toks[4]) + Long.parseLong(toks[6]) + Long.parseLong(toks[7]) + ong.parseLong(toks[8]); return (float) (cpu2 - cpu1) / ((cpu2 + idle2) - (cpu1 + idle1)); } catch (IOException ex) { ex.printStackTrace(); } return 0; }
メモリー使用状況:
以下のコードは、利用可能なシステムメモリーを読み取ります。
Memory Info は、利用可能なメモリーに関する情報を取得するための Android* API です。
1024B = 1 KB で、1024KB = 1MB なので、 利用可能なメモリーは 1024*1024 == 1048576MB です。
public long readMem(ActivityManager am) { MemoryInfo mi = new MemoryInfo(); am.getMemoryInfo(mi); long availableMegs = mi.availMem / 1048576L; return availableMegs; }
実行時間の測定:
start = System.currentTimeMillis(); // 暗号化を実行 stop = System.currentTimeMillis(); seconds = (stop - start);
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください