company_banner

Ускорение криптоопераций или опыт портирования под 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
    Intel
    137.32
    Company
    Share post

    Comments 13

      –4
      «Это увеличивает его переносимость». Чем это увеличивает переносимость? Что вы имели этим ввиду? Может я не так понял.
      По мне так натив куда переносимее. Crypto++ вон хоть под iOs компиль хоть под что.
      • UFO just landed and posted this here
          0
          Не соглашусь, что нативный код более переносимый. java-приложение одинаково работает везде, где есть jre. А для того, чтобы пересобрать нативное приложение под другую платформу порой приходится затратить много усилий.
            0
            … чему, в каком-то смысле, эта статья доказательство
              –2
              Ну если в таком ключе говорить о переносимости, то можно и согласиться.
              Я же имел ввиду переносимость в более широком смысле, в смысле о самой возможности переноса на другие платформы… а тут если нет этого jre, то ты хоть тресни — не будет работать. scummvm.org/ смеется над переносимостью jar.
              А насчет много усилий… Приведенное в этой статье — много усилий? Эти усилия настолько мелочны в сравнении с профитом от этих усилий, что ими можно и пренебречь.
            +3
            Вот мне интересно, у вас действительно шифрование узкое место…

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

            Такое впичетление что нужно было просто показать где на интел процесорах можно получить небольшое большое приемущество по сравнению с соперниками
              0
              В первую очередь я хотел посмотреть, насколько сложно портирование библиотеки под Android и сколько можно получить дополнительного быстродействия при этом. Полученное двукратное ускорение в некоторых случаях будет серьезным доводом для того, чтобы усложнить разработку.
                0
                Что там Кнут говорил про преждевременную оптимизацию? :) Редко в каких приложениях криптография будет бутылочным горлышком. Особенно под мобильные платформы.

                Кстати, тестировали вы AES, который и так очень быстр, к тому же создавался с расчем того, что будет выполняться на generic-процессорах.
                А проверьте реализацию RSA или эллиптических кривых.
                +1
                К примеру для vpn и ssl соединений, или какого либо крипто контейнера шифрование необходимо. Дело ещё и в эффективности — чем производительнее код — тем меньше будет потрачено энергии, и тем больше времени останется на выполнение других задач, так что в условиях достаточно жёсткого ограничения в производительности железа, и запаса энергии — 2х кратный выигрыш это очень даже хорошо. Отработка же механизма портирования достаточно сложной библиотеки полезна сама по себе.
                  0
                  Честно говоря не видел ни одного Android девайса, где VPN или криптоконтейнер реализован на Java. Так же как и SSL, впрочем.
                  SSL обычно реализуется силами openssl где есть эфективные реализации всех криптоалгоримтов.

                  Криптоконтейнеры вообще делают на уровне ядра. Где опять же есть Kernel Crypto API и очень часто — поддержка аппаратной криптографии.

                  VPN же делается силами либо OpenVPN либо IPSec опять же на уровне ядра.

                  Так что остаётся только какая-то сильно прикладная app-specific криптография, типа проверки валидности лицензионных ключей…
                  0
                  У меня вот узкое место. Вполне реальная ситуация — получения доступа к зашифрованной базе. Чтобы всё сделать как надо, применяем pbkdf2 — раундов под 10000 как у iphone 5. генерация ключа шифрования и проверки почти две секунды. То есть пользователю надо ждать две секунды пока что-то там посчитается. Разница в 3,5 раза сделает работу приложения гораздо более приятной. Только в моём случае не андройд, где возможен нативный код, а сильверлайт и wp7 где такой возможности нет. по тестам на большом компьютере проигрыш как раз 3,5 раза. Если предположить, что пропорции сохранятся как в этом посте, то переписывание sha2 в нейтив на wp7 привело бы к ещё большей выгоде.
                    0
                    Ну pbkdf2 как раз для того и придумывали, что бы ключи вычислялись медленно :)
                    У меня есть пара вопросов:
                    — зачем 10 000 раундов? Если верить википедии, то 4096 рауднов SHA1 приводят к тому, что можно сгенерить всего 70 ключей в секунду на Intel Core2. Мне кажется, этого более чем достаточно.
                    — Честно говоря не работал с силверлайт на wp7, но помню что в большом .NET Framework есть очень хороший и удобный CryptoAPI с бекендами в нативном коде. Для WP7 такого не сущесвует?
                      0
                      — затем что на iphone5 10000 раундов и ниже уже не очень хорошо. В RIM тоже видимо думали — зачем много и поставили вообще 1 ;) Elcomsoft их удачно брутфорсили по этому поводу. Я не в курсе о реализации про которую говорится в википедии, но у меня с sha256 на Core 2 quad при 10000 раундов те же 80 ключей в секунду (все вектора проходят проверку) и это без каких либо особых оптимизиций. А это всё же обычный десктопный процессор, причём старый.
                      — Всё было бы вполне прилично, если бы было так. Разница на PC между c и c# — 3,5 раза (в один поток). И профайлер VS показывает 95% времени в TransformBlock — Так что сомнительно что CryptoAPI в .NET нативный хоть в каком-то виде.

                      Конечно можно забить и уменьшить раунды до 4000 или даже 2000, но лишние ресурсы всё равно будут тратиться на вычисления и вообще компромиссы в безопасности выходят зачастую боком.

                Only users with full accounts can post comments. Log in, please.