📜 ⬆️ ⬇️

Acceleration of crypto operations or porting experience for Android

The Android platform includes the Bouncycastle framework, which is designed to perform crypto operations, such as encryption or digital signature verification. A distinctive feature of this framework is that it is entirely written in Java, without the use of native code. This increases its portability, but significantly reduces the speed. In the first approximation, the implementation of crypto functions using native code can give a significant performance boost. This can significantly increase the speed of an application using cryptography. Let's see if this assumption is confirmed.
With this post, I want to start a series of articles on the creation of a module that performs crypto operations using the example of encryption / decryption using a symmetric AES algorithm. To begin with, it is necessary to understand how much performance gain the use of native code can give compared to the implementation built into the OS.

In order not to reinvent the bicycle and not re-implement the AES algorithm, the Crypto ++ Open Source library containing high-performance implementations of many cryptoalgorithms, including cryptoalgorithms on elliptic curves (!), Will be applied. Many hardware features of modern processors, from vector instructions such as SSE to aligned memory allocators, are used. The library supports many operating systems and compilers. Let's try to adapt it for Android and call it from managed Java code.

Build a library for Android

First you need to download the source from the library site .
After that, create an Eclipse project. Next steps:
  1. Write the method declaration in the java-class using the native modifier.
  2. Add a static block containing the code that loads the native library. The development environment will compile a * .class file with bytecode.
  3. Using the javah utility, generate the * .h file with C-function declarations from the * .class file.
  4. Write implementations of functions using the Crypto ++ library.
  5. Compare the performance of the system crypto-provider and the native code.

First, write stub functions that will call native code from managed code.
Managed Stub Functions
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(); } } } 

Stub methods for invoking native code are normal method declarations using the native keyword.
A static block running when the class is loaded with a classloader contains nothing but a call to System.loadLibrary surrounded by a try-catch . It should be noted that the argument of the call to the loadLibrary method is the name of the library WITHOUT the lib prefix and the extension!
Encryption / decryption functions are in the inner-class. This is done in order to clearly show what is used CBC-encryption mode. After the class has been written and the environment has successfully compiled it, you can perform step 3 (creating headers for native code).
Also, the AES class contains 2 fields in which the key for encryption and the initialization vector will be stored, with which the pseudo-random number generator is initialized.
To save the file, Eclipse will compile the source file into a class file, which will be needed later when generating * .h header files.

Development of the native part

The native part is divided into the following components:
  1. The Crypto ++ library is a cryptopp static library. The source code is copied from the downloaded archive to the% PRJ% / jni / cryptopp folder.
  2. The nativecryptowrapper dynamic library, which exports jni functions called from managed Java code. The library is linked with the previous library in which cryptography is implemented. Sources are in the% PRJ% / jni / nativecryptowrapper folder.

All files of the native part are located in the jni subfolder of the Eclipse project. Each project files are located in the %PRJ%/bin/%NativePrjName%
Now you can start implementing jni-methods that will be called from managed code. First you need to generate the appropriate headers (* .h files).
You need to go to the %PRJ%/jni/nativecryptowrapper folder in the console and from there start the javah –classpath ../../bin/classes com.cryptodroid.AES . The -classpath parameter specifies where to look for compiled class files. Inside the %PRJ%/bin/classes class files are arranged according to the packages in which they are located, so it’s not necessary to specify a specific path to the class file. The second parameter is the full name of the class for which the header file * .h will be created.
It now remains to implement the functions declared in the headers. The implementation of the encryption function will be in the file nativecryptowrapper / aes_base.cpp and is shown below:
Implementation of jni-functions of cryptography
 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; } 


Here you should pay attention to the additional functions used:
')
  1. to_vector is a function that allows you to convert jbyteArray (jni-array) to a regular std::vector<jbyte>
  2. jbytearray_holder - a class that encapsulates, in accordance with the RAII paradigm, memory management of a managed array
  3. throw_jni_exception - throws an exception for managed code
  4. get_field_value is a template function. While there is only a specialization for getting fields of the byte[] tapa, later, if necessary, other specializations will be added.


Their source code is shown below:
Secondary functions
 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; } 


The call to the system crypto-provider was made as follows:
 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); 

The call to the native implementation of the crypto-provider was made as follows:
 AES.CBC e = new AES.CBC(iv, key); byte[] encrypted = e.Encrypt(data); byte[] decrypted = e.Decrypt(encrypted); 


Build project

The native part build process, implemented using the ndk-build utility included in the NDK, configures the Android.mk file and Application.mk. To build a project, you need to write the values ​​in the variables describing the project:
  1. LOCAL_SRC_FILES - list of source files
  2. LOCAL_MODULE - module name
  3. LOCAL_STATIC_LIBRARIES - a list of static libraries to be used when building (optional)
  4. LOCAL_CFLAGS - additional compiler flags (if needed)

Description of the project in the file Android.mk consists of:
  1. CLEAR_VARS macro
  2. Changes to required variables (see above)
  3. BUILD_xxx one of the BUILD_xxx macros, for example BUILD_STATIC_LIBRARY or BUILD_SHARED_LIBRARY

The Application.mk file contains more global settings. The following options were used:
  1. APP_STL - STL Usage Flag
  2. APP_ABI - list of target architectures
  3. APP_OPTIM - build type (debug / release)
  4. APP_PLATFORM - target platform indication

The resulting Android.mk file (for more flags on more details below), describing all three projects:
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) 



Contents of the Application.mk file:
 APP_STL := gnustl_static APP_ABI := armeabi armeabi-v7a x86 APP_OPTIM := release APP_PLATFORM=android-9 


To make the static library containing Crypto ++ start to build correctly, the following changes were made:

It can be summarized that porting a normally written code for Android does not constitute a special problem.
The sources of the resulting project are on github . NDK r8d was used for the assembly.

Launch

It's time to measure performance. For this, the System. nanoTime() method was used System. nanoTime() System. nanoTime() from the standard Java class library on Android. Measurements were taken on: Megafon Mint, Pocketbook A10. The results are as follows:
Megafon Mint, msPocketbook A10, ms
System Crypt Provider9981835
Crypto ++ library231970
Acceleration4.3x1.9x

As can be seen from the table, the use of an optimized native library will significantly increase the encryption speed. It is worth saying that the library Crypto ++ actively uses intrinsiki from SIMD extensions SSEx when compiling under x86.
In the following articles I will talk about the architecture of the java cryptography architecture and show you how to write your own crypto-provider for it. As a result, a crypto-provider will be created that implements the AES cryptographic algorithm using the native code.

Useful links:

1. Encryption modes: http://ru.wikipedia.org
2. Crypto ++ library: http://www.cryptopp.com
3. Adnroid NDK: http://developer.android.com

Source: https://habr.com/ru/post/170517/


All Articles