通过 JNI 调用 OpenSSL 实现加密解密

y37f 5年前

Intel® Developer Zone 为跨平台app开发提供工具和信息指引,平台和技术信息,示例代码,以及同行专家来帮助开发者创新和成功。加入我们的 Android, Internet of Things, Intel® RealSense™ Technology, 以及 Windows社区来下载工具,获取开发套件,与志趣相投的开发者分享观点,参与编程马拉松,竞赛,宣传以及本地事件。

这个博客概括了通过OpenSSL库整合Intel的AES-NI指令到Android应用的步骤,通过下面的过程,你可以构建一个被AES-NI加速的JNI程序。

Intel 高级加密标准新操作指南(Intel AES-NI)

Intel AES-NI 在 2008 年 3 月提出,是 Inter 微处理器 x86 指令集架构的一个扩展,这个指令集的目的是提高应用程序使用高级加密标准(AES)进行加密和解密时的性能、安全性、以及执行效率。

在 Android 上使用 Intel AES-NI

OpenSSL 库的 AES 算法比 Java 原生提供的有显著的性能提升,这是因为 OpenSSL 库是为 Inter 处理器优化的并且使用了AES-NI指令。下面是一个一步一步的如何使用OpenSSL来加密一个文件的描述。

Android 4.3 开始,安卓开源工程(AOSP)中的 OpenSSL 支持 Inter AES-NI,所以你仅需使用正确的配置来编译它。另外,你可以从官方网站下载并自己编译,然后在你的工程中直接使用 *.a/*.so,有两种方式获得加密库。

如果你没有AOSP源代码,你可以从http://www.openssl.org/source/下载OpenSSL源代码。使用最新的OpenSSL版本可以避免任何已知的旧版本缺陷。AOSP集成了OpenSSL库,可以直接将它放到应用程序的jni目录来访问已包含的目录。

如果你正在下载OpenSSL源代码,并打算通过交叉编译来创建库,请按照下面的步骤进行:

  1. 下载源代码:
    wget https://www.openssl.org/source/openssl-1.0.1j.tar.gz

  2. 编译 ‒ 在控制台运行下面的命令(注意,你需要设置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 &ndash;j8          cd external/openssl          mm

这创建了 libcrypto.a,并放到目录 out/host/linux_x86/bin

通过NDK在Android项目中使用OpenSSL

  1. 创建一个 android项目,在你最喜欢的IDE中加密文件 -- 这个例子基于 Eclipse。

  2. 通过Android.mk将OpenSSL相关的函数声明为 native 函数

  3. 在Android项目源代码下创建一个jni目录。

  4. 将之前编译的文件,include目录放置到jni目录下。

  5. 包含在jni目录下创建的OpenSSL库目录<OpenSSL source/include/>。

  6. 接下来在 jni/*.c 中编写C函数来实现加密。完成之后,你需要拷贝 *.a/*.so 以及头文件到项目之中。

  7. 在步骤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 &ndash;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提供的函数来实现 加密/解密/SSL 函数。 为了使用Intel AES-NI, 只需要使用下面的EVP_* 系列函数, 如果CPU支持,这些函数会自动使用Intel 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.");          }                           }          /* native function available from encodeFile library */      public native int encodeFileFromJNI(String fileOut, String fileIn);      public native void setBlocksizeFromJNI(int blocksize);      public native byte[] generateKeyFromJNI(int keysize);                /* To load the library that encrypts (encodeFile) on application startup.       * The Package manager would have alredy unpacked the library has into /data/data/com.example.openssldataencryption/lib/libencodeFile.so       * at installation time.       */      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];          // create key, if it&apos;s uninitialized        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;          /**       * ENCRYPT       */        //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");        }               // go through file, and encrypt        if ( orig_file != NULL ) {              origData = new unsigned char[aes_blocksize];              encData = new unsigned char[aes_blocksize+EVP_CIPHER_CTX_block_size(e_ctx)]; // potential for encryption to be 16 bytes longer than original                printf( "Encoding file: %s\n", filename);                bytesread = fread(origData, 1, aes_blocksize, orig_file);              // read bytes from file, then send to cipher              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 );                    // read more bytes                    bytesread = fread(origData, 1, aes_blocksize, orig_file);              }              // last step encryption              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 );                // free cipher              EVP_CIPHER_CTX_free(e_ctx);                //    close files              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进行编译。

/<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占用率

下面的代码可以帮助我们了解cpu平均使用率 (利用存储在/proc/stat的信息)

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占用率

下面的代码读取可用的系统内存.

Memory Info 是一个Android API,它允许我们查询可用的内存信息.

由于, 1024 Bytes = 1 kB & 1024 kB = 1 MB. 因此, 转换可用内存到 MB - 1024*1024 == 1048576

public long readMem(ActivityManager am) {              MemoryInfo mi = new MemoryInfo();              am.getMemoryInfo(mi);              long availableMegs = mi.availMem / 1048576L;              return availableMegs;        }

定时分析

start = System.currentTimeMillis();  // Perform Encryption.  stop = System.currentTimeMillis();  seconds = (stop - start);