Ускорение криптоопераций или опыт портирования под Android

в 6:06, , рубрики: android, c++, Блог компании Intel, криптография, Программирование, Разработка под android, метки: , , ,

Ускорение криптоопераций или опыт портирования под AndroidВ состав платформы Android входит фреймворк Bouncycastle, предназначенный для выполнения криптоопераций, например, шифрования или проверки цифровой подписи. Отличительной чертой данного фреймворка является то, что он целиком написан на Java, без применения нативного кода. Это увеличивает его переносимость, однако значительно уменьшает быстродействие. В первом приближении реализация криптофункций с помощью нативного кода может дать значительный прирост производительности. Это может существенно увеличить быстродействие приложения, использующего криптографию. Посмотрим, подтвердится ли это предположение.
Данным постом я хочу начать серию статей о создании модуля, выполняющего криптооперации на примере шифрования/расшифровки симметричным алгоритмом AES. Для начала необходимо понять, какой прирост производительности может дать применение нативного кода по сравнению со встроенной в ОС реализацией.

Чтобы не заниматься изобретением велосипеда и не реализовывать алгоритм AES заново, будет применена Open Source библиотека Crypto++, содержащая высокопроизводительные реализации многих криптоалгоритмов, включая криптоалгоритмы на эллиптических кривых(!). Используются многие аппаратные особенности современных процессоров от векторных инструкций типа SSE до выровненных распределителей памяти. Библиотека поддерживает множество операционных систем и компиляторов. Попробуем адаптировать ее для Android’а и вызывать ее из управляемого Java-кода.

Сборка библиотеки под Android

Для начала необходимо скачать исходники с сайта библиотеки.
После этого создадим проект Eclipse. Дальнейшие шаги:

  1. Написать в java-классе объявление метода, используя модификатор native.
  2. Добавить блок static, содержащий код, загружающий нативную библиотеку. Среда разработки скомпилирует *.class файл с байт-кодом.
  3. С помощью утилиты javah сгенерировать файл *.h с объявлениями С-функций из *.class файла.
  4. Написать реализации функций, используя библиотеку Crypto++.
  5. Сравнить производительность системного криптопровайдера и нативного кода.

Вначале стоит написать функции-заглушки, которые будут вызывать нативный код из управляемого.

Управляемые функции-заглушки

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 заголовочных файлов.

Разработка нативной части

Нативная часть разбита на следующие компоненты:

  1. Библиотека Crypto++ — статическая библиотека cryptopp. Исходный код скопирован из скачанного архива в папку %PRJ%/jni/cryptopp.
  2. Динамическая библиотека 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;
}

Здесь стоит обратить внимание на дополнительные использованные функции:

  1. to_vector – функция, позволяющая преобразовать jbyteArray (jni-массив) в обычный std::vector<jbyte>
  2. jbytearray_holder – класс, инкапсулирующий, в соответствии с парадигмой RAII, управление памятью управляемого массива
  3. throw_jni_exception – генерирует исключение для управляемого кода
  4. 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. Для сборки проекта необходимо записать значения в переменные, описывающие проект:

  1. LOCAL_SRC_FILES — список исходных файлов
  2. LOCAL_MODULE – название модуля
  3. LOCAL_STATIC_LIBRARIES – список статических библиотек, которые необходимо использовать при компоновке (дополнительно)
  4. LOCAL_CFLAGS – дополнительные флаги компилятора (если нужно)

Описание проект в файле Android.mk состоит из:

  1. Вызова макроса CLEAR_VARS
  2. Изменения необходимых переменных (см.выше)
  3. Вызова одного из макросов BUILD_xxx, например BUILD_STATIC_LIBRARY или BUILD_SHARED_LIBRARY

В файле Application.mk содержатся более глобальные настройки. Были использованы следующие опции:

  1. APP_STL – флаг использования STL
  2. APP_ABI – список целевых архитектур
  3. APP_OPTIM – тип билда (отладочный/релизный)
  4. 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

Автор: RainM

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js