В состав платформы Android входит фреймворк Bouncycastle, предназначенный для выполнения криптоопераций, например, шифрования или проверки цифровой подписи. Отличительной чертой данного фреймворка является то, что он целиком написан на Java, без применения нативного кода. Это увеличивает его переносимость, однако значительно уменьшает быстродействие. В первом приближении реализация криптофункций с помощью нативного кода может дать значительный прирост производительности. Это может существенно увеличить быстродействие приложения, использующего криптографию. Посмотрим, подтвердится ли это предположение.
Данным постом я хочу начать серию статей о создании модуля, выполняющего криптооперации на примере шифрования/расшифровки симметричным алгоритмом AES. Для начала необходимо понять, какой прирост производительности может дать применение нативного кода по сравнению со встроенной в ОС реализацией.
Чтобы не заниматься изобретением велосипеда и не реализовывать алгоритм AES заново, будет применена Open Source библиотека Crypto++, содержащая высокопроизводительные реализации многих криптоалгоритмов, включая криптоалгоритмы на эллиптических кривых(!). Используются многие аппаратные особенности современных процессоров от векторных инструкций типа SSE до выровненных распределителей памяти. Библиотека поддерживает множество операционных систем и компиляторов. Попробуем адаптировать ее для Android’а и вызывать ее из управляемого Java-кода.
Для начала необходимо скачать исходники с сайта библиотеки.
После этого создадим проект Eclipse. Дальнейшие шаги:
Вначале стоит написать функции-заглушки, которые будут вызывать нативный код из управляемого.
Методы-заглушки для вызова нативного кода представляют собой обычные объявления методов с использованием ключевого слова
Статический блок, выполняющейся при загрузке класса classloader’ом не содержит ничего, кроме вызова
Функции шифрования/расшифровки вынесены в inner-класс. Это сделано для того, чтобы явно показать то, что используется CBC-режим шифрования. После того, как класс написан и среда успешно откомпилировала его, можно выполнить шаг 3 (создание заголовков для нативного кода).
Также класс AES содержит 2 поля, в которых будут храниться ключ для шифрования и инициализационный вектор, которым инициализируется генератор псевдослучайных чисел.
По сохранению файла Eclipse скомпилирует исходник в class-файл, который понадобится позднее при генерации *.h заголовочных файлов.
Нативная часть разбита на следующие компоненты:
Все файлы нативной части располагаются в подпапке jni проекта Eclipse. Файлы каждого проекта находятся в подпапке
Теперь можно приступить к реализации jni-методов, которые будут вызываться из управляемого кода. Для начала необходимо сгенерировать соответствующие заголовки (файлы *.h).
Необходимо в консоли зайти в папку
Теперь остается реализовать функции, объявленные в заголовках. Реализация функции шифрования будет находиться в файле nativecryptowrapper/aes_base.cpp и приведена ниже:
Здесь стоит обратить внимание на дополнительные использованные функции:
Их исходный код показаны ниже:
Вызов системного криптопровайдера производился таким образом:
Вызов нативной реализации криптопровайдера осуществлялся так:
Процесс сборки нативной части, осуществляемый с помощью утилиты ndk-build, входящей в состав NDK, конфигурируют файл Android.mk и Application.mk. Для сборки проекта необходимо записать значения в переменные, описывающие проект:
Описание проект в файле Android.mk состоит из:
В файле Application.mk содержатся более глобальные настройки. Были использованы следующие опции:
Получившийся файл Android.mk (о дополнительных флагах подробнее ниже), описывающий все три проекта:
Содержимое файла Application.mk:
Чтобы статическая библиотека, содержащая Crypto++ начала корректно собираться, были произведены следующие изменения:
Можно резюмировать, что портирование нормально написанного кода под Android не составляет особых проблем.
Исходники получившегося проекта лежат на github. Для сборки использовался NDK r8d.
Пришло время измерить производительность. Для этого был использован метод
Как можно увидеть из таблицы, применение оптимизированной нативной библиотеки позволяет значительно увеличит скорость шифрования. Стоит сказать, что библиотека Crypto++ активно использует интринсики из SIMD расширений SSEx при компиляции под x86.
В следующих статьях я расскажу о архитектуре java cryptography architecture и покажу как написать собственный криптопровайдер для нее. В результате будет создан криптопровайдер, реализующий криптоалгоритм AES с помощью нативного кода.
1. Режимы шифрования: http://ru.wikipedia.org
2. Библиотека Crypto++: http://www.cryptopp.com
3. Adnroid NDK: http://developer.android.com
Данным постом я хочу начать серию статей о создании модуля, выполняющего криптооперации на примере шифрования/расшифровки симметричным алгоритмом AES. Для начала необходимо понять, какой прирост производительности может дать применение нативного кода по сравнению со встроенной в ОС реализацией.
Чтобы не заниматься изобретением велосипеда и не реализовывать алгоритм AES заново, будет применена Open Source библиотека Crypto++, содержащая высокопроизводительные реализации многих криптоалгоритмов, включая криптоалгоритмы на эллиптических кривых(!). Используются многие аппаратные особенности современных процессоров от векторных инструкций типа SSE до выровненных распределителей памяти. Библиотека поддерживает множество операционных систем и компиляторов. Попробуем адаптировать ее для Android’а и вызывать ее из управляемого Java-кода.
Сборка библиотеки под Android
Для начала необходимо скачать исходники с сайта библиотеки.
После этого создадим проект Eclipse. Дальнейшие шаги:
- Написать в java-классе объявление метода, используя модификатор native.
- Добавить блок static, содержащий код, загружающий нативную библиотеку. Среда разработки скомпилирует *.class файл с байт-кодом.
- С помощью утилиты javah сгенерировать файл *.h с объявлениями С-функций из *.class файла.
- Написать реализации функций, используя библиотеку Crypto++.
- Сравнить производительность системного криптопровайдера и нативного кода.
Вначале стоит написать функции-заглушки, которые будут вызывать нативный код из управляемого.
Управляемые функции-заглушки
public class AES {
public static int KEY_SIZE = 16;
public static int IV_SIZE = 16;
static public class CBC {
private byte[] __key;
private byte[] __iv;
public CBC(byte[] key, byte[] iv) {
__key = key;
__iv = iv;
}
public CBC() {
__key = GenerateKey();
__iv = GenerateIV();
}
public native byte[] Encrypt(byte[] data);
public native byte[] Decrypt(byte[] data);
public byte[] GetKey() {
return __key;
}
public byte[] GetIV() {
return __iv;
}
}
public static native byte[] GenerateIV();
public static native byte[] GenerateKey();
static {
try {
System.loadLibrary("nativecryptowrapper");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Методы-заглушки для вызова нативного кода представляют собой обычные объявления методов с использованием ключевого слова
native
.Статический блок, выполняющейся при загрузке класса classloader’ом не содержит ничего, кроме вызова
System.loadLibrary
обрамленного блоком try-catch
. Следует обратить внимание, что аргументом вызова метода loadLibrary
является имя библиотеки БЕЗ префикса lib и расширения!Функции шифрования/расшифровки вынесены в inner-класс. Это сделано для того, чтобы явно показать то, что используется CBC-режим шифрования. После того, как класс написан и среда успешно откомпилировала его, можно выполнить шаг 3 (создание заголовков для нативного кода).
Также класс AES содержит 2 поля, в которых будут храниться ключ для шифрования и инициализационный вектор, которым инициализируется генератор псевдослучайных чисел.
По сохранению файла Eclipse скомпилирует исходник в class-файл, который понадобится позднее при генерации *.h заголовочных файлов.
Разработка нативной части
Нативная часть разбита на следующие компоненты:
- Библиотека Crypto++ — статическая библиотека cryptopp. Исходный код скопирован из скачанного архива в папку %PRJ%/jni/cryptopp.
- Динамическая библиотека nativecryptowrapper, экспортирующая jni-функции, вызываемые из управляемого Java-кода. Библиотека скомпонована с предыдущей бибилиотекой, в которой реализована криптография. Исходники лежат в папке %PRJ%/jni/nativecryptowrapper.
Все файлы нативной части располагаются в подпапке jni проекта Eclipse. Файлы каждого проекта находятся в подпапке
%PRJ%/bin/%NativePrjName%
Теперь можно приступить к реализации jni-методов, которые будут вызываться из управляемого кода. Для начала необходимо сгенерировать соответствующие заголовки (файлы *.h).
Необходимо в консоли зайти в папку
%PRJ%/jni/nativecryptowrapper
и оттуда запустить команду javah –classpath ../../bin/classes com.cryptodroid.AES
. Параметр -classpath
указывает где искать скомпилированные class-файлы. Внутри папки %PRJ%/bin/classes
class-файлы расположены в соответствии с пакетами, в которых они находятся, по этому конкретный путь до class-файла указывать ненужно. Второй параметр – полное имя класса для которого будет создаваться заголовочный файл *.h.Теперь остается реализовать функции, объявленные в заголовках. Реализация функции шифрования будет находиться в файле nativecryptowrapper/aes_base.cpp и приведена ниже:
Реализация jni-функций криптографии
JNIEXPORT jbyteArray JNICALL Java_com_cryptodroid_crypto_AES_00024CBC_Encrypt(JNIEnv* env, jobject obj, jbyteArray source) {
try {
std::vector<jbyte> key = to_vector(env, get_field_value<jbyteArray>(env, obj, "__key"));
std::vector<jbyte> iv = to_vector(env, get_field_value<jbyteArray>(env, obj, "__iv" ));
CryptoPP::CBC_Mode< CryptoPP::AES >::Encryption e;
e.SetKeyWithIV(
reinterpret_cast<byte*>(&key.front()),
KEY_SIZE,
reinterpret_cast<byte*>(&iv.front())
);
CryptoPP::StreamTransformationFilter filter (e);
jbytearray_holder data_holder(source, env);
filter.Put(reinterpret_cast<byte*>(&*data_holder.begin()), data_holder.size());
filter.MessageEnd();
jbyteArray result = env->NewByteArray(filter.MaxRetrievable());
if (!result) throw std::runtime_error("No memory!");
jbytearray_holder result_holder(result, env);
filter.Get(reinterpret_cast<byte*>(&*result_holder.begin()), result_holder.size());
return result;
} catch (std::exception& e) {
throw_jni_exception(env, e);
}
return NULL;
}
JNIEXPORT jbyteArray JNICALL Java_com_cryptodroid_crypto_AES_00024CBC_Decrypt(JNIEnv* env, jobject obj, jbyteArray source) {
try {
std::vector<jbyte> key = to_vector(env, get_field_value<jbyteArray>(env, obj, "__key"));
std::vector<jbyte> iv = to_vector(env, get_field_value<jbyteArray>(env, obj, "__iv" ));
CryptoPP::CBC_Mode< CryptoPP::AES >::Decryption d;
d.SetKeyWithIV(
reinterpret_cast<byte*>(&key.front()),
KEY_SIZE,
reinterpret_cast<byte*>(&iv.front())
);
CryptoPP::StreamTransformationFilter filter (d);
jbytearray_holder data_holder(source, env);
filter.Put(reinterpret_cast<byte*>(&*data_holder.begin()), data_holder.size());
filter.MessageEnd();
jbyteArray result = env->NewByteArray(filter.MaxRetrievable());
if (!result) throw std::runtime_error("No memory!");
jbytearray_holder result_holder(result, env);
filter.Get(reinterpret_cast<byte*>(&*result_holder.begin()), result_holder.size());
return result;
} catch (std::exception& e) {
throw_jni_exception(env, e);
}
return NULL;
}
Здесь стоит обратить внимание на дополнительные использованные функции:
to_vector
– функция, позволяющая преобразоватьjbyteArray
(jni-массив) в обычныйstd::vector<jbyte>
jbytearray_holder
– класс, инкапсулирующий, в соответствии с парадигмой RAII, управление памятью управляемого массиваthrow_jni_exception
– генерирует исключение для управляемого кодаget_field_value
– шаблонная функция. Пока существует только специализация для получения полей тапаbyte[]
, позднее, в случае надобности, будут добавлены другие специализации
Их исходный код показаны ниже:
Вспомогательные функции
const size_t KEY_SIZE = 16;
const size_t IV_SIZE = 16;
std::vector<jbyte> to_vector(JNIEnv* env, jbyteArray data) {
size_t data_len = env->GetArrayLength(data);
std::vector<jbyte> result(data_len);
if (data_len) {
env->GetByteArrayRegion(data, 0, data_len, &*result.begin());
}
return result;
}
class jbytearray_holder {
public:
typedef jbyte* iterator;
jbytearray_holder(jbyteArray& ar, JNIEnv* env): m_env(env), m_ar(ar) {
jboolean is_copy;
m_data = m_env->GetByteArrayElements(m_ar, &is_copy);
}
template<typename T>
T get_as() {
return reinterpret_cast<T>(m_data);
}
iterator begin() {
return reinterpret_cast<iterator>(m_data);
}
iterator end() {
return begin() + size();
}
size_t size() {
return m_env->GetArrayLength(m_ar);
}
~jbytearray_holder() {
m_env->ReleaseByteArrayElements(m_ar, m_data, 0);
}
private:
JNIEnv* m_env;
jbyte* m_data;
jbyteArray& m_ar;
jbytearray_holder(jbytearray_holder&);
jbytearray_holder& operator= (jbytearray_holder&);
};
void throw_jni_exception(JNIEnv* env, const std::exception& e) {
jclass excClass = env->FindClass("java/lang/IllegalArgumentException");
if (excClass) {
std::string message = "Exception from native code: ";
message += e.what();
env->ThrowNew(excClass, message.c_str());
}
}
template<typename T>
T get_field_value(JNIEnv* env, jobject obj, const std::string& field_name);
template<>
jbyteArray get_field_value<jbyteArray>(JNIEnv* env, jobject obj, const std::string& field_name) {
jclass clazz = env->GetObjectClass(obj);
if (!clazz)
throw std::runtime_error("No class!");
jfieldID fld = env->GetFieldID(clazz, field_name.c_str(), "[B");
jbyteArray result = static_cast<jbyteArray>(env->GetObjectField(obj, fld));
return result;
}
JNIEXPORT jbyteArray JNICALL Java_com_cryptodroid_crypto_AES_00024CBC_Encrypt(JNIEnv* env, jobject obj, jbyteArray source) {
try {
std::vector<jbyte> key = to_vector(env, get_field_value<jbyteArray>(env, obj, "__key"));
std::vector<jbyte> iv = to_vector(env, get_field_value<jbyteArray>(env, obj, "__iv" ));
CryptoPP::CBC_Mode< CryptoPP::AES >::Encryption e;
e.SetKeyWithIV(
reinterpret_cast<byte*>(&key.front()),
KEY_SIZE,
reinterpret_cast<byte*>(&iv.front())
);
CryptoPP::StreamTransformationFilter filter (e);
jbytearray_holder data_holder(source, env);
filter.Put(reinterpret_cast<byte*>(&*data_holder.begin()), data_holder.size());
filter.MessageEnd();
jbyteArray result = env->NewByteArray(filter.MaxRetrievable());
if (!result) throw std::runtime_error("No memory!");
jbytearray_holder result_holder(result, env);
filter.Get(reinterpret_cast<byte*>(&*result_holder.begin()), result_holder.size());
return result;
} catch (std::exception& e) {
throw_jni_exception(env, e);
}
return NULL;
}
Вызов системного криптопровайдера производился таким образом:
Cipher _e = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv);
_e.init(Cipher.ENCRYPT_MODE, skeySpec, ivspec);
byte[] encrypted = _e.doFinal(data);
Cipher _d = Cipher.getInstance("AES/CBC/PKCS5Padding");
_d.init(Cipher.DECRYPT_MODE, skeySpec, ivspec);
byte[] decrypted = _d.doFinal(encrypted);
Вызов нативной реализации криптопровайдера осуществлялся так:
AES.CBC e = new AES.CBC(iv, key);
byte[] encrypted = e.Encrypt(data);
byte[] decrypted = e.Decrypt(encrypted);
Сборка проекта
Процесс сборки нативной части, осуществляемый с помощью утилиты ndk-build, входящей в состав NDK, конфигурируют файл Android.mk и Application.mk. Для сборки проекта необходимо записать значения в переменные, описывающие проект:
LOCAL_SRC_FILES
— список исходных файловLOCAL_MODULE
– название модуляLOCAL_STATIC_LIBRARIES
– список статических библиотек, которые необходимо использовать при компоновке (дополнительно)LOCAL_CFLAGS
– дополнительные флаги компилятора (если нужно)
Описание проект в файле Android.mk состоит из:
- Вызова макроса
CLEAR_VARS
- Изменения необходимых переменных (см.выше)
- Вызова одного из макросов
BUILD_xxx
, напримерBUILD_STATIC_LIBRARY
илиBUILD_SHARED_LIBRARY
В файле Application.mk содержатся более глобальные настройки. Были использованы следующие опции:
APP_STL
– флаг использования STLAPP_ABI
– список целевых архитектурAPP_OPTIM
– тип билда (отладочный/релизный)APP_PLATFORM
– указание целевой платформы
Получившийся файл Android.mk (о дополнительных флагах подробнее ниже), описывающий все три проекта:
Andriod.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := nativecryptowrapper
LOCAL_CFLAGS := -fexceptions -frtti
LOCAL_SRC_FILES := nativecryptowrapper/aes_base.cpp
LOCAL_STATIC_LIBRARIES := cryptopp
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := cryptopp
LOCAL_CFLAGS := -fexceptions -frtti
LOCAL_SRC_FILES := \
cryptopp/3way.cpp \
....
cryptopp/zdeflate.cpp \
cryptopp/zinflate.cpp \
cryptopp/zlib.cpp
include $(BUILD_STATIC_LIBRARY)
Содержимое файла Application.mk:
APP_STL := gnustl_static
APP_ABI := armeabi armeabi-v7a x86
APP_OPTIM := release
APP_PLATFORM=android-9
Чтобы статическая библиотека, содержащая Crypto++ начала корректно собираться, были произведены следующие изменения:
- Добавлены флаги компиляции
-fexceptions
и-frtti
в переменнуюLOCAL_CFLAGS
файла Android.mk (так как библиотека Crypto++ использует и исключения и приведения dynamic_cast) - Добавлена зависимость от STL (реализация STL — gnustl_static) в файл Application.mk
- Добавлена поддержка аппаратных архитектур (x86, arm)
Можно резюмировать, что портирование нормально написанного кода под Android не составляет особых проблем.
Исходники получившегося проекта лежат на github. Для сборки использовался NDK r8d.
Запуск
Пришло время измерить производительность. Для этого был использован метод
System. nanoTime()
из стандартной библиотеки классов Java в Android. Замеры проводились на: Megafon Mint, Pocketbook A10. Результаты следующие:Megafon Mint, мс | Pocketbook A10, мс | |
---|---|---|
Системный криптопровайдер | 998 | 1835 |
Библиотека Crypto++ | 231 | 970 |
Ускорение | 4.3х | 1.9x |
Как можно увидеть из таблицы, применение оптимизированной нативной библиотеки позволяет значительно увеличит скорость шифрования. Стоит сказать, что библиотека Crypto++ активно использует интринсики из SIMD расширений SSEx при компиляции под x86.
В следующих статьях я расскажу о архитектуре java cryptography architecture и покажу как написать собственный криптопровайдер для нее. В результате будет создан криптопровайдер, реализующий криптоалгоритм AES с помощью нативного кода.
Полезные ссылки:
1. Режимы шифрования: http://ru.wikipedia.org
2. Библиотека Crypto++: http://www.cryptopp.com
3. Adnroid NDK: http://developer.android.com